Commit 765e5d5b authored by Hans Muller's avatar Hans Muller Committed by GitHub

Added SliverPrototypeExtentList et al (#10097)

parent 73dcca65
......@@ -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
......
......@@ -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.
......
......@@ -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;
......
// 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;
}
}
......@@ -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';
......
// 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: <Widget>[
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<Widget> items = new List<Widget>.generate(10, (int index) {
return new TestItem(key: new ValueKey<int>(index), item: index, height: index == 0 ? 60.0 : null);
}).toList();
await tester.pumpWidget(
new CustomScrollView(
slivers: <Widget>[
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);
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment