diff --git a/dev/benchmarks/complex_layout/lib/main.dart b/dev/benchmarks/complex_layout/lib/main.dart index 72358dbd4c..68e0189fe6 100644 --- a/dev/benchmarks/complex_layout/lib/main.dart +++ b/dev/benchmarks/complex_layout/lib/main.dart @@ -64,6 +64,11 @@ class FancyItemDelegate extends LazyBlockDelegate { @override bool shouldRebuild(FancyItemDelegate oldDelegate) => false; + + @override + double estimateTotalExtent(int firstIndex, int lastIndex, double minOffset, double firstStartOffset, double lastEndOffset) { + return double.INFINITY; + } } class ComplexLayoutState extends State { diff --git a/dev/manual_tests/overlay_geometry.dart b/dev/manual_tests/overlay_geometry.dart index 4f5495053f..81459ea497 100644 --- a/dev/manual_tests/overlay_geometry.dart +++ b/dev/manual_tests/overlay_geometry.dart @@ -125,6 +125,11 @@ class CardBuilder extends LazyBlockDelegate { bool shouldRebuild(CardBuilder oldDelegate) { return oldDelegate.cardModels != cardModels; } + + @override + double estimateTotalExtent(int firstIndex, int lastIndex, double minOffset, double firstStartOffset, double lastEndOffset) { + return (lastEndOffset - minOffset) * cardModels.length / (lastIndex + 1); + } } class OverlayGeometryAppState extends State { diff --git a/examples/stocks/lib/stock_home.dart b/examples/stocks/lib/stock_home.dart index 6844259d64..e7a48a9a5a 100644 --- a/examples/stocks/lib/stock_home.dart +++ b/examples/stocks/lib/stock_home.dart @@ -132,29 +132,7 @@ class StockHomeState extends State { ), new DrawerItem( icon: new Icon(Icons.account_balance), - onPressed: () { - showDialog( - context: context, - child: new Dialog( - title: new Text('Not Implemented'), - content: new Text('This feature has not yet been implemented.'), - actions: [ - new FlatButton( - onPressed: () { - Navigator.pop(context, false); - }, - child: new Text('USE IT') - ), - new FlatButton( - onPressed: () { - Navigator.pop(context, false); - }, - child: new Text('OH WELL') - ), - ] - ) - ); - }, + onPressed: null, child: new Text('Account Balance') ), new DrawerItem( @@ -199,7 +177,8 @@ class StockHomeState extends State { child: new Text('Settings')), new DrawerItem( icon: new Icon(Icons.help), - child: new Text('Help & Feedback')) + onPressed: _handleShowAbout, + child: new Text('About')) ]) ); } @@ -208,6 +187,10 @@ class StockHomeState extends State { Navigator.popAndPushNamed(context, '/settings'); } + void _handleShowAbout() { + showAboutDialog(context: context); + } + Widget buildAppBar() { return new AppBar( elevation: 0, diff --git a/examples/stocks/test/icon_color_test.dart b/examples/stocks/test/icon_color_test.dart index cba9f8f7a5..feea6a40aa 100644 --- a/examples/stocks/test/icon_color_test.dart +++ b/examples/stocks/test/icon_color_test.dart @@ -58,10 +58,10 @@ void main() { // sanity check expect(find.text('MARKET'), findsOneWidget); - expect(find.text('Help & Feedback'), findsNothing); + expect(find.text('Account Balance'), findsNothing); await tester.pump(new Duration(seconds: 2)); expect(find.text('MARKET'), findsOneWidget); - expect(find.text('Help & Feedback'), findsNothing); + expect(find.text('Account Balance'), findsNothing); // drag the drawer out Point left = new Point(0.0, ui.window.size.height / 2.0); @@ -73,12 +73,12 @@ void main() { await gesture.up(); await tester.pump(); expect(find.text('MARKET'), findsOneWidget); - expect(find.text('Help & Feedback'), findsOneWidget); + expect(find.text('Account Balance'), findsOneWidget); // check the colour of the icon - light mode checkIconColor(tester, 'Stock List', Colors.purple[500]); // theme primary color - checkIconColor(tester, 'Account Balance', Colors.black45); // enabled - checkIconColor(tester, 'Help & Feedback', Colors.black26); // disabled + checkIconColor(tester, 'Account Balance', Colors.black26); // disabled + checkIconColor(tester, 'About', Colors.black45); // enabled // switch to dark mode await tester.tap(find.text('Pessimistic')); @@ -88,7 +88,7 @@ void main() { // check the colour of the icon - dark mode checkIconColor(tester, 'Stock List', Colors.redAccent[200]); // theme accent color - checkIconColor(tester, 'Account Balance', Colors.white); // enabled - checkIconColor(tester, 'Help & Feedback', Colors.white30); // disabled + checkIconColor(tester, 'Account Balance', Colors.white30); // disabled + checkIconColor(tester, 'About', Colors.white); // enabled }); } diff --git a/packages/flutter/lib/src/material/about.dart b/packages/flutter/lib/src/material/about.dart index 68cab6db72..8254357c38 100644 --- a/packages/flutter/lib/src/material/about.dart +++ b/packages/flutter/lib/src/material/about.dart @@ -17,6 +17,7 @@ import 'icon.dart'; import 'page.dart'; import 'progress_indicator.dart'; import 'scaffold.dart'; +import 'scrollbar.dart'; import 'theme.dart'; /// A [DrawerItem] to show an about box. @@ -426,10 +427,12 @@ class _LicensePageState extends State { ), body: new DefaultTextStyle( style: Theme.of(context).textTheme.caption, - child: new LazyBlock( - padding: new EdgeInsets.symmetric(horizontal: 8.0, vertical: 12.0), - delegate: new LazyBlockChildren( - children: contents + child: new Scrollbar( + child: new LazyBlock( + padding: new EdgeInsets.symmetric(horizontal: 8.0, vertical: 12.0), + delegate: new LazyBlockChildren( + children: contents + ) ) ) ) diff --git a/packages/flutter/lib/src/material/scrollbar.dart b/packages/flutter/lib/src/material/scrollbar.dart index fadf41e2c7..1c1140fcc6 100644 --- a/packages/flutter/lib/src/material/scrollbar.dart +++ b/packages/flutter/lib/src/material/scrollbar.dart @@ -64,10 +64,10 @@ class _Painter extends CustomPainter { @override bool shouldRepaint(_Painter oldPainter) { return oldPainter.scrollOffset != scrollOffset - || oldPainter.scrollDirection != scrollDirection - || oldPainter.contentExtent != contentExtent - || oldPainter.containerExtent != containerExtent - || oldPainter.color != color; + || oldPainter.scrollDirection != scrollDirection + || oldPainter.contentExtent != contentExtent + || oldPainter.containerExtent != containerExtent + || oldPainter.color != color; } } @@ -98,7 +98,6 @@ class Scrollbar extends StatefulWidget { class _ScrollbarState extends State { final AnimationController _fade = new AnimationController(duration: _kScrollbarThumbFadeDuration); CurvedAnimation _opacity; - double _scrollOffsetAnchor; double _scrollOffset; Axis _scrollDirection; double _containerExtent; @@ -119,28 +118,25 @@ class _ScrollbarState extends State { void _updateState(ScrollableState scrollable) { if (scrollable.scrollBehavior is! ExtentScrollBehavior) return; + if (_scrollOffset != scrollable.scrollOffset) + setState(() { _scrollOffset = scrollable.scrollOffset; }); + if (_scrollDirection != scrollable.config.scrollDirection) + setState(() { _scrollDirection = scrollable.config.scrollDirection; }); final ExtentScrollBehavior scrollBehavior = scrollable.scrollBehavior; - _scrollOffset = scrollable.scrollOffset; - _scrollDirection = scrollable.config.scrollDirection; - _contentExtent = scrollBehavior.contentExtent; - _containerExtent = scrollBehavior.containerExtent; + if (_contentExtent != scrollBehavior.contentExtent) + setState(() { _contentExtent = scrollBehavior.contentExtent; }); + if (_containerExtent != scrollBehavior.containerExtent) + setState(() { _containerExtent = scrollBehavior.containerExtent; }); } void _onScrollStarted(ScrollableState scrollable) { _updateState(scrollable); - _scrollOffsetAnchor = _scrollOffset; } void _onScrollUpdated(ScrollableState scrollable) { _updateState(scrollable); - if (!_fade.isAnimating) { - if (_scrollOffsetAnchor != _scrollOffset && _fade.value == 0.0) - _fade.forward(); // Lazily start the scrollbar fade-in. - setState(() { - // If the scrollbar has faded in, rebuild it per the new scrollable state. - // If the fade-in is underway this setState() will have no effect. - }); - } + if (_fade.status != AnimationStatus.completed) + _fade.forward(); } void _onScrollEnded(ScrollableState scrollable) { @@ -150,8 +146,8 @@ class _ScrollbarState extends State { bool _handleScrollNotification(ScrollNotification notification) { if (config.scrollableKey == null) { - if (notification.depth != 0) - return false; + if (notification.depth != 0) + return false; } else if (config.scrollableKey != notification.scrollable.config.key) { return false; } diff --git a/packages/flutter/lib/src/scheduler/binding.dart b/packages/flutter/lib/src/scheduler/binding.dart index fa45028e54..8a739274a3 100644 --- a/packages/flutter/lib/src/scheduler/binding.dart +++ b/packages/flutter/lib/src/scheduler/binding.dart @@ -72,6 +72,7 @@ class _FrameCallbackEntry { }); stack = currentCallbackStack; } else { + // TODO(ianh): trim the frames from this library, so that the call to scheduleFrameCallback is the top one stack = StackTrace.current; } return true; @@ -283,25 +284,30 @@ abstract class SchedulerBinding extends BindingBase { bool debugAssertNoTransientCallbacks(String reason) { assert(() { if (transientCallbackCount > 0) { + // We cache the values so that we can produce them later + // even if the information collector is called after + // the problem has been resolved. + final int count = transientCallbackCount; + final Map callbacks = new Map.from(_transientCallbacks); FlutterError.reportError(new FlutterErrorDetails( exception: reason, library: 'scheduler library', informationCollector: (StringBuffer information) { - if (transientCallbackCount == 1) { + if (count == 1) { information.writeln( 'There was one transient callback left. ' - 'The stack traces for when it was registered is as follows:' + 'The stack trace for when it was registered is as follows:' ); } else { information.writeln( - 'There were $transientCallbackCount transient callbacks left. ' + 'There were $count transient callbacks left. ' 'The stack traces for when they were registered are as follows:' ); } - for (int id in _transientCallbacks.keys) { - _FrameCallbackEntry entry = _transientCallbacks[id]; - information.writeln('-- callback $id --'); - information.writeln(entry.stack); + for (int id in callbacks.keys) { + _FrameCallbackEntry entry = callbacks[id]; + information.writeln('── callback $id ──'); + FlutterError.defaultStackFilter(entry.stack.toString().trimRight().split('\n')).forEach(information.writeln); } } )); diff --git a/packages/flutter/lib/src/widgets/container.dart b/packages/flutter/lib/src/widgets/container.dart index ff706aa3b6..b4431b947a 100644 --- a/packages/flutter/lib/src/widgets/container.dart +++ b/packages/flutter/lib/src/widgets/container.dart @@ -72,6 +72,8 @@ class DecoratedBox extends SingleChildRenderObjectWidget { /// extent. class Container extends StatelessWidget { /// Creates a widget that combines common painting, positioning, and sizing widgets. + /// + /// The `height` and `width` values include the padding. Container({ Key key, this.align, @@ -116,6 +118,8 @@ class Container extends StatelessWidget { final Decoration foregroundDecoration; /// Additional constraints to apply to the child. + /// + /// The [padding] goes inside the constraints. final BoxConstraints constraints; /// Empty space to surround the decoration. diff --git a/packages/flutter/lib/src/widgets/framework.dart b/packages/flutter/lib/src/widgets/framework.dart index 12d15cd731..a0cb221bb6 100644 --- a/packages/flutter/lib/src/widgets/framework.dart +++ b/packages/flutter/lib/src/widgets/framework.dart @@ -405,8 +405,8 @@ abstract class Widget { /// use another widget as its configuration if, and only if, the two widgets /// have [runtimeType] and [key] properties that are [operator==]. static bool canUpdate(Widget oldWidget, Widget newWidget) { - return oldWidget.runtimeType == newWidget.runtimeType && - oldWidget.key == newWidget.key; + return oldWidget.runtimeType == newWidget.runtimeType + && oldWidget.key == newWidget.key; } } diff --git a/packages/flutter/lib/src/widgets/lazy_block.dart b/packages/flutter/lib/src/widgets/lazy_block.dart index 1b2298f683..2df11708f6 100644 --- a/packages/flutter/lib/src/widgets/lazy_block.dart +++ b/packages/flutter/lib/src/widgets/lazy_block.dart @@ -50,17 +50,56 @@ abstract class LazyBlockDelegate { /// When calling this function, [LazyBlock] will always pass an argument that /// matches the runtimeType of the receiver. bool shouldRebuild(LazyBlockDelegate oldDelegate); + + /// Returns the estimated total height of the children, in pixels. + /// + /// If there's an infinite number of children, this should return + /// [double.INFINITY]. + /// + /// The provided values can be used to estimate the total extent. + /// + /// The `firstIndex` and `lastIndex` values give the integers that were passed + /// to [buildItem] to build the respective widgets. + /// + /// The `minOffset` is the offset of the widget with index 0. Unless the + /// `firstIndex` is 0, the `minOffset` is only itself an estimate. + /// + /// The `firstStartOffset` is the offset of the widget with `firstIndex`, in + /// the same coordinate space as `minOffset`. + /// + /// The `lastEndOffset` is the offset of the widget that would be after + /// `lastIndex`, in the same coordinate space as `minOffset`. (In other words, + /// it's the offset to the end of the `lastIndex` widget.) + /// + /// A simple algorithm for this function, which works well when there are many + /// children, the exact child count is known, and the children near the top of + /// the list are more or less representative of the length of the other + /// children, is the following: + /// + /// ```dart + /// // childCount is the number of children + /// return (lastEndOffset - minOffset) * childCount / (lastIndex + 1); + /// ``` + double estimateTotalExtent(int firstIndex, int lastIndex, double minOffset, double firstStartOffset, double lastEndOffset); } +/// Signature for callbacks that estimate the total height of a [LazyBlock]'s contents. +/// +/// See [LazyBlockDelegate.estimateTotalExtent] for details. +typedef double TotalExtentEstimator(int firstIndex, int lastIndex, double minOffset, double firstStartOffset, double lastEndOffset); + /// Uses an [IndexedWidgetBuilder] to provide children for [LazyBlock]. /// /// A LazyBlockBuilder rebuilds the children whenever the [LazyBlock] is /// rebuilt, similar to the behavior of [Builder]. /// +/// To use a [Scrollbar] with this delegate, you must provide an +/// [estimateTotalExtent] callback. +/// /// See also [LazyBlockViewport]. class LazyBlockBuilder extends LazyBlockDelegate { /// Creates a LazyBlockBuilder based on the given builder. - LazyBlockBuilder({ this.builder }) { + LazyBlockBuilder({ this.builder, this.totalExtentEstimator }) { assert(builder != null); } @@ -76,11 +115,26 @@ class LazyBlockBuilder extends LazyBlockDelegate { /// pipeline. final IndexedWidgetBuilder builder; + /// Returns the estimated total height of the children, in pixels. + /// + /// If null, the estimate will be infinite, even if a null child has been + /// returned by [builder]. + /// + /// See [LazyBlockDelegate.estimateTotalExtent] for details. + final TotalExtentEstimator totalExtentEstimator; + @override Widget buildItem(BuildContext context, int index) => builder(context, index); @override bool shouldRebuild(LazyBlockDelegate oldDelegate) => true; + + @override + double estimateTotalExtent(int firstIndex, int lastIndex, double minOffset, double firstStartOffset, double lastEndOffset) { + if (totalExtentEstimator != null) + return totalExtentEstimator(firstIndex, lastIndex, minOffset, firstStartOffset, lastEndOffset); + return double.INFINITY; + } } /// Uses a [List] to provide children for [LazyBlock]. @@ -110,6 +164,14 @@ class LazyBlockChildren extends LazyBlockDelegate { bool shouldRebuild(LazyBlockChildren oldDelegate) { return children != oldDelegate.children; } + + @override + double estimateTotalExtent(int firstIndex, int lastIndex, double minOffset, double firstStartOffset, double lastEndOffset) { + final int childCount = children.length; + if (childCount == 0) + return 0.0; + return (lastEndOffset - minOffset) * childCount / (lastIndex + 1); + } } /// An infinite scrolling list of variably-sized children. @@ -202,10 +264,10 @@ class LazyBlock extends StatelessWidget { startOffset: scrollOffset, mainAxis: scrollDirection, padding: padding, - onExtentsChanged: (double contentExtent, double containerExtent, double minScrollOffset) { + onExtentsChanged: (int firstIndex, int lastIndex, double firstStartOffset, double lastEndOffset, double minScrollOffset, double containerExtent) { final BoundedBehavior scrollBehavior = state.scrollBehavior; state.didUpdateScrollBehavior(scrollBehavior.updateExtents( - contentExtent: contentExtent, + contentExtent: delegate.estimateTotalExtent(firstIndex, lastIndex, minScrollOffset, firstStartOffset, lastEndOffset), containerExtent: containerExtent, minScrollOffset: minScrollOffset, scrollOffset: state.scrollOffset @@ -237,12 +299,16 @@ class LazyBlock extends StatelessWidget { /// Signature used by [LazyBlockViewport] to report its interior and exterior dimensions. /// -/// * The [contentExtent] is the interior dimension of the viewport (i.e., the -/// size of the thing that's being viewed through the viewport). -/// * The [containerExtent] is the exterior dimension of the viewport (i.e., -/// the amount of the thing inside the viewport that is visible from outside -/// the viewport). -/// * The [minScrollOffset] is the offset at which the starting edge of the +/// * The `firstIndex` is the index of the child that is visible at the +/// starting edge of the viewport. +/// * The `lastIndex` is the index of the child that is visible at the ending +/// edge of the viewport. This could be the same as the `firstIndex` if the +/// child is bigger than the viewport or if it is the last child. +/// * The `firstStartOffset` is the offset of the starting edge of the child +/// with index `firstIndex`. +/// * The `lastEndOffset` is the offset of the ending edge of the child with +/// index `lastIndex`. +/// * The `minScrollOffset` is the offset at which the starting edge of the /// first item in the viewport is aligned with the starting edge of the /// viewport. (As the scroll offset increases, items with larger indices are /// revealed in the viewport.) Typically the min scroll offset is 0.0, but @@ -250,7 +316,10 @@ class LazyBlock extends StatelessWidget { /// might not always be 0.0. For example, if an item that's offscreen changes /// size, the visible items will retain their current scroll offsets even if /// the distance to the starting edge of the first item changes. -typedef void LazyBlockExtentsChangedCallback(double contentExtent, double containerExtent, double minScrollOffset); +/// * The `containerExtent` is the exterior dimension of the viewport (i.e., +/// the amount of the thing inside the viewport that is visible from outside +/// the viewport). +typedef void LazyBlockExtentsChangedCallback(int firstIndex, int lastIndex, double firstStartOffset, double lastEndOffset, double minScrollOffset, double containerExtent); /// A viewport on an infinite list of variable height children. /// @@ -315,8 +384,6 @@ class LazyBlockViewport extends RenderObjectWidget { /// See [LazyBlockDelegate] for details. final LazyBlockDelegate delegate; - double get _mainAxisPadding => padding == null ? 0.0 : padding.along(mainAxis); - @override _LazyBlockElement createElement() => new _LazyBlockElement(this); @@ -424,21 +491,18 @@ class _LazyBlockElement extends RenderObjectElement { /// reprsented explicitly in _children. double _minScrollOffset = 0.0; - /// The maximum scroll offset used by the scroll behavior. - /// - /// Not all the items between the minimum and maximum scroll offsets are - /// reprsented explicitly in _children. - double _maxScrollOffset = 0.0; - /// The smallest start offset (inclusive) that can be displayed properly with the items currently represented in [_children]. double _startOffsetLowerLimit = 0.0; /// The largest start offset (exclusive) that can be displayed properly with the items currently represented in [_children]. double _startOffsetUpperLimit = 0.0; - double _lastReportedContentExtent; - double _lastReportedContainerExtent; + int _lastReportedFirstChildLogicalIndex; + int _lastReportedLastChildLogicalIndex; + double _lastReportedFirstChildLogicalOffset; + double _lastReportedLastChildLogicalOffset; double _lastReportedMinScrollOffset; + double _lastReportedContainerExtent; @override void visitChildren(ElementVisitor visitor) { @@ -485,13 +549,45 @@ class _LazyBlockElement extends RenderObjectElement { super.unmount(); } + Widget _callBuilder(IndexedWidgetBuilder builder, int index, { bool requireNonNull: false }) { + Widget result; + try { + result = builder(this, index); + if (requireNonNull && result == null) { + throw new FlutterError( + 'buildItem must not return null after returning non-null.\n' + 'If buildItem for a LazyBlockDelegate returns a non-null widget for a given ' + 'index, it must return non-null widgets for every smaller index as well. The ' + 'buildItem function for ${widget.delegate.runtimeType} returned null for ' + 'index $index after having returned a non-null value for index ' + '${index - 1}.' + ); + } + } catch (e, stack) { + FlutterError.reportError(new FlutterErrorDetails( + exception: e, + stack: stack, + library: 'widgets library', + context: 'while building items for a LazyBlock', + informationCollector: (StringBuffer information) { + information.writeln('The LazyBlock in question was:\n $this'); + information.writeln('The delegate that was being used was:\n ${widget.delegate}'); + information.write('The index of the offending child widget was: $index'); + } + )); + result = new ErrorWidget(e); + } + return result; + } + + @override void performRebuild() { IndexedWidgetBuilder builder = widget.delegate.buildItem; List widgets = []; for (int i = 0; i < _children.length; ++i) { int logicalIndex = _firstChildLogicalIndex + i; - Widget childWidget = builder(this, logicalIndex); + Widget childWidget = _callBuilder(builder, logicalIndex); if (childWidget == null) break; widgets.add(new RepaintBoundary.wrap(childWidget, logicalIndex)); @@ -534,18 +630,7 @@ class _LazyBlockElement extends RenderObjectElement { currentLogicalIndex -= 1; Element newElement; owner.lockState(() { - // TODO(abarth): Handle exceptions from builder gracefully. - Widget newWidget = builder(this, currentLogicalIndex); - if (newWidget == null) { - throw new FlutterError( - 'buildItem must not return null after returning non-null.\n' - 'If buildItem for a LazyBlockDelegate returns a non-null widget for a given ' - 'index, it must return non-null widgets for every smaller index as well. The ' - 'buildItem function for ${widget.delegate.runtimeType} returned null for ' - 'index $currentLogicalIndex after having returned a non-null value for index ' - '${currentLogicalIndex - 1}.' - ); - } + Widget newWidget = _callBuilder(builder, currentLogicalIndex, requireNonNull: true); newWidget = new RepaintBoundary.wrap(newWidget, currentLogicalIndex); newElement = inflateWidget(newWidget, null); }, building: true); @@ -597,12 +682,10 @@ class _LazyBlockElement extends RenderObjectElement { if (currentLogicalOffset >= startLogicalOffset) { // The first element is visible. We need to update our reckoning of where // the min scroll offset is. - _minScrollOffset = currentLogicalOffset; _startOffsetLowerLimit = double.NEGATIVE_INFINITY; } else { // The first element is not visible. Ensure that we have one blockExtent // of headroom so we don't hit the min scroll offset prematurely. - _minScrollOffset = currentLogicalOffset - blockExtent; _startOffsetLowerLimit = currentLogicalOffset; } @@ -616,8 +699,7 @@ class _LazyBlockElement extends RenderObjectElement { assert(physicalIndex == _children.length); Element newElement; owner.lockState(() { - // TODO(abarth): Handle exceptions from builder gracefully. - Widget newWidget = builder(this, currentLogicalIndex); + Widget newWidget = _callBuilder(builder, currentLogicalIndex); if (newWidget == null) return; newWidget = new RepaintBoundary.wrap(newWidget, currentLogicalIndex); @@ -644,14 +726,10 @@ class _LazyBlockElement extends RenderObjectElement { // we don't need. if (currentLogicalOffset < endLogicalOffset) { - // The last element is visible. We need to update our reckoning of where - // the max scroll offset is. - _maxScrollOffset = currentLogicalOffset + widget._mainAxisPadding - blockExtent; + // The last element is visible. We can scroll as far as they want, there's + // nothing more to paint. _startOffsetUpperLimit = double.INFINITY; } else { - // The last element is not visible. Ensure that we have one blockExtent - // of headroom so we don't hit the max scroll offset prematurely. - _maxScrollOffset = currentLogicalOffset; _startOffsetUpperLimit = currentLogicalOffset - blockExtent; } @@ -684,14 +762,27 @@ class _LazyBlockElement extends RenderObjectElement { LazyBlockExtentsChangedCallback onExtentsChanged = widget.onExtentsChanged; if (onExtentsChanged != null) { - double contentExtent = _maxScrollOffset - _minScrollOffset + blockExtent; - if (_lastReportedContentExtent != contentExtent || - _lastReportedContainerExtent != blockExtent || - _lastReportedMinScrollOffset != _minScrollOffset) { - _lastReportedContentExtent = contentExtent; - _lastReportedContainerExtent = blockExtent; + int lastChildLogicalIndex = _firstChildLogicalIndex + _children.length - 1; + if (_lastReportedFirstChildLogicalIndex != _firstChildLogicalIndex || + _lastReportedLastChildLogicalIndex != lastChildLogicalIndex || + _lastReportedFirstChildLogicalOffset != _firstChildLogicalIndex || + _lastReportedLastChildLogicalOffset != currentLogicalOffset || + _lastReportedMinScrollOffset != _minScrollOffset || + _lastReportedContainerExtent != blockExtent) { + _lastReportedFirstChildLogicalIndex = _firstChildLogicalIndex; + _lastReportedLastChildLogicalIndex = lastChildLogicalIndex; + _lastReportedFirstChildLogicalOffset = _firstChildLogicalOffset; + _lastReportedLastChildLogicalOffset = currentLogicalOffset; _lastReportedMinScrollOffset = _minScrollOffset; - onExtentsChanged(_lastReportedContentExtent, _lastReportedContainerExtent, _lastReportedMinScrollOffset); + _lastReportedContainerExtent = blockExtent; + onExtentsChanged( + _firstChildLogicalIndex, + lastChildLogicalIndex, + _firstChildLogicalOffset, + currentLogicalOffset, + _lastReportedMinScrollOffset, + _lastReportedContainerExtent + ); } } } diff --git a/packages/flutter/lib/src/widgets/scroll_behavior.dart b/packages/flutter/lib/src/widgets/scroll_behavior.dart index 91c822c664..26d1b1f283 100644 --- a/packages/flutter/lib/src/widgets/scroll_behavior.dart +++ b/packages/flutter/lib/src/widgets/scroll_behavior.dart @@ -233,7 +233,7 @@ class OverscrollWhenScrollableBehavior extends OverscrollBehavior { @override Simulation createScrollSimulation(double position, double velocity) { - if (isScrollable || position < minScrollOffset || position > maxScrollOffset) { + if ((isScrollable && velocity.abs() > 0) || position < minScrollOffset || position > maxScrollOffset) { // If the triggering gesture starts at or beyond the contentExtent's limits // then the simulation only serves to settle the scrollOffset back to its // minimum or maximum value. diff --git a/packages/flutter/lib/src/widgets/scrollable.dart b/packages/flutter/lib/src/widgets/scrollable.dart index 9e91ef2782..d923ed7af2 100644 --- a/packages/flutter/lib/src/widgets/scrollable.dart +++ b/packages/flutter/lib/src/widgets/scrollable.dart @@ -463,7 +463,6 @@ class ScrollableState extends State { Future fling(double scrollVelocity) { if (scrollVelocity.abs() > kPixelScrollTolerance.velocity || !_controller.isAnimating) return _startToEndAnimation(scrollVelocity); - return new Future.value(); } @@ -524,7 +523,7 @@ class ScrollableState extends State { } Simulation _createFlingSimulation(double scrollVelocity) { - final Simulation simulation = scrollBehavior.createScrollSimulation(scrollOffset, scrollVelocity); + final Simulation simulation = scrollBehavior.createScrollSimulation(scrollOffset, scrollVelocity); if (simulation != null) { final double endVelocity = pixelOffsetToScrollOffset(kPixelScrollTolerance.velocity).abs(); final double endDistance = pixelOffsetToScrollOffset(kPixelScrollTolerance.distance).abs(); diff --git a/packages/flutter/test/material/scrollbar_test.dart b/packages/flutter/test/material/scrollbar_test.dart new file mode 100644 index 0000000000..2aa75d5b2a --- /dev/null +++ b/packages/flutter/test/material/scrollbar_test.dart @@ -0,0 +1,51 @@ +// 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/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('Scrollbar doesn\'t show when tapping list', (WidgetTester tester) async { + await tester.pumpWidget( + new Center( + child: new Container( + decoration: new BoxDecoration( + border: new Border.all(color: const Color(0xFFFFFF00)) + ), + height: 200.0, + width: 300.0, + child: new Scrollbar( + child: new Block( + children: [ + new Container(height: 40.0, child: new Text('0')), + new Container(height: 40.0, child: new Text('1')), + new Container(height: 40.0, child: new Text('2')), + new Container(height: 40.0, child: new Text('3')), + new Container(height: 40.0, child: new Text('4')), + new Container(height: 40.0, child: new Text('5')), + new Container(height: 40.0, child: new Text('6')), + new Container(height: 40.0, child: new Text('7')), + ] + ) + ) + ) + ) + ); + + SchedulerBinding.instance.debugAssertNoTransientCallbacks('Building a list with a scrollbar triggered an animation.'); + await tester.tap(find.byType(Block)); + SchedulerBinding.instance.debugAssertNoTransientCallbacks('Tapping a block with a scrollbar triggered an animation.'); + await tester.pump(const Duration(milliseconds: 200)); + await tester.pump(const Duration(milliseconds: 200)); + await tester.pump(const Duration(milliseconds: 200)); + await tester.pump(const Duration(milliseconds: 200)); + await tester.scroll(find.byType(Block), const Offset(0.0, -10.0)); + expect(SchedulerBinding.instance.transientCallbackCount, greaterThan(0)); + await tester.pump(const Duration(milliseconds: 200)); + await tester.pump(const Duration(milliseconds: 200)); + await tester.pump(const Duration(milliseconds: 200)); + await tester.pump(const Duration(milliseconds: 200)); + }); +} diff --git a/packages/flutter/test/widget/lazy_block_viewport_test.dart b/packages/flutter/test/widget/lazy_block_viewport_test.dart index d782767f03..021944313c 100644 --- a/packages/flutter/test/widget/lazy_block_viewport_test.dart +++ b/packages/flutter/test/widget/lazy_block_viewport_test.dart @@ -267,13 +267,19 @@ void main() { }); testWidgets('Underflow extents', (WidgetTester tester) async { - double lastContentExtent; - double lastContainerExtent; + int lastFirstIndex; + int lastLastIndex; + double lastFirstStartOffset; + double lastLastEndOffset; double lastMinScrollOffset; - void handleExtendsChanged(double contentExtent, double containerExtent, double minScrollOffset) { - lastContentExtent = contentExtent; - lastContainerExtent = containerExtent; + double lastContainerExtent; + void handleExtendsChanged(int firstIndex, int lastIndex, double firstStartOffset, double lastEndOffset, double minScrollOffset, double containerExtent) { + lastFirstIndex = firstIndex; + lastLastIndex = lastIndex; + lastFirstStartOffset = firstStartOffset; + lastLastEndOffset = lastEndOffset; lastMinScrollOffset = minScrollOffset; + lastContainerExtent = containerExtent; } await tester.pumpWidget(new LazyBlockViewport( @@ -287,8 +293,11 @@ void main() { ) )); - expect(lastContentExtent, equals(300.0)); - expect(lastContainerExtent, equals(600.0)); - expect(lastMinScrollOffset, equals(0.0)); + expect(lastFirstIndex, 0); + expect(lastLastIndex, 2); + expect(lastFirstStartOffset, 0.0); + expect(lastLastEndOffset, 300.0); + expect(lastContainerExtent, 600.0); + expect(lastMinScrollOffset, 0.0); }); } diff --git a/packages/flutter/test/widget/scrollable_list_horizontal_test.dart b/packages/flutter/test/widget/scrollable_list_horizontal_test.dart index 0c4102c08e..39e8d4eb1f 100644 --- a/packages/flutter/test/widget/scrollable_list_horizontal_test.dart +++ b/packages/flutter/test/widget/scrollable_list_horizontal_test.dart @@ -27,7 +27,7 @@ Widget buildFrame(ViewportAnchor scrollAnchor) { } void main() { - testWidgets('Drag horizontally with scroll anchor at top', (WidgetTester tester) async { + testWidgets('Drag horizontally with scroll anchor at start', (WidgetTester tester) async { await tester.pumpWidget(buildFrame(ViewportAnchor.start)); await tester.pump(const Duration(seconds: 1));