diff --git a/sdk/BUILD.gn b/sdk/BUILD.gn index b2756e5f620..76bb4c8f4e9 100644 --- a/sdk/BUILD.gn +++ b/sdk/BUILD.gn @@ -87,6 +87,7 @@ dart_pkg("sky") { "lib/widgets/theme.dart", "lib/widgets/toggleable.dart", "lib/widgets/tool_bar.dart", + "lib/widgets/variable_height_scrollable.dart", "lib/widgets/widget.dart", "pubspec.yaml", ] diff --git a/sdk/example/widgets/card_collection.dart b/sdk/example/widgets/card_collection.dart index cbd064ed2d6..44852f0bff0 100644 --- a/sdk/example/widgets/card_collection.dart +++ b/sdk/example/widgets/card_collection.dart @@ -6,16 +6,14 @@ import 'dart:sky' as sky; import 'package:vector_math/vector_math.dart'; import 'package:sky/animation/animation_performance.dart'; -import 'package:sky/animation/scroll_behavior.dart'; import 'package:sky/base/lerp.dart'; import 'package:sky/painting/text_style.dart'; import 'package:sky/theme/colors.dart'; import 'package:sky/widgets/animation_builder.dart'; import 'package:sky/widgets/basic.dart'; -import 'package:sky/widgets/block_viewport.dart'; import 'package:sky/widgets/card.dart'; import 'package:sky/widgets/scaffold.dart'; -import 'package:sky/widgets/scrollable.dart'; +import 'package:sky/widgets/variable_height_scrollable.dart'; import 'package:sky/widgets/theme.dart'; import 'package:sky/widgets/tool_bar.dart'; import 'package:sky/widgets/widget.dart'; @@ -26,50 +24,13 @@ const double _kMinFlingVelocity = 700.0; const double _kMinFlingVelocityDelta = 400.0; const double _kDismissCardThreshold = 0.6; -class VariableHeightScrollable extends Scrollable { - VariableHeightScrollable({ - String key, - this.builder, - this.token - }) : super(key: key); - - IndexedBuilder builder; - Object token; - - void syncFields(VariableHeightScrollable source) { - builder = source.builder; - token = source.token; - super.syncFields(source); - } - - ScrollBehavior createScrollBehavior() => new OverscrollBehavior(); - OverscrollBehavior get scrollBehavior => super.scrollBehavior; - - void _handleSizeChanged(Size newSize) { - setState(() { - scrollBehavior.containerSize = newSize.height; - scrollBehavior.contentsSize = 5000.0; - }); - } - - Widget buildContent() { - return new SizeObserver( - callback: _handleSizeChanged, - child: new BlockViewport( - builder: builder, - startOffset: scrollOffset, - token: token - ) - ); - } -} - class CardCollectionApp extends App { final TextStyle cardLabelStyle = new TextStyle(color: White, fontSize: 18.0, fontWeight: bold); final List cardHeights = [ + 48.0, 64.0, 82.0, 46.0, 60.0, 55.0, 84.0, 96.0, 50.0, 48.0, 64.0, 82.0, 46.0, 60.0, 55.0, 84.0, 96.0, 50.0, 48.0, 64.0, 82.0, 46.0, 60.0, 55.0, 84.0, 96.0, 50.0, 48.0, 64.0, 82.0, 46.0, 60.0, 55.0, 84.0, 96.0, 50.0 diff --git a/sdk/lib/widgets/block_viewport.dart b/sdk/lib/widgets/block_viewport.dart index 8aa0f0c3756..feb3e7157d1 100644 --- a/sdk/lib/widgets/block_viewport.dart +++ b/sdk/lib/widgets/block_viewport.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:collection'; + import '../rendering/block.dart'; import '../rendering/box.dart'; import '../rendering/object.dart'; @@ -10,6 +12,13 @@ import 'widget.dart'; // return null if index is greater than index of last entry typedef Widget IndexedBuilder(int index); +typedef void LayoutChangedCallback( + int firstVisibleChildIndex, + int visibleChildCount, + UnmodifiableListView childOffsets, + bool didReachLastChild +); + class _Key { const _Key(this.type, this.key); factory _Key.fromWidget(Widget widget) => new _Key(widget.runtimeType, widget.key); @@ -20,12 +29,13 @@ class _Key { } class BlockViewport extends RenderObjectWrapper { - BlockViewport({ this.builder, this.startOffset, this.token, String key }) + BlockViewport({ this.builder, this.startOffset, this.token, this.onLayoutChanged, String key }) : super(key: key); IndexedBuilder builder; double startOffset; Object token; + LayoutChangedCallback onLayoutChanged; RenderBlockViewport get root => super.root; RenderBlockViewport createNode() => new RenderBlockViewport(); @@ -83,6 +93,7 @@ class BlockViewport extends RenderObjectWrapper { List _offsets = [0.0]; int _currentStartIndex = 0; int _currentChildCount = 0; + bool _didReachLastChild = false; int _findIndexForOffsetBeforeOrAt(double offset) { int left = 0; @@ -113,6 +124,7 @@ class BlockViewport extends RenderObjectWrapper { builder = newNode.builder; token = newNode.token; _offsets = [0.0]; + _didReachLastChild = false; } return true; } @@ -196,6 +208,7 @@ class BlockViewport extends RenderObjectWrapper { haveChildren = true; } else { haveChildren = false; + _didReachLastChild = true; } } } else { @@ -207,8 +220,10 @@ class BlockViewport extends RenderObjectWrapper { // list is complete (and thus we are overscrolled). while (true) { Widget widget = _getWidget(startIndex, innerConstraints); - if (widget == null) + if (widget == null) { + _didReachLastChild = true; break; + } _Key widgetKey = new _Key.fromWidget(widget); if (_offsets.last > startOffset) { newChildren[widgetKey] = widget; @@ -238,6 +253,7 @@ class BlockViewport extends RenderObjectWrapper { } } assert(haveChildren != null); + assert(haveChildren || _didReachLastChild); assert(startIndex >= 0); assert(startIndex < _offsets.length); @@ -249,8 +265,10 @@ class BlockViewport extends RenderObjectWrapper { while (_offsets[index] < endOffset) { if (!builtChildren.containsKey(index)) { Widget widget = _getWidget(index, innerConstraints); - if (widget == null) - break; // reached the end of the list + if (widget == null) { + _didReachLastChild = true; + break; + } newChildren[new _Key.fromWidget(widget)] = widget; builtChildren[index] = widget; } @@ -287,6 +305,15 @@ class BlockViewport extends RenderObjectWrapper { _childrenByKey = newChildren; _currentStartIndex = startIndex; _currentChildCount = _childrenByKey.length; + + if (onLayoutChanged != null) { + onLayoutChanged( + _currentStartIndex, + _currentChildCount, + new UnmodifiableListView(_offsets), + _didReachLastChild + ); + } } } diff --git a/sdk/lib/widgets/variable_height_scrollable.dart b/sdk/lib/widgets/variable_height_scrollable.dart new file mode 100644 index 00000000000..a3768684109 --- /dev/null +++ b/sdk/lib/widgets/variable_height_scrollable.dart @@ -0,0 +1,60 @@ +// 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 'dart:collection'; + +import '../animation/scroll_behavior.dart'; +import 'basic.dart'; +import 'block_viewport.dart'; +import 'scrollable.dart'; +import 'widget.dart'; + +class VariableHeightScrollable extends Scrollable { + VariableHeightScrollable({ + String key, + this.builder, + this.token + }) : super(key: key); + + IndexedBuilder builder; + Object token; + + void syncFields(VariableHeightScrollable source) { + builder = source.builder; + token = source.token; + super.syncFields(source); + } + + ScrollBehavior createScrollBehavior() => new OverscrollBehavior(); + OverscrollBehavior get scrollBehavior => super.scrollBehavior; + + void _handleSizeChanged(Size newSize) { + setState(() { + scrollBehavior.containerSize = newSize.height; + }); + } + + void _handleLayoutChanged( + int firstVisibleChildIndex, + int visibleChildCount, + UnmodifiableListView childOffsets, + bool didReachLastChild) { + assert(childOffsets.length > 0); + scrollBehavior.contentsSize = didReachLastChild ? childOffsets.last : double.INFINITY; + if (didReachLastChild && scrollOffset > scrollBehavior.maxScrollOffset) + settleScrollOffset(); + } + + Widget buildContent() { + return new SizeObserver( + callback: _handleSizeChanged, + child: new BlockViewport( + builder: builder, + onLayoutChanged: _handleLayoutChanged, + startOffset: scrollOffset, + token: token + ) + ); + } +}