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 {
@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<ComplexLayout> {
......
......@@ -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<OverlayGeometryApp> {
......
......@@ -132,29 +132,7 @@ class StockHomeState extends State<StockHome> {
),
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: <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')
),
]
)
);
},
onPressed: null,
child: new Text('Account Balance')
),
new DrawerItem(
......@@ -199,7 +177,8 @@ class StockHomeState extends State<StockHome> {
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<StockHome> {
Navigator.popAndPushNamed(context, '/settings');
}
void _handleShowAbout() {
showAboutDialog(context: context);
}
Widget buildAppBar() {
return new AppBar(
elevation: 0,
......
......@@ -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
});
}
......@@ -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<LicensePage> {
),
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
)
)
)
)
......
......@@ -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<Scrollbar> {
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<Scrollbar> {
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<Scrollbar> {
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;
}
......
......@@ -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<int, _FrameCallbackEntry> callbacks = new Map<int, _FrameCallbackEntry>.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);
}
}
));
......
......@@ -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.
......
......@@ -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;
}
}
......
......@@ -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.
......
......@@ -463,7 +463,6 @@ class ScrollableState<T extends Scrollable> extends State<T> {
Future<Null> fling(double scrollVelocity) {
if (scrollVelocity.abs() > kPixelScrollTolerance.velocity || !_controller.isAnimating)
return _startToEndAnimation(scrollVelocity);
return new Future<Null>.value();
}
......@@ -524,7 +523,7 @@ class ScrollableState<T extends Scrollable> extends State<T> {
}
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();
......
// 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() {
});
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);
});
}
......@@ -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));
......
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