Commit 1f06dc44 authored by Hans Muller's avatar Hans Muller

ScrollConfiguration (#4026)

parent 10c861d6
......@@ -191,13 +191,11 @@ class ListDemoState extends State<ListDemo> {
)
]
),
body: new OverscrollIndicator(
child: new Scrollbar(
child: new MaterialList(
type: _itemType,
padding: new EdgeInsets.symmetric(vertical: _dense ? 4.0 : 8.0),
children: listItems
)
body: new Scrollbar(
child: new MaterialList(
type: _itemType,
padding: new EdgeInsets.symmetric(vertical: _dense ? 4.0 : 8.0),
children: listItems
)
)
);
......
......@@ -43,17 +43,22 @@ class OverscrollDemoState extends State<OverscrollDemo> {
break;
}
Widget body = new MaterialList(
type: MaterialListType.threeLine,
padding: const EdgeInsets.all(8.0),
children: _items.map((String item) {
return new ListItem(
isThreeLine: true,
leading: new CircleAvatar(child: new Text(item)),
title: new Text('This item represents $item.'),
subtitle: new Text('Even more additional list item information appears on line three.')
);
})
// The default ScrollConfiguration doesn't include the
// OverscrollIndicator. That's what we want, since this demo
// adds the OverscrollIndicator itself.
Widget body = new ScrollConfiguration(
child: new MaterialList(
type: MaterialListType.threeLine,
padding: const EdgeInsets.all(8.0),
children: _items.map((String item) {
return new ListItem(
isThreeLine: true,
leading: new CircleAvatar(child: new Text(item)),
title: new Text('This item represents $item.'),
subtitle: new Text('Even more additional list item information appears on line three.')
);
})
)
);
switch(_type) {
case IndicatorType.overscroll:
......@@ -91,5 +96,4 @@ class OverscrollDemoState extends State<OverscrollDemo> {
body: body
);
}
}
......@@ -2,10 +2,13 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:io' show Platform;
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'overscroll_indicator.dart';
import 'page.dart';
import 'theme.dart';
......@@ -141,6 +144,13 @@ class MaterialApp extends StatefulWidget {
_MaterialAppState createState() => new _MaterialAppState();
}
class _IndicatorScrollConfigurationDelegate extends ScrollConfigurationDelegate {
@override
Widget wrapScrollWidget(Widget scrollWidget) => new OverscrollIndicator(child: scrollWidget);
}
final ScrollConfigurationDelegate _indicatorScroll = new _IndicatorScrollConfigurationDelegate();
final ScrollConfigurationDelegate _bounceScroll = new ScrollConfigurationDelegate();
class _MaterialAppState extends State<MaterialApp> {
final HeroController _heroController = new HeroController();
......@@ -190,6 +200,9 @@ class _MaterialAppState extends State<MaterialApp> {
return true;
});
return result;
return new ScrollConfiguration(
delegate: (Platform.isIOS || Platform.isMacOS) ? _bounceScroll : _indicatorScroll,
child: result
);
}
}
......@@ -36,7 +36,7 @@ class FrictionSimulation extends Simulation {
/// velocity, and the velocities must be in the direction appropriate for the
/// particle to start from the start position and reach the end position.
factory FrictionSimulation.through(double startPosition, double endPosition, double startVelocity, double endVelocity) {
assert(startVelocity.sign == endVelocity.sign);
assert(startVelocity == 0.0 || endVelocity == 0.0 || startVelocity.sign == endVelocity.sign);
assert(startVelocity.abs() >= endVelocity.abs());
assert((endPosition - startPosition).sign == startVelocity.sign);
return new FrictionSimulation(
......
......@@ -47,6 +47,7 @@ export 'package:flutter/rendering.dart' show
SingleChildLayoutDelegate,
TextOverflow,
ValueChanged,
ValueGetter,
ViewportAnchor,
ViewportDimensions,
ViewportDimensionsChangeCallback;
......
// 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 'framework.dart';
import 'scrollable.dart';
typedef Widget ViewportBuilder(BuildContext context, ScrollableState state, double scrollOffset);
/// If true, the ClampOverscroll's [Scrollable] descendant will clamp its
/// viewport's scrollOffsets to the [ScrollBehavior]'s min and max values.
/// In this case the Scrollable's scrollOffset will still over- and undershoot
/// the ScrollBehavior's limits, but the viewport itself will not.
class ClampOverscrolls extends InheritedWidget {
ClampOverscrolls({
Key key,
this.value,
Widget child
}) : super(key: key, child: child) {
assert(value != null);
assert(child != null);
}
/// True if the [Scrollable] descendant should clamp its viewport's scrollOffset
/// values when they are less than the [ScrollBehavior]'s minimum or greater than
/// its maximum.
final bool value;
static bool of(BuildContext context) {
final ClampOverscrolls result = context.inheritFromWidgetOfExactType(ClampOverscrolls);
return result?.value ?? false;
}
/// If ClampOverscrolls is true, clamps the ScrollableState's scrollOffset to the
/// [ScrollBehavior] minimum and maximum values and then constructs the viewport
/// with the clamped scrollOffset. ClampOverscrolls is reset to false for viewport
/// descendants.
///
/// This utility function is typically used by [Scrollable.builder] callbacks.
static Widget buildViewport(BuildContext context, ScrollableState state, ViewportBuilder builder) {
final bool clampOverscrolls = ClampOverscrolls.of(context);
final double clampedScrollOffset = clampOverscrolls
? state.scrollOffset.clamp(state.scrollBehavior.minScrollOffset, state.scrollBehavior.maxScrollOffset)
: state.scrollOffset;
Widget viewport = builder(context, state, clampedScrollOffset);
if (clampOverscrolls)
viewport = new ClampOverscrolls(value: false, child: viewport);
return viewport;
}
@override
bool updateShouldNotify(ClampOverscrolls old) => value != old.value;
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('value: $value');
}
}
......@@ -7,7 +7,9 @@ import 'dart:math' as math;
import 'package:flutter/rendering.dart';
import 'basic.dart';
import 'clamp_overscrolls.dart';
import 'framework.dart';
import 'scroll_configuration.dart';
import 'scrollable.dart';
import 'scrollable_list.dart';
import 'scroll_behavior.dart';
......@@ -205,13 +207,9 @@ class LazyBlock extends StatelessWidget {
});
}
Widget _buildContent(BuildContext context, ScrollableState state) {
final bool clampOverscrolls = ClampOverscrolls.of(context);
final double startOffset = clampOverscrolls
? state.scrollOffset.clamp(state.scrollBehavior.minScrollOffset, state.scrollBehavior.maxScrollOffset)
: state.scrollOffset;
Widget viewport = new LazyBlockViewport(
startOffset: startOffset,
Widget _buildViewport(BuildContext context, ScrollableState state, double scrollOffset) {
return new LazyBlockViewport(
startOffset: scrollOffset,
mainAxis: scrollDirection,
padding: padding,
onExtentsChanged: (double contentExtent, double containerExtent, double minScrollOffset) {
......@@ -219,14 +217,15 @@ class LazyBlock extends StatelessWidget {
},
delegate: delegate
);
if (clampOverscrolls)
viewport = new ClampOverscrolls(value: false, child: viewport);
return viewport;
}
Widget _buildContent(BuildContext context, ScrollableState state) {
return ClampOverscrolls.buildViewport(context, state, _buildViewport);
}
@override
Widget build(BuildContext context) {
return new Scrollable(
final Widget result = new Scrollable(
key: scrollableKey,
initialScrollOffset: initialScrollOffset,
scrollDirection: scrollDirection,
......@@ -236,6 +235,7 @@ class LazyBlock extends StatelessWidget {
snapOffsetCallback: snapOffsetCallback,
builder: _buildContent
);
return ScrollConfiguration.wrap(context, result);
}
}
......
......@@ -9,6 +9,11 @@ import 'package:flutter/physics.dart';
const double _kSecondsPerMillisecond = 1000.0;
const double _kScrollDrag = 0.025;
// TODO(hansmuller): Simplify these classes. We're no longer using the ScrollBehavior<T, U>
// base class directly. Only LazyBlock uses BoundedBehavior's updateExtents minScrollOffset
// parameter; simpler to move that into ExtentScrollBehavior. All of the classes should
// be called FooScrollBehavior.
/// An interface for controlling the behavior of scrollable widgets.
///
/// The type argument T is the type that describes the scroll offset.
......@@ -220,8 +225,14 @@ class OverscrollWhenScrollableBehavior extends OverscrollBehavior {
@override
Simulation createScrollSimulation(double position, double velocity) {
if (isScrollable || position < minScrollOffset || position > maxScrollOffset)
if (isScrollable || 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.
if (position < minScrollOffset || position > maxScrollOffset)
velocity = 0.0;
return super.createScrollSimulation(position, velocity);
}
return null;
}
......
// 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 'framework.dart';
import 'scroll_behavior.dart';
class ScrollConfigurationDelegate {
/// Returns the ScrollBehavior to be used by generic scrolling containers like
/// [Block]. Returns a new [OverscrollWhenScrollableBehavior] by default.
ExtentScrollBehavior createScrollBehavior() => new OverscrollWhenScrollableBehavior();
/// Generic scrolling containers like [Block] will apply this function to the
/// Scrollable they create. It can be used to add widgets that wrap the
/// Scrollable, like scrollbars or overscroll indicators. By default the
/// [scrollWidget] parameter is returned unchanged.
Widget wrapScrollWidget(Widget scrollWidget) => scrollWidget;
/// Overrides should return true if the this ScrollConfigurationDelegate has
/// changed in a way that requires rebuilding its scrolling container descendants.
/// Returns false by default.
bool updateShouldNotify(ScrollConfigurationDelegate old) => false;
}
/// Used by descendants to initialize and wrap the [Scrollable] widgets
/// they create.
///
/// Classes that create Scrollables are not required to depend on this
/// Widget. The following general purpose scrolling widgets do depend
/// on ScrollConfiguration: Block, LazyBlock, ScrollableViewport,
/// ScrollableList, ScrollableLazyList. The Scrollable base class uses
/// ScrollConfiguration to create its [ScrollBehavior].
class ScrollConfiguration extends InheritedWidget {
ScrollConfiguration({
Key key,
this.delegate,
Widget child
}) : super(key: key, child: child);
static final ScrollConfigurationDelegate _defaultDelegate = new ScrollConfigurationDelegate();
/// Defines the ScrollBehavior and scrollable wrapper for descendants.
final ScrollConfigurationDelegate delegate;
/// The delegate property of the closest instance of this class that encloses
/// the given context.
///
/// If no such instance exists, returns an instance of the
/// [ScrollConfigurationDelegate] base class.
static ScrollConfigurationDelegate of(BuildContext context) {
ScrollConfiguration configuration = context.inheritFromWidgetOfExactType(ScrollConfiguration);
return configuration?.delegate ?? _defaultDelegate;
}
/// A utility function that calls [ScrollConfigurationDelegate.wrapScrollWidget].
static Widget wrap(BuildContext context, Widget scrollWidget) {
return of(context).wrapScrollWidget(scrollWidget);
}
@override
bool updateShouldNotify(ScrollConfiguration old) {
return delegate?.updateShouldNotify(old.delegate) ?? false;
}
}
......@@ -11,11 +11,13 @@ import 'package:flutter/gestures.dart';
import 'package:meta/meta.dart';
import 'basic.dart';
import 'clamp_overscrolls.dart';
import 'framework.dart';
import 'gesture_detector.dart';
import 'notification_listener.dart';
import 'page_storage.dart';
import 'scroll_behavior.dart';
import 'scroll_configuration.dart';
/// The accuracy to which scrolling is computed.
final Tolerance kPixelScrollTolerance = new Tolerance(
......@@ -319,9 +321,15 @@ class ScrollableState<T extends Scrollable> extends State<T> {
}
ExtentScrollBehavior _scrollBehavior;
/// Subclasses should override this function to create the [ScrollBehavior]
/// they desire.
ExtentScrollBehavior createScrollBehavior() => new OverscrollWhenScrollableBehavior();
/// Use the value returned by [ScrollConfiguration.createScrollBehavior].
/// If this widget doesn't have a ScrollConfiguration ancestor,
/// or its createScrollBehavior callback is null, then return a new instance
/// of [OverscrollWhenScrollableBehavior].
ExtentScrollBehavior createScrollBehavior() {
// TODO(hansmuller): this will not be called when the ScrollConfiguration changes.
// An override of dependenciesChanged() is probably needed.
return ScrollConfiguration.of(context)?.createScrollBehavior();
}
bool _scrollOffsetIsInBounds(double scrollOffset) {
if (scrollBehavior is! ExtentScrollBehavior)
......@@ -773,9 +781,9 @@ class _ScrollableViewportState extends State<ScrollableViewport> {
return state.scrollOffsetToPixelDelta(state.scrollOffset);
}
Widget _buildContent(BuildContext context, ScrollableState state) {
Widget _buildViewport(BuildContext context, ScrollableState state, double scrollOffset) {
return new Viewport(
paintOffset: state.scrollOffsetToPixelDelta(state.scrollOffset),
paintOffset: state.scrollOffsetToPixelDelta(scrollOffset),
mainAxis: config.scrollDirection,
anchor: config.scrollAnchor,
onPaintOffsetUpdateNeeded: (ViewportDimensions dimensions) {
......@@ -785,9 +793,13 @@ class _ScrollableViewportState extends State<ScrollableViewport> {
);
}
Widget _buildContent(BuildContext context, ScrollableState state) {
return ClampOverscrolls.buildViewport(context, state, _buildViewport);
}
@override
Widget build(BuildContext context) {
return new Scrollable(
final Widget result = new Scrollable(
key: config.scrollableKey,
initialScrollOffset: config.initialScrollOffset,
scrollDirection: config.scrollDirection,
......@@ -798,6 +810,7 @@ class _ScrollableViewportState extends State<ScrollableViewport> {
snapOffsetCallback: config.snapOffsetCallback,
builder: _buildContent
);
return ScrollConfiguration.wrap(context, result);
}
}
......
......@@ -7,7 +7,9 @@ import 'dart:math' as math;
import 'package:collection/collection.dart' show lowerBound;
import 'package:flutter/rendering.dart';
import 'clamp_overscrolls.dart';
import 'framework.dart';
import 'scroll_configuration.dart';
import 'scrollable.dart';
import 'virtual_viewport.dart';
......@@ -74,9 +76,9 @@ class ScrollableGrid extends StatelessWidget {
});
}
Widget _buildContent(BuildContext context, ScrollableState state) {
Widget _buildViewport(BuildContext context, ScrollableState state, double scrollOffset) {
return new GridViewport(
startOffset: state.scrollOffset,
startOffset: scrollOffset,
delegate: delegate,
onExtentsChanged: (double contentExtent, double containerExtent) {
_handleExtentsChanged(state, contentExtent, containerExtent);
......@@ -85,9 +87,13 @@ class ScrollableGrid extends StatelessWidget {
);
}
Widget _buildContent(BuildContext context, ScrollableState state) {
return ClampOverscrolls.buildViewport(context, state, _buildViewport);
}
@override
Widget build(BuildContext context) {
return new Scrollable(
final Widget result = new Scrollable(
key: scrollableKey,
initialScrollOffset: initialScrollOffset,
// TODO(abarth): Support horizontal offsets. For horizontally scrolling
......@@ -100,6 +106,7 @@ class ScrollableGrid extends StatelessWidget {
snapOffsetCallback: snapOffsetCallback,
builder: _buildContent
);
return ScrollConfiguration.wrap(context, result);
}
}
......
......@@ -4,47 +4,14 @@
import 'dart:math' as math;
import 'clamp_overscrolls.dart';
import 'framework.dart';
import 'scroll_behavior.dart';
import 'scroll_configuration.dart';
import 'scrollable.dart';
import 'virtual_viewport.dart';
import 'package:flutter/rendering.dart';
/// If true, the ClampOverscroll's [Scrollable] descendant will clamp its
/// viewport's scrollOffsets to the [ScrollBehavior]'s min and max values.
/// In this case the Scrollable's scrollOffset will still over and undershoot
/// the ScrollBehavior's limits, but the viewport itself will not.
class ClampOverscrolls extends InheritedWidget {
ClampOverscrolls({
Key key,
this.value,
Widget child
}) : super(key: key, child: child) {
assert(value != null);
assert(child != null);
}
/// True if the [Scrollable] descendant should clamp its viewport's scrollOffset
/// values when they are less than the [ScrollBehavior]'s minimum or greater than
/// its maximum.
final bool value;
static bool of(BuildContext context) {
final ClampOverscrolls result = context.inheritFromWidgetOfExactType(ClampOverscrolls);
return result?.value ?? false;
}
@override
bool updateShouldNotify(ClampOverscrolls old) => value != old.value;
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('value: $value');
}
}
class ScrollableList extends StatelessWidget {
ScrollableList({
Key key,
......@@ -144,16 +111,12 @@ class ScrollableList extends StatelessWidget {
});
}
Widget _buildContent(BuildContext context, ScrollableState state) {
final bool clampOverscrolls = ClampOverscrolls.of(context);
final double listScrollOffset = clampOverscrolls
? state.scrollOffset.clamp(state.scrollBehavior.minScrollOffset, state.scrollBehavior.maxScrollOffset)
: state.scrollOffset;
Widget viewport = new ListViewport(
Widget _buildViewport(BuildContext context, ScrollableState state, double scrollOffset) {
return new ListViewport(
onExtentsChanged: (double contentExtent, double containerExtent) {
_handleExtentsChanged(state, contentExtent, containerExtent);
},
scrollOffset: listScrollOffset,
scrollOffset: scrollOffset,
mainAxis: scrollDirection,
anchor: scrollAnchor,
itemExtent: itemExtent,
......@@ -161,14 +124,15 @@ class ScrollableList extends StatelessWidget {
padding: padding,
children: children
);
if (clampOverscrolls)
viewport = new ClampOverscrolls(value: false, child: viewport);
return viewport;
}
Widget _buildContent(BuildContext context, ScrollableState state) {
return ClampOverscrolls.buildViewport(context, state, _buildViewport);
}
@override
Widget build(BuildContext context) {
return new Scrollable(
final Widget result = new Scrollable(
key: scrollableKey,
initialScrollOffset: initialScrollOffset,
scrollDirection: scrollDirection,
......@@ -179,6 +143,7 @@ class ScrollableList extends StatelessWidget {
snapOffsetCallback: snapOffsetCallback,
builder: _buildContent
);
return ScrollConfiguration.wrap(context, result);
}
}
......@@ -477,12 +442,12 @@ class ScrollableLazyList extends StatelessWidget {
});
}
Widget _buildContent(BuildContext context, ScrollableState state) {
Widget _buildViewport(BuildContext context, ScrollableState state, double scrollOffset) {
return new LazyListViewport(
onExtentsChanged: (double contentExtent, double containerExtent) {
_handleExtentsChanged(state, contentExtent, containerExtent);
},
scrollOffset: state.scrollOffset,
scrollOffset: scrollOffset,
mainAxis: scrollDirection,
anchor: scrollAnchor,
itemExtent: itemExtent,
......@@ -492,9 +457,13 @@ class ScrollableLazyList extends StatelessWidget {
);
}
Widget _buildContent(BuildContext context, ScrollableState state) {
return ClampOverscrolls.buildViewport(context, state, _buildViewport);
}
@override
Widget build(BuildContext context) {
return new Scrollable(
final Widget result = new Scrollable(
key: scrollableKey,
initialScrollOffset: initialScrollOffset,
scrollDirection: scrollDirection,
......@@ -505,6 +474,7 @@ class ScrollableLazyList extends StatelessWidget {
snapOffsetCallback: snapOffsetCallback,
builder: _buildContent
);
return ScrollConfiguration.wrap(context, result);
}
}
......
......@@ -15,6 +15,7 @@ export 'src/widgets/banner.dart';
export 'src/widgets/basic.dart';
export 'src/widgets/binding.dart';
export 'src/widgets/child_view.dart';
export 'src/widgets/clamp_overscrolls.dart';
export 'src/widgets/debug.dart';
export 'src/widgets/dismissable.dart';
export 'src/widgets/drag_target.dart';
......@@ -43,6 +44,7 @@ export 'src/widgets/placeholder.dart';
export 'src/widgets/raw_keyboard_listener.dart';
export 'src/widgets/routes.dart';
export 'src/widgets/scroll_behavior.dart';
export 'src/widgets/scroll_configuration.dart';
export 'src/widgets/scrollable_grid.dart';
export 'src/widgets/scrollable_list.dart';
export 'src/widgets/scrollable.dart';
......
......@@ -60,7 +60,7 @@ void main() {
Duration dt = const Duration(seconds: 2);
fling(0.8);
fling(1.0);
await tester.pump(); // Start the scheduler at 0.0
await tester.pump(dt);
expect(scrollOffset, closeTo(200.0, 1.0));
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment