Commit 098af183 authored by Adam Barth's avatar Adam Barth Committed by GitHub

Add SliverList (#7727)

Add SliverList

A SliverList is a linear layout of box children in a viewport that all
have a common, fixed extent along the scroll axis. The layout is similar
to a SliverBlock but more efficient.
parent 6e30cae8
......@@ -47,6 +47,8 @@ export 'src/rendering/shifted_box.dart';
export 'src/rendering/sliver.dart';
export 'src/rendering/sliver_app_bar.dart';
export 'src/rendering/sliver_block.dart';
export 'src/rendering/sliver_list.dart';
export 'src/rendering/sliver_multi_box_adaptor.dart';
export 'src/rendering/sliver_padding.dart';
export 'src/rendering/stack.dart';
export 'src/rendering/table.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 'dart:math' as math;
import 'package:meta/meta.dart';
import 'box.dart';
import 'sliver.dart';
import 'sliver_multi_box_adaptor.dart';
class RenderSliverList extends RenderSliverMultiBoxAdaptor {
RenderSliverList({
@required RenderSliverBoxChildManager childManager,
double itemExtent,
}) : _itemExtent = itemExtent, super(childManager: childManager);
/// The main-axis extent of each item in the list.
double get itemExtent => _itemExtent;
double _itemExtent;
set itemExtent (double newValue) {
assert(newValue != null);
if (_itemExtent == newValue)
return;
_itemExtent = newValue;
markNeedsLayout();
}
double _indexToScrollOffset(int index) => _itemExtent * index;
@override
void performLayout() {
assert(childManager.debugAssertChildListLocked());
final double scrollOffset = constraints.scrollOffset;
assert(scrollOffset >= 0.0);
final double remainingPaintExtent = constraints.remainingPaintExtent;
assert(remainingPaintExtent >= 0.0);
final double targetEndScrollOffset = scrollOffset + remainingPaintExtent;
BoxConstraints childConstraints = constraints.asBoxConstraints(
minExtent: itemExtent,
maxExtent: itemExtent,
);
final int firstIndex = math.max(0, scrollOffset ~/ _itemExtent);
final int targetLastIndex = math.max(0, (targetEndScrollOffset / itemExtent).ceil());
if (firstChild != null) {
final int oldFirstIndex = indexOf(firstChild);
final int oldLastIndex = indexOf(lastChild);
final int leadingGarbage = (firstIndex - oldFirstIndex).clamp(0, childCount);
final int trailingGarbage = (oldLastIndex - targetLastIndex).clamp(0, childCount);
if (leadingGarbage + trailingGarbage > 0)
collectGarbage(leadingGarbage, trailingGarbage);
}
if (firstChild == null) {
if (!addInitialChild(index: firstIndex, scrollOffset: _indexToScrollOffset(firstIndex))) {
// There are no children.
geometry = SliverGeometry.zero;
return;
}
}
RenderBox trailingChildWithLayout;
for (int index = indexOf(firstChild) - 1; index >= firstIndex; --index) {
final RenderBox child = insertAndLayoutLeadingChild(childConstraints);
final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
childParentData.scrollOffset = _indexToScrollOffset(index);
assert(childParentData.index == index);
trailingChildWithLayout ??= child;
}
assert(offsetOf(firstChild) <= scrollOffset);
if (trailingChildWithLayout == null) {
firstChild.layout(childConstraints);
trailingChildWithLayout = firstChild;
}
while (indexOf(trailingChildWithLayout) < targetLastIndex) {
RenderBox child = childAfter(trailingChildWithLayout);
if (child == null) {
child = insertAndLayoutChild(childConstraints, after: trailingChildWithLayout);
if (child == null) {
// We have run out of children.
break;
}
} else {
child.layout(childConstraints);
}
trailingChildWithLayout = child;
assert(child != null);
final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
childParentData.scrollOffset = _indexToScrollOffset(childParentData.index);
}
final int lastIndex = indexOf(lastChild);
final double leadingScrollOffset = _indexToScrollOffset(firstIndex);
final double trailingScrollOffset = _indexToScrollOffset(lastIndex + 1);
assert(debugAssertChildListIsNonEmptyAndContiguous());
assert(indexOf(firstChild) == firstIndex);
assert(lastIndex <= targetLastIndex);
final double estimatedTotalExtent = childManager.estimateScrollOffsetExtent(
firstIndex: firstIndex,
lastIndex: lastIndex,
leadingScrollOffset: leadingScrollOffset,
trailingScrollOffset: trailingScrollOffset,
);
final double paintedExtent = calculatePaintOffset(
constraints,
from: leadingScrollOffset,
to: trailingScrollOffset,
);
geometry = new SliverGeometry(
scrollExtent: estimatedTotalExtent,
paintExtent: paintedExtent,
maxPaintExtent: estimatedTotalExtent,
// Conservative to avoid flickering away the clip during scroll.
hasVisualOverflow: lastIndex >= targetLastIndex || constraints.scrollOffset > 0.0,
);
assert(childManager.debugAssertChildListLocked());
}
}
// Copyright 2016 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/rendering.dart';
import 'framework.dart';
import 'basic.dart';
import 'scrollable.dart';
import 'sliver.dart';
class ScrollView extends StatelessWidget {
ScrollView({
Key key,
this.padding,
this.scrollDirection: Axis.vertical,
this.anchor: 0.0,
this.initialScrollOffset: 0.0,
this.itemExtent,
this.center,
this.children,
}) : super(key: key);
final EdgeInsets padding;
final Axis scrollDirection;
final double anchor;
final double initialScrollOffset;
final double itemExtent;
final Key center;
final List<Widget> children;
AxisDirection _getDirection(BuildContext context) {
// TODO(abarth): Consider reading direction.
switch (scrollDirection) {
case Axis.horizontal:
return AxisDirection.right;
case Axis.vertical:
return AxisDirection.down;
}
return null;
}
@override
Widget build(BuildContext context) {
final SliverChildListDelegate delegate = new SliverChildListDelegate(children);
Widget sliver;
if (itemExtent == null) {
sliver = new SliverBlock(delegate: delegate);
} else {
sliver = new SliverList(
delegate: delegate,
itemExtent: itemExtent,
);
}
if (padding != null)
sliver = new SliverPadding(padding: padding, child: sliver);
return new ScrollableViewport2(
axisDirection: _getDirection(context),
anchor: anchor,
initialScrollOffset: initialScrollOffset,
center: center,
slivers: <Widget>[ sliver ],
);
}
}
......@@ -14,7 +14,6 @@ export 'src/widgets/app_bar.dart';
export 'src/widgets/banner.dart';
export 'src/widgets/basic.dart';
export 'src/widgets/binding.dart';
export 'src/widgets/block.dart';
export 'src/widgets/clamp_overscrolls.dart';
export 'src/widgets/container.dart';
export 'src/widgets/debug.dart';
......@@ -51,12 +50,14 @@ export 'src/widgets/scroll_behavior.dart';
export 'src/widgets/scroll_configuration.dart';
export 'src/widgets/scroll_notification.dart';
export 'src/widgets/scroll_simulation.dart';
export 'src/widgets/scroll_view.dart';
export 'src/widgets/scrollable.dart';
export 'src/widgets/scrollable_grid.dart';
export 'src/widgets/scrollable_list.dart';
export 'src/widgets/semantics_debugger.dart';
export 'src/widgets/single_child_scroll_view.dart';
export 'src/widgets/size_changed_layout_notifier.dart';
export 'src/widgets/sliver.dart';
export 'src/widgets/status_transitions.dart';
export 'src/widgets/table.dart';
export 'src/widgets/text.dart';
......
......@@ -8,19 +8,38 @@ import 'package:test/test.dart';
import 'rendering_tester.dart';
class RenderSliverBlockTest extends RenderSliverBlock {
RenderSliverBlockTest({
class TestRenderSliverBoxChildManager extends RenderSliverBoxChildManager {
TestRenderSliverBoxChildManager({
this.children,
});
RenderSliverBlock _renderObject;
List<RenderBox> children;
RenderSliverBlock createRenderObject() {
assert(_renderObject == null);
_renderObject = new RenderSliverBlock(childManager: this);
return _renderObject;
}
int _currentlyUpdatingChildIndex;
@override
void createChild(int index, { @required RenderBox after }) {
assert(index >= 0);
if (index < 0 || index >= children.length)
return null;
insert(children[index], after: after);
try {
_currentlyUpdatingChildIndex = index;
_renderObject.insert(children[index], after: after);
} finally {
_currentlyUpdatingChildIndex = null;
}
}
@override
void removeChild(RenderBox child) {
_renderObject.remove(child);
}
@override
......@@ -33,25 +52,33 @@ class RenderSliverBlockTest extends RenderSliverBlock {
assert(lastIndex >= firstIndex);
return children.length * (trailingScrollOffset - leadingScrollOffset) / (lastIndex - firstIndex + 1);
}
@override
void didAdoptChild(RenderBox child) {
assert(_currentlyUpdatingChildIndex != null);
final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
childParentData.index = _currentlyUpdatingChildIndex;
}
}
void main() {
test('RenderSliverBlock basic test - down', () {
RenderObject inner;
RenderBox a, b, c, d, e;
TestRenderSliverBoxChildManager childManager = new TestRenderSliverBoxChildManager(
children: <RenderBox>[
a = new RenderSizedBox(const Size(100.0, 400.0)),
b = new RenderSizedBox(const Size(100.0, 400.0)),
c = new RenderSizedBox(const Size(100.0, 400.0)),
d = new RenderSizedBox(const Size(100.0, 400.0)),
e = new RenderSizedBox(const Size(100.0, 400.0)),
],
);
RenderViewport2 root = new RenderViewport2(
axisDirection: AxisDirection.down,
offset: new ViewportOffset.zero(),
children: <RenderSliver>[
inner = new RenderSliverBlockTest(
children: <RenderBox>[
a = new RenderSizedBox(const Size(100.0, 400.0)),
b = new RenderSizedBox(const Size(100.0, 400.0)),
c = new RenderSizedBox(const Size(100.0, 400.0)),
d = new RenderSizedBox(const Size(100.0, 400.0)),
e = new RenderSizedBox(const Size(100.0, 400.0)),
],
),
inner = childManager.createRenderObject(),
],
);
layout(root);
......@@ -112,19 +139,20 @@ void main() {
test('RenderSliverBlock basic test - up', () {
RenderObject inner;
RenderBox a, b, c, d, e;
TestRenderSliverBoxChildManager childManager = new TestRenderSliverBoxChildManager(
children: <RenderBox>[
a = new RenderSizedBox(const Size(100.0, 400.0)),
b = new RenderSizedBox(const Size(100.0, 400.0)),
c = new RenderSizedBox(const Size(100.0, 400.0)),
d = new RenderSizedBox(const Size(100.0, 400.0)),
e = new RenderSizedBox(const Size(100.0, 400.0)),
],
);
RenderViewport2 root = new RenderViewport2(
axisDirection: AxisDirection.up,
offset: new ViewportOffset.zero(),
children: <RenderSliver>[
inner = new RenderSliverBlockTest(
children: <RenderBox>[
a = new RenderSizedBox(const Size(100.0, 400.0)),
b = new RenderSizedBox(const Size(100.0, 400.0)),
c = new RenderSizedBox(const Size(100.0, 400.0)),
d = new RenderSizedBox(const Size(100.0, 400.0)),
e = new RenderSizedBox(const Size(100.0, 400.0)),
],
),
inner = childManager.createRenderObject(),
],
);
layout(root);
......
......@@ -121,7 +121,7 @@ void main() {
});
testWidgets('SliverBlockChildListDelegate.estimateScrollOffsetExtent hits end', (WidgetTester tester) async {
SliverBlockChildListDelegate delegate = new SliverBlockChildListDelegate(<Widget>[
SliverChildListDelegate delegate = new SliverChildListDelegate(<Widget>[
new Container(),
new Container(),
new Container(),
......
// Copyright 2015 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';
void main() {
testWidgets('ScrollView itemExtent control test', (WidgetTester tester) async {
await tester.pumpWidget(
new ScrollView(
itemExtent: 200.0,
children: new List<Widget>.generate(20, (int i) {
return new Container(
child: new Text('$i'),
);
}),
),
);
RenderBox box = tester.renderObject<RenderBox>(find.byType(Container).first);
expect(box.size.height, equals(200.0));
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsOneWidget);
expect(find.text('2'), findsOneWidget);
expect(find.text('3'), findsOneWidget);
expect(find.text('4'), findsNothing);
await tester.scroll(find.byType(ScrollView), const Offset(0.0, -250.0));
await tester.pump();
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
expect(find.text('2'), findsOneWidget);
expect(find.text('3'), findsOneWidget);
expect(find.text('4'), findsOneWidget);
expect(find.text('5'), findsOneWidget);
expect(find.text('6'), findsNothing);
await tester.scroll(find.byType(ScrollView), const Offset(0.0, 200.0));
await tester.pump();
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsOneWidget);
expect(find.text('2'), findsOneWidget);
expect(find.text('3'), findsOneWidget);
expect(find.text('4'), findsOneWidget);
expect(find.text('5'), findsNothing);
});
testWidgets('ScrollView large scroll jump', (WidgetTester tester) async {
List<int> log = <int>[];
await tester.pumpWidget(
new ScrollView(
itemExtent: 200.0,
children: new List<Widget>.generate(20, (int i) {
return new Builder(
builder: (BuildContext context) {
log.add(i);
return new Container(
child: new Text('$i'),
);
}
);
}),
),
);
expect(log, equals(<int>[0, 1, 2, 3]));
log.clear();
Scrollable2State state = tester.state(find.byType(Scrollable2));
AbsoluteScrollPosition position = state.position;
position.jumpTo(2025.0);
expect(log, isEmpty);
await tester.pump();
expect(log, equals(<int>[10, 11, 12, 13, 14]));
log.clear();
position.jumpTo(975.0);
expect(log, isEmpty);
await tester.pump();
expect(log, equals(<int>[4, 5, 6, 7, 8]));
log.clear();
});
}
......@@ -28,7 +28,7 @@ Future<Null> test(WidgetTester tester, double offset, List<int> keys) {
offset: new ViewportOffset.fixed(offset),
slivers: <Widget>[
new SliverBlock(
delegate: new SliverBlockChildListDelegate(keys.map((int key) {
delegate: new SliverChildListDelegate(keys.map((int key) {
return new SizedBox(key: new GlobalObjectKey(key), height: 100.0, child: new GenerationText(key));
}).toList()),
),
......
......@@ -13,7 +13,7 @@ Future<Null> test(WidgetTester tester, double offset) {
offset: new ViewportOffset.fixed(offset),
slivers: <Widget>[
new SliverBlock(
delegate: new SliverBlockChildListDelegate(<Widget>[
delegate: new SliverChildListDelegate(<Widget>[
new SizedBox(height: 400.0, child: new Text('a')),
new SizedBox(height: 400.0, child: new Text('b')),
new SizedBox(height: 400.0, child: new Text('c')),
......@@ -78,7 +78,7 @@ void main() {
offset: offset,
slivers: <Widget>[
new SliverBlock(
delegate: new SliverBlockChildListDelegate(<Widget>[
delegate: new SliverChildListDelegate(<Widget>[
new SizedBox(height: 251.0, child: new Text('a')),
new SizedBox(height: 252.0, child: new Text('b')),
new SizedBox(key: key1, height: 253.0, child: new Text('c')),
......@@ -95,7 +95,7 @@ void main() {
offset: offset,
slivers: <Widget>[
new SliverBlock(
delegate: new SliverBlockChildListDelegate(<Widget>[
delegate: new SliverChildListDelegate(<Widget>[
new SizedBox(key: key1, height: 253.0, child: new Text('c')),
new SizedBox(height: 251.0, child: new Text('a')),
new SizedBox(height: 252.0, child: new Text('b')),
......@@ -112,7 +112,7 @@ void main() {
offset: offset,
slivers: <Widget>[
new SliverBlock(
delegate: new SliverBlockChildListDelegate(<Widget>[
delegate: new SliverChildListDelegate(<Widget>[
new SizedBox(height: 251.0, child: new Text('a')),
new SizedBox(key: key1, height: 253.0, child: new Text('c')),
new SizedBox(height: 252.0, child: new Text('b')),
......@@ -129,7 +129,7 @@ void main() {
offset: offset,
slivers: <Widget>[
new SliverBlock(
delegate: new SliverBlockChildListDelegate(<Widget>[
delegate: new SliverChildListDelegate(<Widget>[
new SizedBox(height: 251.0, child: new Text('a')),
new SizedBox(height: 252.0, child: new Text('b')),
]),
......@@ -144,7 +144,7 @@ void main() {
offset: offset,
slivers: <Widget>[
new SliverBlock(
delegate: new SliverBlockChildListDelegate(<Widget>[
delegate: new SliverChildListDelegate(<Widget>[
new SizedBox(height: 251.0, child: new Text('a')),
new SizedBox(key: key1, height: 253.0, child: new Text('c')),
new SizedBox(height: 252.0, child: new Text('b')),
......@@ -210,7 +210,7 @@ void main() {
offset: new ViewportOffset.zero(),
slivers: <Widget>[
new SliverBlock(
delegate: new SliverBlockChildListDelegate(<Widget>[
delegate: new SliverChildListDelegate(<Widget>[
new SizedBox(height: 400.0, child: new Text('a')),
]),
),
......@@ -223,7 +223,7 @@ void main() {
offset: new ViewportOffset.fixed(100.0),
slivers: <Widget>[
new SliverBlock(
delegate: new SliverBlockChildListDelegate(<Widget>[
delegate: new SliverChildListDelegate(<Widget>[
new SizedBox(height: 400.0, child: new Text('a')),
]),
),
......@@ -236,7 +236,7 @@ void main() {
offset: new ViewportOffset.fixed(100.0),
slivers: <Widget>[
new SliverBlock(
delegate: new SliverBlockChildListDelegate(<Widget>[
delegate: new SliverChildListDelegate(<Widget>[
new SizedBox(height: 4000.0, child: new Text('a')),
]),
),
......@@ -249,7 +249,7 @@ void main() {
offset: new ViewportOffset.zero(),
slivers: <Widget>[
new SliverBlock(
delegate: new SliverBlockChildListDelegate(<Widget>[
delegate: new SliverChildListDelegate(<Widget>[
new SizedBox(height: 4000.0, child: new Text('a')),
]),
),
......
......@@ -119,7 +119,7 @@ void main() {
new SliverAppBar(delegate: new TestSliverAppBarDelegate(150.0), floating: true),
new SliverToBoxAdapter(child: new Container(height: 5.0)),
new SliverBlock(
delegate: new SliverBlockChildListDelegate(<Widget>[
delegate: new SliverChildListDelegate(<Widget>[
new Container(height: 50.0),
new Container(height: 50.0),
new Container(height: 50.0),
......
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