From 765e5d5b5bb40acc7b334cceeef91e3fbf332f79 Mon Sep 17 00:00:00 2001 From: Hans Muller Date: Tue, 16 May 2017 11:56:55 -0700 Subject: [PATCH] Added SliverPrototypeExtentList et al (#10097) --- .../rendering/sliver_fixed_extent_list.dart | 4 + packages/flutter/lib/src/widgets/basic.dart | 8 +- packages/flutter/lib/src/widgets/sliver.dart | 14 +- .../widgets/sliver_prototype_item_list.dart | 183 ++++++++++++++++++ packages/flutter/lib/widgets.dart | 1 + .../sliver_prototype_item_extent_test.dart | 133 +++++++++++++ 6 files changed, 339 insertions(+), 4 deletions(-) create mode 100644 packages/flutter/lib/src/widgets/sliver_prototype_item_list.dart create mode 100644 packages/flutter/test/widgets/sliver_prototype_item_extent_test.dart diff --git a/packages/flutter/lib/src/rendering/sliver_fixed_extent_list.dart b/packages/flutter/lib/src/rendering/sliver_fixed_extent_list.dart index 417774235e1..3ba694e1914 100644 --- a/packages/flutter/lib/src/rendering/sliver_fixed_extent_list.dart +++ b/packages/flutter/lib/src/rendering/sliver_fixed_extent_list.dart @@ -28,6 +28,8 @@ import 'sliver_multi_box_adaptor.dart'; /// See also: /// /// * [RenderSliverFixedExtentList], which has a configurable [itemExtent]. +/// * [RenderSliverPrototypeExtentList], which uses a prototype list item +/// instead of a pixel value, to define the extent of each item. /// * [RenderSliverFillViewport], which determines the [itemExtent] based on /// [SliverConstraints.viewportMainAxisExtent]. /// * [RenderSliverFillRemaining], which determines the [itemExtent] based on @@ -231,6 +233,8 @@ abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAda /// /// See also: /// +/// * [RenderSliverPrototypeExtentList], which uses a prototype list item +/// instead of a pixel value, to define the extent of each item. /// * [RenderSliverList], which does not require its children to have the same /// extent in the main axis. /// * [RenderSliverFillViewport], which determines the [itemExtent] based on diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart index 10ab47c12b9..f299d5e2d38 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -1557,9 +1557,9 @@ class Baseline extends SingleChildRenderObjectWidget { /// /// Rather than using multiple [SliverToBoxAdapter] widgets to display multiple /// box widgets in a [CustomScrollView], consider using [SliverList], -/// [SliverFixedExtentList], or [SliverGrid], which are more efficient because -/// they instantiate only those children that are actually visible through the -/// scroll view's viewport. +/// [SliverFixedExtentList], [SliverPrototypeExtentList], or [SliverGrid], +/// which are more efficient because they instantiate only those children that +/// are actually visible through the scroll view's viewport. /// /// See also: /// @@ -1567,6 +1567,8 @@ class Baseline extends SingleChildRenderObjectWidget { /// * [SliverList], which displays multiple box widgets in a linear array. /// * [SliverFixedExtentList], which displays multiple box widgets with the /// same main-axis extent in a linear array. +/// * [SliverPrototypeExtentList], which displays multiple box widgets with the +/// same main-axis extent as a prototype item, in a linear array. /// * [SliverGrid], which displays multiple box widgets in arbitrary positions. class SliverToBoxAdapter extends SingleChildRenderObjectWidget { /// Creates a sliver that contains a single box widget. diff --git a/packages/flutter/lib/src/widgets/sliver.dart b/packages/flutter/lib/src/widgets/sliver.dart index 3ce5e49fd28..282c26bfacc 100644 --- a/packages/flutter/lib/src/widgets/sliver.dart +++ b/packages/flutter/lib/src/widgets/sliver.dart @@ -304,6 +304,9 @@ abstract class SliverMultiBoxAdaptorWidget extends RenderObjectWidget { /// /// * [SliverFixedExtentList], which is more efficient for children with /// the same extent in the main axis. +/// * [SliverPrototypeExtentList], which is similar to [SliverFixedExtentList] +/// except that it uses a prototype list item intead a pixel value to define +/// the main axis extent of each item. /// * [SliverGrid], which places its children in arbitrary positions. class SliverList extends SliverMultiBoxAdaptorWidget { /// Creates a sliver that places box children in a linear array. @@ -333,6 +336,9 @@ class SliverList extends SliverMultiBoxAdaptorWidget { /// /// See also: /// +/// * [SliverPrototypeExtentList], which is similar to [SliverFixedExtentList] +/// except that it uses a prototype list item intead a pixel value to define +/// the main axis extent of each item. /// * [SliverFillViewport], which determines the [itemExtent] based on /// [SliverConstraints.viewportMainAxisExtent]. /// * [SliverList], which does not require its children to have the same @@ -372,6 +378,9 @@ class SliverFixedExtentList extends SliverMultiBoxAdaptorWidget { /// * [SliverList], which places its children in a linear array. /// * [SliverFixedExtentList], which places its children in a linear /// array with a fixed extent in the main axis. +/// * [SliverPrototypeExtentList], which is similar to [SliverFixedExtentList] +/// except that it uses a prototype list item intead a pixel value to define +/// the main axis extent of each item. class SliverGrid extends SliverMultiBoxAdaptorWidget { /// Creates a sliver that places multiple box children in a two dimensional /// arrangement. @@ -423,6 +432,9 @@ class SliverGrid extends SliverMultiBoxAdaptorWidget { /// /// * [SliverFixedExtentList], which has a configurable /// [SliverFixedExtentList.itemExtent]. +/// * [SliverPrototypeExtentList], which is similar to [SliverFixedExtentList] +/// except that it uses a prototype list item intead a pixel value to define +/// the main axis extent of each item. /// * [SliverList], which does not require its children to have the same /// extent in the main axis. class SliverFillViewport extends SliverMultiBoxAdaptorWidget { @@ -469,7 +481,7 @@ class SliverMultiBoxAdaptorElement extends RenderObjectElement implements Render RenderSliverMultiBoxAdaptor get renderObject => super.renderObject; @override - void update(SliverMultiBoxAdaptorWidget newWidget) { + void update(covariant SliverMultiBoxAdaptorWidget newWidget) { final SliverMultiBoxAdaptorWidget oldWidget = widget; super.update(newWidget); final SliverChildDelegate newDelegate = newWidget.delegate; diff --git a/packages/flutter/lib/src/widgets/sliver_prototype_item_list.dart b/packages/flutter/lib/src/widgets/sliver_prototype_item_list.dart new file mode 100644 index 00000000000..d09316cb9f0 --- /dev/null +++ b/packages/flutter/lib/src/widgets/sliver_prototype_item_list.dart @@ -0,0 +1,183 @@ +// Copyright 2017 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/foundation.dart'; +import 'package:flutter/rendering.dart'; + +import 'basic.dart'; +import 'framework.dart'; +import 'sliver.dart'; + +/// A sliver that places its box children in a linear array and constrains them +/// to have the same extent as a prototype item along the main axis. +/// +/// [SliverPrototypeExtentList] arranges its children in a line along +/// the main axis starting at offset zero and without gaps. Each child is +/// constrained to the same extent as the [prototypeItem] along the main axis +/// and the [SliverConstraints.crossAxisExtent] along the cross axis. +/// +/// [SliverPrototypeExtentList] is more efficient than [SliverList] because +/// [SliverPrototypeExtentList] does not need to lay out its children to obtain +/// their extent along the main axis. It's a little more flexible than +/// [SliverFixedExtentList] because there's no need to determine the approriate +/// item extent in pixels. +/// +/// See also: +/// +/// * [SliverFixedExtentList], whose [itemExtent] is a pixel value. +/// * [SliverList], which does not require its children to have the same +/// extent in the main axis. +/// * [SliverFillViewport], which sizes its children based on the +/// size of the viewport, regardless of what else is in the scroll view. +/// * [SliverList], which shows a list of variable-sized children in a +/// viewport. +class SliverPrototypeExtentList extends SliverMultiBoxAdaptorWidget { + /// Creates a sliver that places its box children in a linear array and + /// constrains them to have the same extent as a prototype item along + /// the main axis. + const SliverPrototypeExtentList({ + Key key, + @required SliverChildDelegate delegate, + @required this.prototypeItem, + }) : assert(prototypeItem != null), super(key: key, delegate: delegate); + + /// Defines the main axis extent of all of this sliver's children. + /// + /// The [prototypeItem] is laid out before the rest of the sliver's children + /// and its size along the main axis fixes the size of each child. The + /// [prototypeItem] is essentially [Offstage]: it is not painted and it + /// cannot respond to input. + final Widget prototypeItem; + + @override + _RenderSliverPrototypeExtentList createRenderObject(BuildContext context) { + final _SliverPrototypeExtentListElement element = context; + return new _RenderSliverPrototypeExtentList(childManager: element); + } + + @override + _SliverPrototypeExtentListElement createElement() => new _SliverPrototypeExtentListElement(this); +} + +class _SliverPrototypeExtentListElement extends SliverMultiBoxAdaptorElement { + _SliverPrototypeExtentListElement(SliverPrototypeExtentList widget) : super(widget); + + @override + SliverPrototypeExtentList get widget => super.widget; + + @override + _RenderSliverPrototypeExtentList get renderObject => super.renderObject; + + Element _prototype; + static final Object _prototypeSlot = new Object(); + + @override + void insertChildRenderObject(covariant RenderObject child, covariant dynamic slot) { + if (slot == _prototypeSlot) { + assert(child is RenderBox); + renderObject.child = child; + } else { + super.insertChildRenderObject(child, slot); + } + } + + @override + void didAdoptChild(RenderBox child) { + if (child != renderObject.child) + super.didAdoptChild(child); + } + + @override + void moveChildRenderObject(RenderBox child, dynamic slot) { + if (slot == _prototypeSlot) + assert(false); // There's only one prototype child so it cannot be moved. + else + super.moveChildRenderObject(child, slot); + } + + @override + void removeChildRenderObject(RenderBox child) { + if (renderObject.child == child) + renderObject.child = null; + else + super.removeChildRenderObject(child); + } + + @override + void visitChildren(ElementVisitor visitor) { + if (_prototype != null) + visitor(_prototype); + super.visitChildren(visitor); + } + + @override + void mount(Element parent, dynamic newSlot) { + super.mount(parent, newSlot); + _prototype = updateChild(_prototype, widget.prototypeItem, _prototypeSlot); + } + + @override + void update(SliverPrototypeExtentList newWidget) { + super.update(newWidget); + assert(widget == newWidget); + _prototype = updateChild(_prototype, widget.prototypeItem, _prototypeSlot); + } +} + +class _RenderSliverPrototypeExtentList extends RenderSliverFixedExtentBoxAdaptor { + _RenderSliverPrototypeExtentList({ + @required _SliverPrototypeExtentListElement childManager, + }) : super(childManager: childManager); + + RenderBox _child; + RenderBox get child => _child; + set child(RenderBox value) { + if (_child != null) + dropChild(_child); + _child = value; + if (_child != null) + adoptChild(_child); + markNeedsLayout(); + } + + @override + void performLayout() { + child.layout(constraints.asBoxConstraints(), parentUsesSize: true); + super.performLayout(); + } + + @override + void attach(PipelineOwner owner) { + super.attach(owner); + if (_child != null) + _child.attach(owner); + } + + @override + void detach() { + super.detach(); + if (_child != null) + _child.detach(); + } + + @override + void redepthChildren() { + if (_child != null) + redepthChild(_child); + super.redepthChildren(); + } + + @override + void visitChildren(RenderObjectVisitor visitor) { + if (_child != null) + visitor(_child); + super.visitChildren(visitor); + } + + @override + double get itemExtent { + assert(child != null && child.hasSize); + return constraints.axis == Axis.vertical ? child.size.height : child.size.width; + } +} diff --git a/packages/flutter/lib/widgets.dart b/packages/flutter/lib/widgets.dart index 76856ff7af9..1d4a6dedf09 100644 --- a/packages/flutter/lib/widgets.dart +++ b/packages/flutter/lib/widgets.dart @@ -72,6 +72,7 @@ export 'src/widgets/single_child_scroll_view.dart'; export 'src/widgets/size_changed_layout_notifier.dart'; export 'src/widgets/sliver.dart'; export 'src/widgets/sliver_persistent_header.dart'; +export 'src/widgets/sliver_prototype_item_list.dart'; export 'src/widgets/status_transitions.dart'; export 'src/widgets/table.dart'; export 'src/widgets/text.dart'; diff --git a/packages/flutter/test/widgets/sliver_prototype_item_extent_test.dart b/packages/flutter/test/widgets/sliver_prototype_item_extent_test.dart new file mode 100644 index 00000000000..455e16a0671 --- /dev/null +++ b/packages/flutter/test/widgets/sliver_prototype_item_extent_test.dart @@ -0,0 +1,133 @@ +// Copyright 2017 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/widgets.dart'; + +class TestItem extends StatelessWidget { + const TestItem({ Key key, this.item, this.width, this.height }) : super(key: key); + final int item; + final double width; + final double height; + @override + Widget build(BuildContext context) { + return new Container( + width: width, + height: height, + alignment: FractionalOffset.center, + child: new Text('Item $item'), + ); + } +} + +Widget buildFrame({ int count, double width, double height, Axis scrollDirection }) { + return new CustomScrollView( + scrollDirection: scrollDirection ?? Axis.vertical, + slivers: [ + new SliverPrototypeExtentList( + prototypeItem: new TestItem(item: -1, width: width, height: height), + delegate: new SliverChildBuilderDelegate( + (BuildContext context, int index) => new TestItem(item: index), + childCount: count, + ), + ), + ], + ); +} + +void main() { + testWidgets('SliverPrototypeExtentList vertical scrolling basics', (WidgetTester tester) async { + await tester.pumpWidget(buildFrame(count: 20, height: 100.0)); + + // The viewport is 600 pixels high, lazily created items are 100 pixels high. + for (int i = 0; i < 6; i += 1) { + final Finder item = find.widgetWithText(Container, 'Item $i'); + expect(item, findsOneWidget); + expect(tester.getTopLeft(item).dy, i * 100.0); + expect(tester.getSize(item).height, 100.0); + } + for (int i = 7; i < 20; i += 1) + expect(find.text('Item $i'), findsNothing); + + // Fling scroll to the end. + await tester.fling(find.text('Item 2'), const Offset(0.0, -200.0), 5000.0); + await tester.pumpAndSettle(); + + for (int i = 19; i >= 14; i -= 1) + expect(find.text('Item $i'), findsOneWidget); + for (int i = 13; i >= 0; i -= 1) + expect(find.text('Item $i'), findsNothing); + }); + + testWidgets('SliverPrototypeExtentList horizontal scrolling basics', (WidgetTester tester) async { + await tester.pumpWidget(buildFrame(count: 20, width: 100.0, scrollDirection: Axis.horizontal)); + + // The viewport is 800 pixels wide, lazily created items are 100 pixels wide. + for (int i = 0; i < 8; i += 1) { + final Finder item = find.widgetWithText(Container, 'Item $i'); + expect(item, findsOneWidget); + expect(tester.getTopLeft(item).dx, i * 100.0); + expect(tester.getSize(item).width, 100.0); + } + for (int i = 9; i < 20; i += 1) + expect(find.text('Item $i'), findsNothing); + + // Fling scroll to the end. + await tester.fling(find.text('Item 3'), const Offset(-200.0, 0.0), 5000.0); + await tester.pumpAndSettle(); + + for (int i = 19; i >= 12; i -= 1) + expect(find.text('Item $i'), findsOneWidget); + for (int i = 11; i >= 0; i -= 1) + expect(find.text('Item $i'), findsNothing); + }); + + testWidgets('SliverPrototypeExtentList change the prototype item', (WidgetTester tester) async { + await tester.pumpWidget(buildFrame(count: 10, height: 60.0)); + + // The viewport is 600 pixels high, each of the 10 items is 60 pixels high + for (int i = 0; i < 10; i += 1) + expect(find.text('Item $i'), findsOneWidget); + + await tester.pumpWidget(buildFrame(count: 10, height: 120.0)); + + // Now the items are 120 pixels high, so only 5 fit. + for (int i = 0; i < 5; i += 1) + expect(find.text('Item $i'), findsOneWidget); + for (int i = 5; i < 10; i += 1) + expect(find.text('Item $i'), findsNothing); + + await tester.pumpWidget(buildFrame(count: 10, height: 60.0)); + + // Now they all fit again + for (int i = 0; i < 10; i += 1) + expect(find.text('Item $i'), findsOneWidget); + }); + + testWidgets('SliverPrototypeExtentList first item is also the prototype', (WidgetTester tester) async { + final List items = new List.generate(10, (int index) { + return new TestItem(key: new ValueKey(index), item: index, height: index == 0 ? 60.0 : null); + }).toList(); + + await tester.pumpWidget( + new CustomScrollView( + slivers: [ + new SliverPrototypeExtentList( + prototypeItem: items[0], + delegate: new SliverChildBuilderDelegate( + (BuildContext context, int index) => items[index], + childCount: 10, + ), + ), + ], + ) + ); + + // Item 0 exists in the list and as the prototype item. + expect(tester.widgetList(find.text('Item 0')).length, 2); + + for (int i = 1; i < 10; i += 1) + expect(find.text('Item $i'), findsOneWidget); + }); +}