diff --git a/packages/flutter/lib/src/widgets/sliver.dart b/packages/flutter/lib/src/widgets/sliver.dart index 58ff7fe17fa..9074396ebab 100644 --- a/packages/flutter/lib/src/widgets/sliver.dart +++ b/packages/flutter/lib/src/widgets/sliver.dart @@ -738,6 +738,20 @@ class SliverMultiBoxAdaptorElement extends RenderObjectElement implements Render }); } + @override + Element updateChild(Element child, Widget newWidget, dynamic newSlot) { + final SliverMultiBoxAdaptorParentData oldParentData = child?.renderObject?.parentData; + final Element newChild = super.updateChild(child, newWidget, newSlot); + final SliverMultiBoxAdaptorParentData newParentData = newChild?.renderObject?.parentData; + + // Preserve the old layoutOffset if the renderObject was swapped out. + if (oldParentData != newParentData && oldParentData != null && newParentData != null) { + newParentData.layoutOffset = oldParentData.layoutOffset; + } + + return newChild; + } + @override void forgetChild(Element child) { assert(child != null); diff --git a/packages/flutter/test/widgets/sliver_list_test.dart b/packages/flutter/test/widgets/sliver_list_test.dart new file mode 100644 index 00000000000..1a124f8a3d8 --- /dev/null +++ b/packages/flutter/test/widgets/sliver_list_test.dart @@ -0,0 +1,181 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; + +void main() { + testWidgets('SliverList reverse children (with keys)', (WidgetTester tester) async { + final List items = new List.generate(20, (int i) => i); + const double itemHeight = 300.0; + const double viewportHeight = 500.0; + + const double scrollPosition = 18 * itemHeight; + final ScrollController controller = new ScrollController(initialScrollOffset: scrollPosition); + + await tester.pumpWidget(_buildSliverList( + items: items, + controller: controller, + itemHeight: itemHeight, + viewportHeight: viewportHeight, + )); + await tester.pumpAndSettle(); + + expect(controller.offset, scrollPosition); + expect(find.text('Tile 0'), findsNothing); + expect(find.text('Tile 1'), findsNothing); + expect(find.text('Tile 18'), findsOneWidget); + expect(find.text('Tile 19'), findsOneWidget); + + await tester.pumpWidget(_buildSliverList( + items: items.reversed.toList(), + controller: controller, + itemHeight: itemHeight, + viewportHeight: viewportHeight, + )); + final int frames = await tester.pumpAndSettle(); + expect(frames, 1); // ensures that there is no (animated) bouncing of the scrollable + + expect(controller.offset, scrollPosition); + expect(find.text('Tile 19'), findsNothing); + expect(find.text('Tile 18'), findsNothing); + expect(find.text('Tile 1'), findsOneWidget); + expect(find.text('Tile 0'), findsOneWidget); + + controller.jumpTo(0.0); + await tester.pumpAndSettle(); + + expect(controller.offset, 0.0); + expect(find.text('Tile 19'), findsOneWidget); + expect(find.text('Tile 18'), findsOneWidget); + expect(find.text('Tile 1'), findsNothing); + expect(find.text('Tile 0'), findsNothing); + }); + + testWidgets('SliverList replace children (with keys)', (WidgetTester tester) async { + final List items = new List.generate(20, (int i) => i); + const double itemHeight = 300.0; + const double viewportHeight = 500.0; + + const double scrollPosition = 18 * itemHeight; + final ScrollController controller = new ScrollController(initialScrollOffset: scrollPosition); + + await tester.pumpWidget(_buildSliverList( + items: items, + controller: controller, + itemHeight: itemHeight, + viewportHeight: viewportHeight, + )); + await tester.pumpAndSettle(); + + expect(controller.offset, scrollPosition); + expect(find.text('Tile 0'), findsNothing); + expect(find.text('Tile 1'), findsNothing); + expect(find.text('Tile 18'), findsOneWidget); + expect(find.text('Tile 19'), findsOneWidget); + + await tester.pumpWidget(_buildSliverList( + items: items.map((int i) => i + 100).toList(), + controller: controller, + itemHeight: itemHeight, + viewportHeight: viewportHeight, + )); + final int frames = await tester.pumpAndSettle(); + expect(frames, 1); // ensures that there is no (animated) bouncing of the scrollable + + expect(controller.offset, scrollPosition); + expect(find.text('Tile 0'), findsNothing); + expect(find.text('Tile 1'), findsNothing); + expect(find.text('Tile 18'), findsNothing); + expect(find.text('Tile 19'), findsNothing); + + expect(find.text('Tile 100'), findsNothing); + expect(find.text('Tile 101'), findsNothing); + expect(find.text('Tile 118'), findsOneWidget); + expect(find.text('Tile 119'), findsOneWidget); + + controller.jumpTo(0.0); + await tester.pumpAndSettle(); + + expect(controller.offset, 0.0); + expect(find.text('Tile 100'), findsOneWidget); + expect(find.text('Tile 101'), findsOneWidget); + expect(find.text('Tile 118'), findsNothing); + expect(find.text('Tile 119'), findsNothing); + }); + + testWidgets('SliverList replace with shorter children list (with keys)', (WidgetTester tester) async { + final List items = new List.generate(20, (int i) => i); + const double itemHeight = 300.0; + const double viewportHeight = 500.0; + + final double scrollPosition = items.length * itemHeight - viewportHeight; + final ScrollController controller = new ScrollController(initialScrollOffset: scrollPosition); + + await tester.pumpWidget(_buildSliverList( + items: items, + controller: controller, + itemHeight: itemHeight, + viewportHeight: viewportHeight, + )); + await tester.pumpAndSettle(); + + expect(controller.offset, scrollPosition); + expect(find.text('Tile 0'), findsNothing); + expect(find.text('Tile 1'), findsNothing); + expect(find.text('Tile 17'), findsNothing); + expect(find.text('Tile 18'), findsOneWidget); + expect(find.text('Tile 19'), findsOneWidget); + + await tester.pumpWidget(_buildSliverList( + items: items.sublist(0, items.length - 1), + controller: controller, + itemHeight: itemHeight, + viewportHeight: viewportHeight, + )); + final int frames = await tester.pumpAndSettle(); + expect(frames, greaterThan(1)); // ensure animation to bring tile17 into view + + expect(controller.offset, scrollPosition - itemHeight); + expect(find.text('Tile 0'), findsNothing); + expect(find.text('Tile 1'), findsNothing); + expect(find.text('Tile 17'), findsOneWidget); + expect(find.text('Tile 18'), findsOneWidget); + expect(find.text('Tile 19'), findsNothing); + }); +} + +Widget _buildSliverList({ + List items: const [], + ScrollController controller, + double itemHeight: 500.0, + double viewportHeight: 300.0, +}) { + return new Directionality( + textDirection: TextDirection.ltr, + child: new Center( + child: new Container( + height: viewportHeight, + child: new CustomScrollView( + controller: controller, + slivers: [ + new SliverList( + delegate: new SliverChildBuilderDelegate( + (BuildContext context, int i) { + return new Container( + key: new ValueKey(items[i]), + height: itemHeight, + child: new Text('Tile ${items[i]}'), + ); + }, + childCount: items.length, + ), + ), + ], + ), + ), + ), + ); +}