Commit 51f8fb99 authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Add a scrollbar to the license screen. (#5114)

And make Scrollbar work with LazyBlock.

And an about box to the Stocks sample app.
parent 2b3099c8
...@@ -64,6 +64,11 @@ class FancyItemDelegate extends LazyBlockDelegate { ...@@ -64,6 +64,11 @@ class FancyItemDelegate extends LazyBlockDelegate {
@override @override
bool shouldRebuild(FancyItemDelegate oldDelegate) => false; 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<ComplexLayout> { class ComplexLayoutState extends State<ComplexLayout> {
......
...@@ -125,6 +125,11 @@ class CardBuilder extends LazyBlockDelegate { ...@@ -125,6 +125,11 @@ class CardBuilder extends LazyBlockDelegate {
bool shouldRebuild(CardBuilder oldDelegate) { bool shouldRebuild(CardBuilder oldDelegate) {
return oldDelegate.cardModels != cardModels; 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<OverlayGeometryApp> { class OverlayGeometryAppState extends State<OverlayGeometryApp> {
......
...@@ -132,29 +132,7 @@ class StockHomeState extends State<StockHome> { ...@@ -132,29 +132,7 @@ class StockHomeState extends State<StockHome> {
), ),
new DrawerItem( new DrawerItem(
icon: new Icon(Icons.account_balance), icon: new Icon(Icons.account_balance),
onPressed: () { onPressed: null,
showDialog(
context: context,
child: new Dialog(
title: new Text('Not Implemented'),
content: new Text('This feature has not yet been implemented.'),
actions: <Widget>[
new FlatButton(
onPressed: () {
Navigator.pop(context, false);
},
child: new Text('USE IT')
),
new FlatButton(
onPressed: () {
Navigator.pop(context, false);
},
child: new Text('OH WELL')
),
]
)
);
},
child: new Text('Account Balance') child: new Text('Account Balance')
), ),
new DrawerItem( new DrawerItem(
...@@ -199,7 +177,8 @@ class StockHomeState extends State<StockHome> { ...@@ -199,7 +177,8 @@ class StockHomeState extends State<StockHome> {
child: new Text('Settings')), child: new Text('Settings')),
new DrawerItem( new DrawerItem(
icon: new Icon(Icons.help), icon: new Icon(Icons.help),
child: new Text('Help & Feedback')) onPressed: _handleShowAbout,
child: new Text('About'))
]) ])
); );
} }
...@@ -208,6 +187,10 @@ class StockHomeState extends State<StockHome> { ...@@ -208,6 +187,10 @@ class StockHomeState extends State<StockHome> {
Navigator.popAndPushNamed(context, '/settings'); Navigator.popAndPushNamed(context, '/settings');
} }
void _handleShowAbout() {
showAboutDialog(context: context);
}
Widget buildAppBar() { Widget buildAppBar() {
return new AppBar( return new AppBar(
elevation: 0, elevation: 0,
......
...@@ -58,10 +58,10 @@ void main() { ...@@ -58,10 +58,10 @@ void main() {
// sanity check // sanity check
expect(find.text('MARKET'), findsOneWidget); expect(find.text('MARKET'), findsOneWidget);
expect(find.text('Help & Feedback'), findsNothing); expect(find.text('Account Balance'), findsNothing);
await tester.pump(new Duration(seconds: 2)); await tester.pump(new Duration(seconds: 2));
expect(find.text('MARKET'), findsOneWidget); expect(find.text('MARKET'), findsOneWidget);
expect(find.text('Help & Feedback'), findsNothing); expect(find.text('Account Balance'), findsNothing);
// drag the drawer out // drag the drawer out
Point left = new Point(0.0, ui.window.size.height / 2.0); Point left = new Point(0.0, ui.window.size.height / 2.0);
...@@ -73,12 +73,12 @@ void main() { ...@@ -73,12 +73,12 @@ void main() {
await gesture.up(); await gesture.up();
await tester.pump(); await tester.pump();
expect(find.text('MARKET'), findsOneWidget); 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 // check the colour of the icon - light mode
checkIconColor(tester, 'Stock List', Colors.purple[500]); // theme primary color checkIconColor(tester, 'Stock List', Colors.purple[500]); // theme primary color
checkIconColor(tester, 'Account Balance', Colors.black45); // enabled checkIconColor(tester, 'Account Balance', Colors.black26); // disabled
checkIconColor(tester, 'Help & Feedback', Colors.black26); // disabled checkIconColor(tester, 'About', Colors.black45); // enabled
// switch to dark mode // switch to dark mode
await tester.tap(find.text('Pessimistic')); await tester.tap(find.text('Pessimistic'));
...@@ -88,7 +88,7 @@ void main() { ...@@ -88,7 +88,7 @@ void main() {
// check the colour of the icon - dark mode // check the colour of the icon - dark mode
checkIconColor(tester, 'Stock List', Colors.redAccent[200]); // theme accent color checkIconColor(tester, 'Stock List', Colors.redAccent[200]); // theme accent color
checkIconColor(tester, 'Account Balance', Colors.white); // enabled checkIconColor(tester, 'Account Balance', Colors.white30); // disabled
checkIconColor(tester, 'Help & Feedback', Colors.white30); // disabled checkIconColor(tester, 'About', Colors.white); // enabled
}); });
} }
...@@ -17,6 +17,7 @@ import 'icon.dart'; ...@@ -17,6 +17,7 @@ import 'icon.dart';
import 'page.dart'; import 'page.dart';
import 'progress_indicator.dart'; import 'progress_indicator.dart';
import 'scaffold.dart'; import 'scaffold.dart';
import 'scrollbar.dart';
import 'theme.dart'; import 'theme.dart';
/// A [DrawerItem] to show an about box. /// A [DrawerItem] to show an about box.
...@@ -426,10 +427,12 @@ class _LicensePageState extends State<LicensePage> { ...@@ -426,10 +427,12 @@ class _LicensePageState extends State<LicensePage> {
), ),
body: new DefaultTextStyle( body: new DefaultTextStyle(
style: Theme.of(context).textTheme.caption, style: Theme.of(context).textTheme.caption,
child: new LazyBlock( child: new Scrollbar(
padding: new EdgeInsets.symmetric(horizontal: 8.0, vertical: 12.0), child: new LazyBlock(
delegate: new LazyBlockChildren( padding: new EdgeInsets.symmetric(horizontal: 8.0, vertical: 12.0),
children: contents delegate: new LazyBlockChildren(
children: contents
)
) )
) )
) )
......
...@@ -64,10 +64,10 @@ class _Painter extends CustomPainter { ...@@ -64,10 +64,10 @@ class _Painter extends CustomPainter {
@override @override
bool shouldRepaint(_Painter oldPainter) { bool shouldRepaint(_Painter oldPainter) {
return oldPainter.scrollOffset != scrollOffset return oldPainter.scrollOffset != scrollOffset
|| oldPainter.scrollDirection != scrollDirection || oldPainter.scrollDirection != scrollDirection
|| oldPainter.contentExtent != contentExtent || oldPainter.contentExtent != contentExtent
|| oldPainter.containerExtent != containerExtent || oldPainter.containerExtent != containerExtent
|| oldPainter.color != color; || oldPainter.color != color;
} }
} }
...@@ -98,7 +98,6 @@ class Scrollbar extends StatefulWidget { ...@@ -98,7 +98,6 @@ class Scrollbar extends StatefulWidget {
class _ScrollbarState extends State<Scrollbar> { class _ScrollbarState extends State<Scrollbar> {
final AnimationController _fade = new AnimationController(duration: _kScrollbarThumbFadeDuration); final AnimationController _fade = new AnimationController(duration: _kScrollbarThumbFadeDuration);
CurvedAnimation _opacity; CurvedAnimation _opacity;
double _scrollOffsetAnchor;
double _scrollOffset; double _scrollOffset;
Axis _scrollDirection; Axis _scrollDirection;
double _containerExtent; double _containerExtent;
...@@ -119,28 +118,25 @@ class _ScrollbarState extends State<Scrollbar> { ...@@ -119,28 +118,25 @@ class _ScrollbarState extends State<Scrollbar> {
void _updateState(ScrollableState scrollable) { void _updateState(ScrollableState scrollable) {
if (scrollable.scrollBehavior is! ExtentScrollBehavior) if (scrollable.scrollBehavior is! ExtentScrollBehavior)
return; return;
if (_scrollOffset != scrollable.scrollOffset)
setState(() { _scrollOffset = scrollable.scrollOffset; });
if (_scrollDirection != scrollable.config.scrollDirection)
setState(() { _scrollDirection = scrollable.config.scrollDirection; });
final ExtentScrollBehavior scrollBehavior = scrollable.scrollBehavior; final ExtentScrollBehavior scrollBehavior = scrollable.scrollBehavior;
_scrollOffset = scrollable.scrollOffset; if (_contentExtent != scrollBehavior.contentExtent)
_scrollDirection = scrollable.config.scrollDirection; setState(() { _contentExtent = scrollBehavior.contentExtent; });
_contentExtent = scrollBehavior.contentExtent; if (_containerExtent != scrollBehavior.containerExtent)
_containerExtent = scrollBehavior.containerExtent; setState(() { _containerExtent = scrollBehavior.containerExtent; });
} }
void _onScrollStarted(ScrollableState scrollable) { void _onScrollStarted(ScrollableState scrollable) {
_updateState(scrollable); _updateState(scrollable);
_scrollOffsetAnchor = _scrollOffset;
} }
void _onScrollUpdated(ScrollableState scrollable) { void _onScrollUpdated(ScrollableState scrollable) {
_updateState(scrollable); _updateState(scrollable);
if (!_fade.isAnimating) { if (_fade.status != AnimationStatus.completed)
if (_scrollOffsetAnchor != _scrollOffset && _fade.value == 0.0) _fade.forward();
_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.
});
}
} }
void _onScrollEnded(ScrollableState scrollable) { void _onScrollEnded(ScrollableState scrollable) {
...@@ -150,8 +146,8 @@ class _ScrollbarState extends State<Scrollbar> { ...@@ -150,8 +146,8 @@ class _ScrollbarState extends State<Scrollbar> {
bool _handleScrollNotification(ScrollNotification notification) { bool _handleScrollNotification(ScrollNotification notification) {
if (config.scrollableKey == null) { if (config.scrollableKey == null) {
if (notification.depth != 0) if (notification.depth != 0)
return false; return false;
} else if (config.scrollableKey != notification.scrollable.config.key) { } else if (config.scrollableKey != notification.scrollable.config.key) {
return false; return false;
} }
......
...@@ -72,6 +72,7 @@ class _FrameCallbackEntry { ...@@ -72,6 +72,7 @@ class _FrameCallbackEntry {
}); });
stack = currentCallbackStack; stack = currentCallbackStack;
} else { } else {
// TODO(ianh): trim the frames from this library, so that the call to scheduleFrameCallback is the top one
stack = StackTrace.current; stack = StackTrace.current;
} }
return true; return true;
...@@ -283,25 +284,30 @@ abstract class SchedulerBinding extends BindingBase { ...@@ -283,25 +284,30 @@ abstract class SchedulerBinding extends BindingBase {
bool debugAssertNoTransientCallbacks(String reason) { bool debugAssertNoTransientCallbacks(String reason) {
assert(() { assert(() {
if (transientCallbackCount > 0) { 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<int, _FrameCallbackEntry> callbacks = new Map<int, _FrameCallbackEntry>.from(_transientCallbacks);
FlutterError.reportError(new FlutterErrorDetails( FlutterError.reportError(new FlutterErrorDetails(
exception: reason, exception: reason,
library: 'scheduler library', library: 'scheduler library',
informationCollector: (StringBuffer information) { informationCollector: (StringBuffer information) {
if (transientCallbackCount == 1) { if (count == 1) {
information.writeln( information.writeln(
'There was one transient callback left. ' '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 { } else {
information.writeln( 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:' 'The stack traces for when they were registered are as follows:'
); );
} }
for (int id in _transientCallbacks.keys) { for (int id in callbacks.keys) {
_FrameCallbackEntry entry = _transientCallbacks[id]; _FrameCallbackEntry entry = callbacks[id];
information.writeln('-- callback $id --'); information.writeln('── callback $id ──');
information.writeln(entry.stack); FlutterError.defaultStackFilter(entry.stack.toString().trimRight().split('\n')).forEach(information.writeln);
} }
} }
)); ));
......
...@@ -72,6 +72,8 @@ class DecoratedBox extends SingleChildRenderObjectWidget { ...@@ -72,6 +72,8 @@ class DecoratedBox extends SingleChildRenderObjectWidget {
/// extent. /// extent.
class Container extends StatelessWidget { class Container extends StatelessWidget {
/// Creates a widget that combines common painting, positioning, and sizing widgets. /// Creates a widget that combines common painting, positioning, and sizing widgets.
///
/// The `height` and `width` values include the padding.
Container({ Container({
Key key, Key key,
this.align, this.align,
...@@ -116,6 +118,8 @@ class Container extends StatelessWidget { ...@@ -116,6 +118,8 @@ class Container extends StatelessWidget {
final Decoration foregroundDecoration; final Decoration foregroundDecoration;
/// Additional constraints to apply to the child. /// Additional constraints to apply to the child.
///
/// The [padding] goes inside the constraints.
final BoxConstraints constraints; final BoxConstraints constraints;
/// Empty space to surround the decoration. /// Empty space to surround the decoration.
......
...@@ -405,8 +405,8 @@ abstract class Widget { ...@@ -405,8 +405,8 @@ abstract class Widget {
/// use another widget as its configuration if, and only if, the two widgets /// use another widget as its configuration if, and only if, the two widgets
/// have [runtimeType] and [key] properties that are [operator==]. /// have [runtimeType] and [key] properties that are [operator==].
static bool canUpdate(Widget oldWidget, Widget newWidget) { static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType && return oldWidget.runtimeType == newWidget.runtimeType
oldWidget.key == newWidget.key; && oldWidget.key == newWidget.key;
} }
} }
......
...@@ -50,17 +50,56 @@ abstract class LazyBlockDelegate { ...@@ -50,17 +50,56 @@ abstract class LazyBlockDelegate {
/// When calling this function, [LazyBlock] will always pass an argument that /// When calling this function, [LazyBlock] will always pass an argument that
/// matches the runtimeType of the receiver. /// matches the runtimeType of the receiver.
bool shouldRebuild(LazyBlockDelegate oldDelegate); 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]. /// Uses an [IndexedWidgetBuilder] to provide children for [LazyBlock].
/// ///
/// A LazyBlockBuilder rebuilds the children whenever the [LazyBlock] is /// A LazyBlockBuilder rebuilds the children whenever the [LazyBlock] is
/// rebuilt, similar to the behavior of [Builder]. /// rebuilt, similar to the behavior of [Builder].
/// ///
/// To use a [Scrollbar] with this delegate, you must provide an
/// [estimateTotalExtent] callback.
///
/// See also [LazyBlockViewport]. /// See also [LazyBlockViewport].
class LazyBlockBuilder extends LazyBlockDelegate { class LazyBlockBuilder extends LazyBlockDelegate {
/// Creates a LazyBlockBuilder based on the given builder. /// Creates a LazyBlockBuilder based on the given builder.
LazyBlockBuilder({ this.builder }) { LazyBlockBuilder({ this.builder, this.totalExtentEstimator }) {
assert(builder != null); assert(builder != null);
} }
...@@ -76,11 +115,26 @@ class LazyBlockBuilder extends LazyBlockDelegate { ...@@ -76,11 +115,26 @@ class LazyBlockBuilder extends LazyBlockDelegate {
/// pipeline. /// pipeline.
final IndexedWidgetBuilder builder; 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 @override
Widget buildItem(BuildContext context, int index) => builder(context, index); Widget buildItem(BuildContext context, int index) => builder(context, index);
@override @override
bool shouldRebuild(LazyBlockDelegate oldDelegate) => true; 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<Widget>] to provide children for [LazyBlock]. /// Uses a [List<Widget>] to provide children for [LazyBlock].
...@@ -110,6 +164,14 @@ class LazyBlockChildren extends LazyBlockDelegate { ...@@ -110,6 +164,14 @@ class LazyBlockChildren extends LazyBlockDelegate {
bool shouldRebuild(LazyBlockChildren oldDelegate) { bool shouldRebuild(LazyBlockChildren oldDelegate) {
return children != oldDelegate.children; 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. /// An infinite scrolling list of variably-sized children.
...@@ -202,10 +264,10 @@ class LazyBlock extends StatelessWidget { ...@@ -202,10 +264,10 @@ class LazyBlock extends StatelessWidget {
startOffset: scrollOffset, startOffset: scrollOffset,
mainAxis: scrollDirection, mainAxis: scrollDirection,
padding: padding, 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; final BoundedBehavior scrollBehavior = state.scrollBehavior;
state.didUpdateScrollBehavior(scrollBehavior.updateExtents( state.didUpdateScrollBehavior(scrollBehavior.updateExtents(
contentExtent: contentExtent, contentExtent: delegate.estimateTotalExtent(firstIndex, lastIndex, minScrollOffset, firstStartOffset, lastEndOffset),
containerExtent: containerExtent, containerExtent: containerExtent,
minScrollOffset: minScrollOffset, minScrollOffset: minScrollOffset,
scrollOffset: state.scrollOffset scrollOffset: state.scrollOffset
...@@ -237,12 +299,16 @@ class LazyBlock extends StatelessWidget { ...@@ -237,12 +299,16 @@ class LazyBlock extends StatelessWidget {
/// Signature used by [LazyBlockViewport] to report its interior and exterior dimensions. /// Signature used by [LazyBlockViewport] to report its interior and exterior dimensions.
/// ///
/// * The [contentExtent] is the interior dimension of the viewport (i.e., the /// * The `firstIndex` is the index of the child that is visible at the
/// size of the thing that's being viewed through the viewport). /// starting edge of the viewport.
/// * The [containerExtent] is the exterior dimension of the viewport (i.e., /// * The `lastIndex` is the index of the child that is visible at the ending
/// the amount of the thing inside the viewport that is visible from outside /// edge of the viewport. This could be the same as the `firstIndex` if the
/// the viewport). /// child is bigger than the viewport or if it is the last child.
/// * The [minScrollOffset] is the offset at which the starting edge of the /// * 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 /// first item in the viewport is aligned with the starting edge of the
/// viewport. (As the scroll offset increases, items with larger indices are /// viewport. (As the scroll offset increases, items with larger indices are
/// revealed in the viewport.) Typically the min scroll offset is 0.0, but /// revealed in the viewport.) Typically the min scroll offset is 0.0, but
...@@ -250,7 +316,10 @@ class LazyBlock extends StatelessWidget { ...@@ -250,7 +316,10 @@ class LazyBlock extends StatelessWidget {
/// might not always be 0.0. For example, if an item that's offscreen changes /// 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 /// size, the visible items will retain their current scroll offsets even if
/// the distance to the starting edge of the first item changes. /// 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. /// A viewport on an infinite list of variable height children.
/// ///
...@@ -315,8 +384,6 @@ class LazyBlockViewport extends RenderObjectWidget { ...@@ -315,8 +384,6 @@ class LazyBlockViewport extends RenderObjectWidget {
/// See [LazyBlockDelegate] for details. /// See [LazyBlockDelegate] for details.
final LazyBlockDelegate delegate; final LazyBlockDelegate delegate;
double get _mainAxisPadding => padding == null ? 0.0 : padding.along(mainAxis);
@override @override
_LazyBlockElement createElement() => new _LazyBlockElement(this); _LazyBlockElement createElement() => new _LazyBlockElement(this);
...@@ -424,21 +491,18 @@ class _LazyBlockElement extends RenderObjectElement { ...@@ -424,21 +491,18 @@ class _LazyBlockElement extends RenderObjectElement {
/// reprsented explicitly in _children. /// reprsented explicitly in _children.
double _minScrollOffset = 0.0; 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]. /// The smallest start offset (inclusive) that can be displayed properly with the items currently represented in [_children].
double _startOffsetLowerLimit = 0.0; double _startOffsetLowerLimit = 0.0;
/// The largest start offset (exclusive) that can be displayed properly with the items currently represented in [_children]. /// The largest start offset (exclusive) that can be displayed properly with the items currently represented in [_children].
double _startOffsetUpperLimit = 0.0; double _startOffsetUpperLimit = 0.0;
double _lastReportedContentExtent; int _lastReportedFirstChildLogicalIndex;
double _lastReportedContainerExtent; int _lastReportedLastChildLogicalIndex;
double _lastReportedFirstChildLogicalOffset;
double _lastReportedLastChildLogicalOffset;
double _lastReportedMinScrollOffset; double _lastReportedMinScrollOffset;
double _lastReportedContainerExtent;
@override @override
void visitChildren(ElementVisitor visitor) { void visitChildren(ElementVisitor visitor) {
...@@ -485,13 +549,45 @@ class _LazyBlockElement extends RenderObjectElement { ...@@ -485,13 +549,45 @@ class _LazyBlockElement extends RenderObjectElement {
super.unmount(); 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 @override
void performRebuild() { void performRebuild() {
IndexedWidgetBuilder builder = widget.delegate.buildItem; IndexedWidgetBuilder builder = widget.delegate.buildItem;
List<Widget> widgets = <Widget>[]; List<Widget> widgets = <Widget>[];
for (int i = 0; i < _children.length; ++i) { for (int i = 0; i < _children.length; ++i) {
int logicalIndex = _firstChildLogicalIndex + i; int logicalIndex = _firstChildLogicalIndex + i;
Widget childWidget = builder(this, logicalIndex); Widget childWidget = _callBuilder(builder, logicalIndex);
if (childWidget == null) if (childWidget == null)
break; break;
widgets.add(new RepaintBoundary.wrap(childWidget, logicalIndex)); widgets.add(new RepaintBoundary.wrap(childWidget, logicalIndex));
...@@ -534,18 +630,7 @@ class _LazyBlockElement extends RenderObjectElement { ...@@ -534,18 +630,7 @@ class _LazyBlockElement extends RenderObjectElement {
currentLogicalIndex -= 1; currentLogicalIndex -= 1;
Element newElement; Element newElement;
owner.lockState(() { owner.lockState(() {
// TODO(abarth): Handle exceptions from builder gracefully. Widget newWidget = _callBuilder(builder, currentLogicalIndex, requireNonNull: true);
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}.'
);
}
newWidget = new RepaintBoundary.wrap(newWidget, currentLogicalIndex); newWidget = new RepaintBoundary.wrap(newWidget, currentLogicalIndex);
newElement = inflateWidget(newWidget, null); newElement = inflateWidget(newWidget, null);
}, building: true); }, building: true);
...@@ -597,12 +682,10 @@ class _LazyBlockElement extends RenderObjectElement { ...@@ -597,12 +682,10 @@ class _LazyBlockElement extends RenderObjectElement {
if (currentLogicalOffset >= startLogicalOffset) { if (currentLogicalOffset >= startLogicalOffset) {
// The first element is visible. We need to update our reckoning of where // The first element is visible. We need to update our reckoning of where
// the min scroll offset is. // the min scroll offset is.
_minScrollOffset = currentLogicalOffset;
_startOffsetLowerLimit = double.NEGATIVE_INFINITY; _startOffsetLowerLimit = double.NEGATIVE_INFINITY;
} else { } else {
// The first element is not visible. Ensure that we have one blockExtent // The first element is not visible. Ensure that we have one blockExtent
// of headroom so we don't hit the min scroll offset prematurely. // of headroom so we don't hit the min scroll offset prematurely.
_minScrollOffset = currentLogicalOffset - blockExtent;
_startOffsetLowerLimit = currentLogicalOffset; _startOffsetLowerLimit = currentLogicalOffset;
} }
...@@ -616,8 +699,7 @@ class _LazyBlockElement extends RenderObjectElement { ...@@ -616,8 +699,7 @@ class _LazyBlockElement extends RenderObjectElement {
assert(physicalIndex == _children.length); assert(physicalIndex == _children.length);
Element newElement; Element newElement;
owner.lockState(() { owner.lockState(() {
// TODO(abarth): Handle exceptions from builder gracefully. Widget newWidget = _callBuilder(builder, currentLogicalIndex);
Widget newWidget = builder(this, currentLogicalIndex);
if (newWidget == null) if (newWidget == null)
return; return;
newWidget = new RepaintBoundary.wrap(newWidget, currentLogicalIndex); newWidget = new RepaintBoundary.wrap(newWidget, currentLogicalIndex);
...@@ -644,14 +726,10 @@ class _LazyBlockElement extends RenderObjectElement { ...@@ -644,14 +726,10 @@ class _LazyBlockElement extends RenderObjectElement {
// we don't need. // we don't need.
if (currentLogicalOffset < endLogicalOffset) { if (currentLogicalOffset < endLogicalOffset) {
// The last element is visible. We need to update our reckoning of where // The last element is visible. We can scroll as far as they want, there's
// the max scroll offset is. // nothing more to paint.
_maxScrollOffset = currentLogicalOffset + widget._mainAxisPadding - blockExtent;
_startOffsetUpperLimit = double.INFINITY; _startOffsetUpperLimit = double.INFINITY;
} else { } 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; _startOffsetUpperLimit = currentLogicalOffset - blockExtent;
} }
...@@ -684,14 +762,27 @@ class _LazyBlockElement extends RenderObjectElement { ...@@ -684,14 +762,27 @@ class _LazyBlockElement extends RenderObjectElement {
LazyBlockExtentsChangedCallback onExtentsChanged = widget.onExtentsChanged; LazyBlockExtentsChangedCallback onExtentsChanged = widget.onExtentsChanged;
if (onExtentsChanged != null) { if (onExtentsChanged != null) {
double contentExtent = _maxScrollOffset - _minScrollOffset + blockExtent; int lastChildLogicalIndex = _firstChildLogicalIndex + _children.length - 1;
if (_lastReportedContentExtent != contentExtent || if (_lastReportedFirstChildLogicalIndex != _firstChildLogicalIndex ||
_lastReportedContainerExtent != blockExtent || _lastReportedLastChildLogicalIndex != lastChildLogicalIndex ||
_lastReportedMinScrollOffset != _minScrollOffset) { _lastReportedFirstChildLogicalOffset != _firstChildLogicalIndex ||
_lastReportedContentExtent = contentExtent; _lastReportedLastChildLogicalOffset != currentLogicalOffset ||
_lastReportedContainerExtent = blockExtent; _lastReportedMinScrollOffset != _minScrollOffset ||
_lastReportedContainerExtent != blockExtent) {
_lastReportedFirstChildLogicalIndex = _firstChildLogicalIndex;
_lastReportedLastChildLogicalIndex = lastChildLogicalIndex;
_lastReportedFirstChildLogicalOffset = _firstChildLogicalOffset;
_lastReportedLastChildLogicalOffset = currentLogicalOffset;
_lastReportedMinScrollOffset = _minScrollOffset; _lastReportedMinScrollOffset = _minScrollOffset;
onExtentsChanged(_lastReportedContentExtent, _lastReportedContainerExtent, _lastReportedMinScrollOffset); _lastReportedContainerExtent = blockExtent;
onExtentsChanged(
_firstChildLogicalIndex,
lastChildLogicalIndex,
_firstChildLogicalOffset,
currentLogicalOffset,
_lastReportedMinScrollOffset,
_lastReportedContainerExtent
);
} }
} }
} }
......
...@@ -233,7 +233,7 @@ class OverscrollWhenScrollableBehavior extends OverscrollBehavior { ...@@ -233,7 +233,7 @@ class OverscrollWhenScrollableBehavior extends OverscrollBehavior {
@override @override
Simulation createScrollSimulation(double position, double velocity) { 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 // If the triggering gesture starts at or beyond the contentExtent's limits
// then the simulation only serves to settle the scrollOffset back to its // then the simulation only serves to settle the scrollOffset back to its
// minimum or maximum value. // minimum or maximum value.
......
...@@ -463,7 +463,6 @@ class ScrollableState<T extends Scrollable> extends State<T> { ...@@ -463,7 +463,6 @@ class ScrollableState<T extends Scrollable> extends State<T> {
Future<Null> fling(double scrollVelocity) { Future<Null> fling(double scrollVelocity) {
if (scrollVelocity.abs() > kPixelScrollTolerance.velocity || !_controller.isAnimating) if (scrollVelocity.abs() > kPixelScrollTolerance.velocity || !_controller.isAnimating)
return _startToEndAnimation(scrollVelocity); return _startToEndAnimation(scrollVelocity);
return new Future<Null>.value(); return new Future<Null>.value();
} }
...@@ -524,7 +523,7 @@ class ScrollableState<T extends Scrollable> extends State<T> { ...@@ -524,7 +523,7 @@ class ScrollableState<T extends Scrollable> extends State<T> {
} }
Simulation _createFlingSimulation(double scrollVelocity) { Simulation _createFlingSimulation(double scrollVelocity) {
final Simulation simulation = scrollBehavior.createScrollSimulation(scrollOffset, scrollVelocity); final Simulation simulation = scrollBehavior.createScrollSimulation(scrollOffset, scrollVelocity);
if (simulation != null) { if (simulation != null) {
final double endVelocity = pixelOffsetToScrollOffset(kPixelScrollTolerance.velocity).abs(); final double endVelocity = pixelOffsetToScrollOffset(kPixelScrollTolerance.velocity).abs();
final double endDistance = pixelOffsetToScrollOffset(kPixelScrollTolerance.distance).abs(); final double endDistance = pixelOffsetToScrollOffset(kPixelScrollTolerance.distance).abs();
......
// 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: <Widget>[
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));
});
}
...@@ -267,13 +267,19 @@ void main() { ...@@ -267,13 +267,19 @@ void main() {
}); });
testWidgets('Underflow extents', (WidgetTester tester) async { testWidgets('Underflow extents', (WidgetTester tester) async {
double lastContentExtent; int lastFirstIndex;
double lastContainerExtent; int lastLastIndex;
double lastFirstStartOffset;
double lastLastEndOffset;
double lastMinScrollOffset; double lastMinScrollOffset;
void handleExtendsChanged(double contentExtent, double containerExtent, double minScrollOffset) { double lastContainerExtent;
lastContentExtent = contentExtent; void handleExtendsChanged(int firstIndex, int lastIndex, double firstStartOffset, double lastEndOffset, double minScrollOffset, double containerExtent) {
lastContainerExtent = containerExtent; lastFirstIndex = firstIndex;
lastLastIndex = lastIndex;
lastFirstStartOffset = firstStartOffset;
lastLastEndOffset = lastEndOffset;
lastMinScrollOffset = minScrollOffset; lastMinScrollOffset = minScrollOffset;
lastContainerExtent = containerExtent;
} }
await tester.pumpWidget(new LazyBlockViewport( await tester.pumpWidget(new LazyBlockViewport(
...@@ -287,8 +293,11 @@ void main() { ...@@ -287,8 +293,11 @@ void main() {
) )
)); ));
expect(lastContentExtent, equals(300.0)); expect(lastFirstIndex, 0);
expect(lastContainerExtent, equals(600.0)); expect(lastLastIndex, 2);
expect(lastMinScrollOffset, equals(0.0)); expect(lastFirstStartOffset, 0.0);
expect(lastLastEndOffset, 300.0);
expect(lastContainerExtent, 600.0);
expect(lastMinScrollOffset, 0.0);
}); });
} }
...@@ -27,7 +27,7 @@ Widget buildFrame(ViewportAnchor scrollAnchor) { ...@@ -27,7 +27,7 @@ Widget buildFrame(ViewportAnchor scrollAnchor) {
} }
void main() { 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.pumpWidget(buildFrame(ViewportAnchor.start));
await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1));
......
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