Commit c288c706 authored by Adam Barth's avatar Adam Barth Committed by GitHub

Refactor scrolling code to prepare for nested scrolling (#9575)

This patch reworks some of the guts of scrolling to make it easier to
implement nested scrolling effects. The actually nested scrolling effect
will be included in a later patch.
parent 57648ba0
...@@ -411,7 +411,7 @@ class _AnimationDemoHomeState extends State<AnimationDemoHome> { ...@@ -411,7 +411,7 @@ class _AnimationDemoHomeState extends State<AnimationDemoHome> {
if (notification.depth == 0 && notification is ScrollUpdateNotification) { if (notification.depth == 0 && notification is ScrollUpdateNotification) {
selectedIndex.value = leader.page; selectedIndex.value = leader.page;
if (follower.page != leader.page) if (follower.page != leader.page)
follower.position.jumpTo(leader.position.pixels, settle: false); follower.position.jumpToWithoutSettling(leader.position.pixels); // ignore: deprecated_member_use
} }
return false; return false;
} }
......
...@@ -87,7 +87,7 @@ class ContactsDemo extends StatefulWidget { ...@@ -87,7 +87,7 @@ class ContactsDemo extends StatefulWidget {
ContactsDemoState createState() => new ContactsDemoState(); ContactsDemoState createState() => new ContactsDemoState();
} }
enum AppBarBehavior { normal, pinned, floating } enum AppBarBehavior { normal, pinned, floating, snapping }
class ContactsDemoState extends State<ContactsDemo> { class ContactsDemoState extends State<ContactsDemo> {
static final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>(); static final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
...@@ -110,7 +110,8 @@ class ContactsDemoState extends State<ContactsDemo> { ...@@ -110,7 +110,8 @@ class ContactsDemoState extends State<ContactsDemo> {
new SliverAppBar( new SliverAppBar(
expandedHeight: _appBarHeight, expandedHeight: _appBarHeight,
pinned: _appBarBehavior == AppBarBehavior.pinned, pinned: _appBarBehavior == AppBarBehavior.pinned,
floating: _appBarBehavior == AppBarBehavior.floating, floating: _appBarBehavior == AppBarBehavior.floating || _appBarBehavior == AppBarBehavior.snapping,
snap: _appBarBehavior == AppBarBehavior.snapping,
actions: <Widget>[ actions: <Widget>[
new IconButton( new IconButton(
icon: const Icon(Icons.create), icon: const Icon(Icons.create),
...@@ -140,6 +141,10 @@ class ContactsDemoState extends State<ContactsDemo> { ...@@ -140,6 +141,10 @@ class ContactsDemoState extends State<ContactsDemo> {
value: AppBarBehavior.floating, value: AppBarBehavior.floating,
child: const Text('App bar floats') child: const Text('App bar floats')
), ),
const PopupMenuItem<AppBarBehavior>(
value: AppBarBehavior.snapping,
child: const Text('App bar snaps')
),
], ],
), ),
], ],
......
...@@ -543,6 +543,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate { ...@@ -543,6 +543,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
@required this.flexibleSpace, @required this.flexibleSpace,
@required this.bottom, @required this.bottom,
@required this.elevation, @required this.elevation,
@required this.forceElevated,
@required this.backgroundColor, @required this.backgroundColor,
@required this.brightness, @required this.brightness,
@required this.iconTheme, @required this.iconTheme,
...@@ -565,6 +566,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate { ...@@ -565,6 +566,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
final Widget flexibleSpace; final Widget flexibleSpace;
final PreferredSizeWidget bottom; final PreferredSizeWidget bottom;
final int elevation; final int elevation;
final bool forceElevated;
final Color backgroundColor; final Color backgroundColor;
final Brightness brightness; final Brightness brightness;
final IconThemeData iconTheme; final IconThemeData iconTheme;
...@@ -604,7 +606,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate { ...@@ -604,7 +606,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
actions: actions, actions: actions,
flexibleSpace: flexibleSpace, flexibleSpace: flexibleSpace,
bottom: bottom, bottom: bottom,
elevation: overlapsContent || (pinned && shrinkOffset > maxExtent - minExtent) ? elevation ?? 4 : 0, elevation: forceElevated || overlapsContent || (pinned && shrinkOffset > maxExtent - minExtent) ? elevation ?? 4 : 0,
backgroundColor: backgroundColor, backgroundColor: backgroundColor,
brightness: brightness, brightness: brightness,
iconTheme: iconTheme, iconTheme: iconTheme,
...@@ -685,6 +687,7 @@ class SliverAppBar extends StatefulWidget { ...@@ -685,6 +687,7 @@ class SliverAppBar extends StatefulWidget {
this.flexibleSpace, this.flexibleSpace,
this.bottom, this.bottom,
this.elevation, this.elevation,
this.forceElevated: false,
this.backgroundColor, this.backgroundColor,
this.brightness, this.brightness,
this.iconTheme, this.iconTheme,
...@@ -695,11 +698,13 @@ class SliverAppBar extends StatefulWidget { ...@@ -695,11 +698,13 @@ class SliverAppBar extends StatefulWidget {
this.floating: false, this.floating: false,
this.pinned: false, this.pinned: false,
this.snap: false, this.snap: false,
}) : assert(primary != null), }) : assert(forceElevated != null),
assert(primary != null),
assert(floating != null), assert(floating != null),
assert(pinned != null), assert(pinned != null),
assert(pinned && floating ? bottom != null : true), assert(!pinned || !floating || bottom != null, 'A pinned and floating app bar must have a bottom widget.'),
assert(snap != null), assert(snap != null),
assert(floating || !snap, 'The "snap" argument only makes sense for floating app bars.'),
super(key: key); super(key: key);
/// A widget to display before the [title]. /// A widget to display before the [title].
...@@ -765,17 +770,30 @@ class SliverAppBar extends StatefulWidget { ...@@ -765,17 +770,30 @@ class SliverAppBar extends StatefulWidget {
/// * [PreferredSize], which can be used to give an arbitrary widget a preferred size. /// * [PreferredSize], which can be used to give an arbitrary widget a preferred size.
final PreferredSizeWidget bottom; final PreferredSizeWidget bottom;
/// The z-coordinate at which to place this app bar. /// The z-coordinate at which to place this app bar when it is above other
/// content.
/// ///
/// The following elevations have defined shadows: 1, 2, 3, 4, 6, 8, 9, 12, 16, 24 /// The following elevations have defined shadows: 1, 2, 3, 4, 6, 8, 9, 12, 16, 24
/// ///
/// Defaults to 4, the appropriate elevation for app bars. /// Defaults to 4, the appropriate elevation for app bars.
/// ///
/// The elevation is ignored when the app bar has no content underneath it. /// If [forceElevated] is false, the elevation is ignored when the app bar has
/// For example, if the app bar is [pinned] but no content is scrolled under /// no content underneath it. For example, if the app bar is [pinned] but no
/// it, or if it scrolls with the content. /// content is scrolled under it, or if it scrolls with the content, then no
/// shadow is drawn, regardless of the value of [elevation].
final int elevation; final int elevation;
/// Whether to show the shadow appropriate for the [elevation] even if the
/// content is not scrolled under the [AppBar].
///
/// Defaults to false, meaning that the [elevation] is only applied when the
/// [AppBar] is being displayed over content that is scrolled under it.
///
/// When set to true, the [elevation] is applied regardless.
///
/// Ignored when [elevation] is zero.
final bool forceElevated;
/// The color to use for the app bar's material. Typically this should be set /// The color to use for the app bar's material. Typically this should be set
/// along with [brightness], [iconTheme], [textTheme]. /// along with [brightness], [iconTheme], [textTheme].
/// ///
...@@ -829,12 +847,9 @@ class SliverAppBar extends StatefulWidget { ...@@ -829,12 +847,9 @@ class SliverAppBar extends StatefulWidget {
/// Otherwise, the user will need to scroll near the top of the scroll view to /// Otherwise, the user will need to scroll near the top of the scroll view to
/// reveal the app bar. /// reveal the app bar.
/// ///
/// See also: /// If [snap] is true then a scroll that exposes the app bar will trigger an
/// /// animation that slides the entire app bar into view. Similarly if a scroll
/// * If [snap] is true then a scroll that exposes the app bar will trigger /// dismisses the app bar, the animation will slide it completely out of view.
/// an animation that slides the entire app bar into view. Similarly if
/// a scroll dismisses the app bar, the animation will slide it completely
/// out of view.
final bool floating; final bool floating;
/// Whether the app bar should remain visible at the start of the scroll view. /// Whether the app bar should remain visible at the start of the scroll view.
...@@ -905,6 +920,7 @@ class _SliverAppBarState extends State<SliverAppBar> with TickerProviderStateMix ...@@ -905,6 +920,7 @@ class _SliverAppBarState extends State<SliverAppBar> with TickerProviderStateMix
flexibleSpace: widget.flexibleSpace, flexibleSpace: widget.flexibleSpace,
bottom: widget.bottom, bottom: widget.bottom,
elevation: widget.elevation, elevation: widget.elevation,
forceElevated: widget.forceElevated,
backgroundColor: widget.backgroundColor, backgroundColor: widget.backgroundColor,
brightness: widget.brightness, brightness: widget.brightness,
iconTheme: widget.iconTheme, iconTheme: widget.iconTheme,
......
...@@ -177,14 +177,14 @@ class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderS ...@@ -177,14 +177,14 @@ class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderS
if (notification.depth != 0) if (notification.depth != 0)
return false; return false;
if (notification is ScrollStartNotification && notification.metrics.extentBefore == 0.0 && if (notification is ScrollStartNotification && notification.metrics.extentBefore == 0.0 &&
_mode == null && _start(notification.axisDirection)) { _mode == null && _start(notification.metrics.axisDirection)) {
setState(() { setState(() {
_mode = _RefreshIndicatorMode.drag; _mode = _RefreshIndicatorMode.drag;
}); });
return false; return false;
} }
bool indicatorAtTopNow; bool indicatorAtTopNow;
switch (notification.axisDirection) { switch (notification.metrics.axisDirection) {
case AxisDirection.down: case AxisDirection.down:
indicatorAtTopNow = true; indicatorAtTopNow = true;
break; break;
......
...@@ -55,7 +55,7 @@ class _ScrollbarState extends State<Scrollbar> with TickerProviderStateMixin { ...@@ -55,7 +55,7 @@ class _ScrollbarState extends State<Scrollbar> with TickerProviderStateMixin {
bool _handleScrollNotification(ScrollNotification notification) { bool _handleScrollNotification(ScrollNotification notification) {
if (notification is ScrollUpdateNotification || if (notification is ScrollUpdateNotification ||
notification is OverscrollNotification) notification is OverscrollNotification)
_controller.update(notification.metrics, notification.axisDirection); _controller.update(notification.metrics, notification.metrics.axisDirection);
return false; return false;
} }
......
...@@ -338,15 +338,15 @@ class _DragAnimation extends Animation<double> with AnimationWithParentMixin<dou ...@@ -338,15 +338,15 @@ class _DragAnimation extends Animation<double> with AnimationWithParentMixin<dou
// where a scrollable TabBar has a non-zero initialIndex. In that case we can // where a scrollable TabBar has a non-zero initialIndex. In that case we can
// only compute the scroll position's initial scroll offset (the "correct" // only compute the scroll position's initial scroll offset (the "correct"
// pixels value) after the TabBar viewport width and scroll limits are known. // pixels value) after the TabBar viewport width and scroll limits are known.
class _TabBarScrollPosition extends ScrollPosition { class _TabBarScrollPosition extends ScrollPositionWithSingleContext {
_TabBarScrollPosition({ _TabBarScrollPosition({
ScrollPhysics physics, ScrollPhysics physics,
AbstractScrollState state, ScrollContext context,
ScrollPosition oldPosition, ScrollPosition oldPosition,
this.tabBar, this.tabBar,
}) : super( }) : super(
physics: physics, physics: physics,
state: state, context: context,
initialPixels: null, initialPixels: null,
oldPosition: oldPosition, oldPosition: oldPosition,
); );
...@@ -372,10 +372,10 @@ class _TabBarScrollController extends ScrollController { ...@@ -372,10 +372,10 @@ class _TabBarScrollController extends ScrollController {
final _TabBarState tabBar; final _TabBarState tabBar;
@override @override
ScrollPosition createScrollPosition(ScrollPhysics physics, AbstractScrollState state, ScrollPosition oldPosition) { ScrollPosition createScrollPosition(ScrollPhysics physics, ScrollContext context, ScrollPosition oldPosition) {
return new _TabBarScrollPosition( return new _TabBarScrollPosition(
physics: physics, physics: physics,
state: state, context: context,
oldPosition: oldPosition, oldPosition: oldPosition,
tabBar: tabBar, tabBar: tabBar,
); );
......
...@@ -109,6 +109,24 @@ AxisDirection flipAxisDirection(AxisDirection axisDirection) { ...@@ -109,6 +109,24 @@ AxisDirection flipAxisDirection(AxisDirection axisDirection) {
return null; return null;
} }
/// Returns whether travelling along the given axis direction visits coordinates
/// along that axis in numerically decreasing order.
///
/// Specifically, returns true for [AxisDirection.up] and [AxisDirection.left]
/// and false for [AxisDirection.down] for [AxisDirection.right].
bool axisDirectionIsReversed(AxisDirection axisDirection) {
assert(axisDirection != null);
switch (axisDirection) {
case AxisDirection.up:
case AxisDirection.left:
return true;
case AxisDirection.down:
case AxisDirection.right:
return false;
}
return null;
}
/// Flips the [AxisDirection] if the [GrowthDirection] is [GrowthDirection.reverse]. /// Flips the [AxisDirection] if the [GrowthDirection] is [GrowthDirection.reverse].
/// ///
/// Specifically, returns `axisDirection` if `growthDirection` is /// Specifically, returns `axisDirection` if `growthDirection` is
......
...@@ -164,7 +164,7 @@ abstract class ViewportOffset extends ChangeNotifier { ...@@ -164,7 +164,7 @@ abstract class ViewportOffset extends ChangeNotifier {
String toString() { String toString() {
final List<String> description = <String>[]; final List<String> description = <String>[];
debugFillDescription(description); debugFillDescription(description);
return '$runtimeType(${description.join(", ")})'; return '$runtimeType#$hashCode(${description.join(", ")})';
} }
/// Add additional information to the given description for use by [toString]. /// Add additional information to the given description for use by [toString].
......
...@@ -1129,7 +1129,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -1129,7 +1129,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
onPointerUp: _handlePointerUpOrCancel, onPointerUp: _handlePointerUpOrCancel,
onPointerCancel: _handlePointerUpOrCancel, onPointerCancel: _handlePointerUpOrCancel,
child: new AbsorbPointer( child: new AbsorbPointer(
absorbing: false, absorbing: false, // it's mutated directly by _cancelActivePointers above
child: new FocusScope( child: new FocusScope(
node: focusScopeNode, node: focusScopeNode,
autofocus: true, autofocus: true,
......
...@@ -152,7 +152,7 @@ class _GlowingOverscrollIndicatorState extends State<GlowingOverscrollIndicator> ...@@ -152,7 +152,7 @@ class _GlowingOverscrollIndicatorState extends State<GlowingOverscrollIndicator>
_accepted[isLeading] = confirmationNotification._accepted; _accepted[isLeading] = confirmationNotification._accepted;
} }
assert(controller != null); assert(controller != null);
assert(notification.axis == widget.axis); assert(notification.metrics.axis == widget.axis);
if (_accepted[isLeading]) { if (_accepted[isLeading]) {
if (notification.velocity != 0.0) { if (notification.velocity != 0.0) {
assert(notification.dragDetails == null); assert(notification.dragDetails == null);
...@@ -166,7 +166,7 @@ class _GlowingOverscrollIndicatorState extends State<GlowingOverscrollIndicator> ...@@ -166,7 +166,7 @@ class _GlowingOverscrollIndicatorState extends State<GlowingOverscrollIndicator>
assert(renderer.hasSize); assert(renderer.hasSize);
final Size size = renderer.size; final Size size = renderer.size;
final Offset position = renderer.globalToLocal(notification.dragDetails.globalPosition); final Offset position = renderer.globalToLocal(notification.dragDetails.globalPosition);
switch (notification.axis) { switch (notification.metrics.axis) {
case Axis.horizontal: case Axis.horizontal:
controller.pull(notification.overscroll.abs(), size.width, position.dy.clamp(0.0, size.height), size.height); controller.pull(notification.overscroll.abs(), size.width, position.dy.clamp(0.0, size.height), size.height);
break; break;
......
...@@ -12,10 +12,13 @@ import 'package:flutter/rendering.dart'; ...@@ -12,10 +12,13 @@ import 'package:flutter/rendering.dart';
import 'basic.dart'; import 'basic.dart';
import 'framework.dart'; import 'framework.dart';
import 'notification_listener.dart'; import 'notification_listener.dart';
import 'scroll_context.dart';
import 'scroll_controller.dart'; import 'scroll_controller.dart';
import 'scroll_metrics.dart';
import 'scroll_notification.dart'; import 'scroll_notification.dart';
import 'scroll_physics.dart'; import 'scroll_physics.dart';
import 'scroll_position.dart'; import 'scroll_position.dart';
import 'scroll_position_with_single_context.dart';
import 'scroll_view.dart'; import 'scroll_view.dart';
import 'scrollable.dart'; import 'scrollable.dart';
import 'sliver.dart'; import 'sliver.dart';
...@@ -70,7 +73,11 @@ class PageController extends ScrollController { ...@@ -70,7 +73,11 @@ class PageController extends ScrollController {
@required Curve curve, @required Curve curve,
}) { }) {
final _PagePosition position = this.position; final _PagePosition position = this.position;
return position.animateTo(position.getPixelsFromPage(page.toDouble()), duration: duration, curve: curve); return position.animateTo(
position.getPixelsFromPage(page.toDouble()),
duration: duration,
curve: curve,
);
} }
/// Changes which page is displayed in the controlled [PageView]. /// Changes which page is displayed in the controlled [PageView].
...@@ -105,10 +112,10 @@ class PageController extends ScrollController { ...@@ -105,10 +112,10 @@ class PageController extends ScrollController {
} }
@override @override
ScrollPosition createScrollPosition(ScrollPhysics physics, AbstractScrollState state, ScrollPosition oldPosition) { ScrollPosition createScrollPosition(ScrollPhysics physics, ScrollContext context, ScrollPosition oldPosition) {
return new _PagePosition( return new _PagePosition(
physics: physics, physics: physics,
state: state, context: context,
initialPage: initialPage, initialPage: initialPage,
viewportFraction: viewportFraction, viewportFraction: viewportFraction,
oldPosition: oldPosition, oldPosition: oldPosition,
...@@ -127,7 +134,7 @@ class PageController extends ScrollController { ...@@ -127,7 +134,7 @@ class PageController extends ScrollController {
/// ///
/// The metrics are available on [ScrollNotification]s generated from /// The metrics are available on [ScrollNotification]s generated from
/// [PageView]s. /// [PageView]s.
class PageMetrics extends ScrollMetrics { class PageMetrics extends FixedScrollMetrics {
/// Creates page metrics that add the given information to the `parent` /// Creates page metrics that add the given information to the `parent`
/// metrics. /// metrics.
PageMetrics({ PageMetrics({
...@@ -139,16 +146,16 @@ class PageMetrics extends ScrollMetrics { ...@@ -139,16 +146,16 @@ class PageMetrics extends ScrollMetrics {
final double page; final double page;
} }
class _PagePosition extends ScrollPosition { class _PagePosition extends ScrollPositionWithSingleContext {
_PagePosition({ _PagePosition({
ScrollPhysics physics, ScrollPhysics physics,
AbstractScrollState state, ScrollContext context,
this.initialPage: 0, this.initialPage: 0,
double viewportFraction: 1.0, double viewportFraction: 1.0,
ScrollPosition oldPosition, ScrollPosition oldPosition,
}) : _viewportFraction = viewportFraction, super( }) : _viewportFraction = viewportFraction, super(
physics: physics, physics: physics,
state: state, context: context,
initialPixels: null, initialPixels: null,
oldPosition: oldPosition, oldPosition: oldPosition,
) { ) {
...@@ -167,7 +174,7 @@ class _PagePosition extends ScrollPosition { ...@@ -167,7 +174,7 @@ class _PagePosition extends ScrollPosition {
final double oldPage = page; final double oldPage = page;
_viewportFraction = value; _viewportFraction = value;
if (oldPage != null) if (oldPage != null)
correctPixels(getPixelsFromPage(oldPage)); forcePixels(getPixelsFromPage(oldPage));
} }
double getPageFromPixels(double pixels, double viewportDimension) { double getPageFromPixels(double pixels, double viewportDimension) {
...@@ -195,9 +202,9 @@ class _PagePosition extends ScrollPosition { ...@@ -195,9 +202,9 @@ class _PagePosition extends ScrollPosition {
} }
@override @override
PageMetrics getMetrics() { PageMetrics cloneMetrics() {
return new PageMetrics( return new PageMetrics(
parent: super.getMetrics(), parent: this,
page: page, page: page,
); );
} }
...@@ -235,7 +242,7 @@ class PageScrollPhysics extends ScrollPhysics { ...@@ -235,7 +242,7 @@ class PageScrollPhysics extends ScrollPhysics {
} }
@override @override
Simulation createBallisticSimulation(ScrollPosition position, double velocity) { Simulation createBallisticSimulation(ScrollMetrics position, double velocity) {
// If we're out of range and not headed back in range, defer to the parent // If we're out of range and not headed back in range, defer to the parent
// ballistics, which should put us back in range at a page boundary. // ballistics, which should put us back in range at a page boundary.
if ((velocity <= 0.0 && position.pixels <= position.minScrollExtent) || if ((velocity <= 0.0 && position.pixels <= position.minScrollExtent) ||
...@@ -243,7 +250,9 @@ class PageScrollPhysics extends ScrollPhysics { ...@@ -243,7 +250,9 @@ class PageScrollPhysics extends ScrollPhysics {
return super.createBallisticSimulation(position, velocity); return super.createBallisticSimulation(position, velocity);
final Tolerance tolerance = this.tolerance; final Tolerance tolerance = this.tolerance;
final double target = _getTargetPixels(position, tolerance, velocity); final double target = _getTargetPixels(position, tolerance, velocity);
return new ScrollSpringSimulation(spring, position.pixels, target, velocity, tolerance: tolerance); if (target != position.pixels)
return new ScrollSpringSimulation(spring, position.pixels, target, velocity, tolerance: tolerance);
return null;
} }
} }
...@@ -421,10 +430,10 @@ class _PageViewState extends State<PageView> { ...@@ -421,10 +430,10 @@ class _PageViewState extends State<PageView> {
axisDirection: axisDirection, axisDirection: axisDirection,
controller: widget.controller, controller: widget.controller,
physics: widget.physics == null ? _kPagePhysics : _kPagePhysics.applyTo(widget.physics), physics: widget.physics == null ? _kPagePhysics : _kPagePhysics.applyTo(widget.physics),
viewportBuilder: (BuildContext context, ViewportOffset offset) { viewportBuilder: (BuildContext context, ViewportOffset position) {
return new Viewport( return new Viewport(
axisDirection: axisDirection, axisDirection: axisDirection,
offset: offset, offset: position,
slivers: <Widget>[ slivers: <Widget>[
new SliverFillViewport( new SliverFillViewport(
viewportFraction: widget.controller.viewportFraction, viewportFraction: widget.controller.viewportFraction,
......
...@@ -34,6 +34,6 @@ class PrimaryScrollController extends InheritedWidget { ...@@ -34,6 +34,6 @@ class PrimaryScrollController extends InheritedWidget {
@override @override
void debugFillDescription(List<String> description) { void debugFillDescription(List<String> description) {
super.debugFillDescription(description); super.debugFillDescription(description);
description.add('${controller ?? 'no controller'}'); description.add('${controller ?? "no controller"}');
} }
} }
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/physics.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'basic.dart';
import 'framework.dart';
import 'gesture_detector.dart';
import 'scroll_metrics.dart';
import 'scroll_notification.dart';
import 'ticker_provider.dart';
abstract class ScrollActivityDelegate {
AxisDirection get axisDirection;
double get pixels;
double setPixels(double pixels);
double applyUserOffset(double delta);
void goIdle();
void goBallistic(double velocity);
}
/// Base class for scrolling activities like dragging and flinging.
///
/// See also:
///
/// * [ScrollPositionWithSingleContext], which uses [ScrollActivity] objects to
/// manage the [ScrollPosition] of a [Scrollable].
abstract class ScrollActivity {
ScrollActivity(this._delegate);
ScrollActivityDelegate get delegate => _delegate;
ScrollActivityDelegate _delegate;
/// Updates the activity's link to the [ScrollActivityDelegate].
///
/// This should only be called when an activity is being moved from a defunct
/// (or about-to-be defunct) [ScrollActivityDelegate] object to a new one.
void updateDelegate(ScrollActivityDelegate value) {
assert(_delegate != value);
_delegate = value;
}
/// Called by the [ScrollActivityDelegate] when it has changed type (for
/// example, when changing from an Android-style scroll position to an
/// iOS-style scroll position). If this activity can differ between the two
/// modes, then it should tell the position to restart that activity
/// appropriately.
///
/// For example, [BallisticScrollActivity]'s implementation calls
/// [ScrollActivityDelegate.goBallistic].
void resetActivity() { }
void dispatchScrollStartNotification(ScrollMetrics metrics, BuildContext context) {
new ScrollStartNotification(metrics: metrics, context: context).dispatch(context);
}
void dispatchScrollUpdateNotification(ScrollMetrics metrics, BuildContext context, double scrollDelta) {
new ScrollUpdateNotification(metrics: metrics, context: context, scrollDelta: scrollDelta).dispatch(context);
}
void dispatchOverscrollNotification(ScrollMetrics metrics, BuildContext context, double overscroll) {
new OverscrollNotification(metrics: metrics, context: context, overscroll: overscroll).dispatch(context);
}
void dispatchScrollEndNotification(ScrollMetrics metrics, BuildContext context) {
new ScrollEndNotification(metrics: metrics, context: context).dispatch(context);
}
void didTouch() { }
void applyNewDimensions() { }
bool get shouldIgnorePointer;
bool get isScrolling;
@mustCallSuper
void dispose() {
_delegate = null;
}
@override
String toString() => '$runtimeType';
}
class IdleScrollActivity extends ScrollActivity {
IdleScrollActivity(ScrollActivityDelegate delegate) : super(delegate);
@override
void applyNewDimensions() {
delegate.goBallistic(0.0);
}
@override
bool get shouldIgnorePointer => false;
@override
bool get isScrolling => false;
}
class DragScrollActivity extends ScrollActivity implements Drag {
DragScrollActivity(
ScrollActivityDelegate delegate,
DragStartDetails details,
this.onDragCanceled,
) : _lastDetails = details, super(delegate);
final VoidCallback onDragCanceled;
@override
void didTouch() {
assert(false);
}
bool get _reversed => axisDirectionIsReversed(delegate.axisDirection);
@override
void update(DragUpdateDetails details) {
assert(details.primaryDelta != null);
_lastDetails = details;
double offset = details.primaryDelta;
if (offset == 0.0)
return;
if (_reversed) // e.g. an AxisDirection.up scrollable
offset = -offset;
delegate.applyUserOffset(offset);
// We ignore any reported overscroll returned by setPixels,
// because it gets reported via the reportOverscroll path.
}
@override
void end(DragEndDetails details) {
assert(details.primaryVelocity != null);
double velocity = details.primaryVelocity;
if (_reversed) // e.g. an AxisDirection.up scrollable
velocity = -velocity;
_lastDetails = details;
// We negate the velocity here because if the touch is moving downwards,
// the scroll has to move upwards. It's the same reason that update()
// above negates the delta before applying it to the scroll offset.
delegate.goBallistic(-velocity);
}
@override
void cancel() {
delegate.goBallistic(0.0);
}
@override
void dispose() {
_lastDetails = null;
if (onDragCanceled != null)
onDragCanceled();
super.dispose();
}
dynamic _lastDetails;
@override
void dispatchScrollStartNotification(ScrollMetrics metrics, BuildContext context) {
assert(_lastDetails is DragStartDetails);
new ScrollStartNotification(metrics: metrics, context: context, dragDetails: _lastDetails).dispatch(context);
}
@override
void dispatchScrollUpdateNotification(ScrollMetrics metrics, BuildContext context, double scrollDelta) {
assert(_lastDetails is DragUpdateDetails);
new ScrollUpdateNotification(metrics: metrics, context: context, scrollDelta: scrollDelta, dragDetails: _lastDetails).dispatch(context);
}
@override
void dispatchOverscrollNotification(ScrollMetrics metrics, BuildContext context, double overscroll) {
assert(_lastDetails is DragUpdateDetails);
new OverscrollNotification(metrics: metrics, context: context, overscroll: overscroll, dragDetails: _lastDetails).dispatch(context);
}
@override
void dispatchScrollEndNotification(ScrollMetrics metrics, BuildContext context) {
// We might not have DragEndDetails yet if we're being called from beginActivity.
new ScrollEndNotification(
metrics: metrics,
context: context,
dragDetails: _lastDetails is DragEndDetails ? _lastDetails : null
).dispatch(context);
}
@override
bool get shouldIgnorePointer => true;
@override
bool get isScrolling => true;
}
class BallisticScrollActivity extends ScrollActivity {
// ///
// /// The velocity should be in logical pixels per second.
BallisticScrollActivity(
ScrollActivityDelegate delegate,
Simulation simulation,
TickerProvider vsync,
) : super(delegate) {
_controller = new AnimationController.unbounded(
debugLabel: '$runtimeType',
vsync: vsync,
)
..addListener(_tick)
..animateWith(simulation)
.whenComplete(_end); // won't trigger if we dispose _controller first
}
double get velocity => _controller.velocity;
AnimationController _controller;
@override
void resetActivity() {
delegate.goBallistic(velocity);
}
@override
void didTouch() {
delegate.goIdle();
}
@override
void applyNewDimensions() {
delegate.goBallistic(velocity);
}
void _tick() {
if (!applyMoveTo(_controller.value))
delegate.goIdle();
}
/// Move the position to the given location.
///
/// If the new position was fully applied, return true.
/// If there was any overflow, return false.
///
/// The default implementation calls [ScrollActivityDelegate.setPixels]
/// and returns true if the overflow was zero.
@protected
bool applyMoveTo(double value) {
return delegate.setPixels(value) == 0.0;
}
void _end() {
delegate?.goBallistic(0.0);
}
@override
void dispatchOverscrollNotification(ScrollMetrics metrics, BuildContext context, double overscroll) {
new OverscrollNotification(metrics: metrics, context: context, overscroll: overscroll, velocity: velocity).dispatch(context);
}
@override
bool get shouldIgnorePointer => true;
@override
bool get isScrolling => true;
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
String toString() {
return '$runtimeType($_controller)';
}
}
class DrivenScrollActivity extends ScrollActivity {
DrivenScrollActivity(
ScrollActivityDelegate delegate, {
@required double from,
@required double to,
@required Duration duration,
@required Curve curve,
@required TickerProvider vsync,
}) : super(delegate) {
assert(from != null);
assert(to != null);
assert(duration != null);
assert(duration > Duration.ZERO);
assert(curve != null);
_completer = new Completer<Null>();
_controller = new AnimationController.unbounded(
value: from,
debugLabel: '$runtimeType',
vsync: vsync,
)
..addListener(_tick)
..animateTo(to, duration: duration, curve: curve)
.whenComplete(_end); // won't trigger if we dispose _controller first
}
Completer<Null> _completer;
AnimationController _controller;
Future<Null> get done => _completer.future;
double get velocity => _controller.velocity;
@override
void didTouch() {
delegate.goIdle();
}
void _tick() {
if (delegate.setPixels(_controller.value) != 0.0)
delegate.goIdle();
}
void _end() {
delegate?.goBallistic(velocity);
}
@override
void dispatchOverscrollNotification(ScrollMetrics metrics, BuildContext context, double overscroll) {
new OverscrollNotification(metrics: metrics, context: context, overscroll: overscroll, velocity: velocity).dispatch(context);
}
@override
bool get shouldIgnorePointer => true;
@override
bool get isScrolling => true;
@override
void dispose() {
_completer.complete();
_controller.dispose();
super.dispose();
}
@override
String toString() {
return '$runtimeType($_controller)';
}
}
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/scheduler.dart';
import 'package:flutter/rendering.dart';
import 'framework.dart';
import 'ticker_provider.dart';
abstract class ScrollContext {
BuildContext get notificationContext;
TickerProvider get vsync;
AxisDirection get axisDirection;
void setIgnorePointer(bool value);
void setCanDrag(bool value);
}
...@@ -7,7 +7,10 @@ import 'dart:async'; ...@@ -7,7 +7,10 @@ import 'dart:async';
import 'package:flutter/animation.dart'; import 'package:flutter/animation.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'scroll_context.dart';
import 'scroll_physics.dart';
import 'scroll_position.dart'; import 'scroll_position.dart';
import 'scroll_position_with_single_context.dart';
class ScrollController extends ChangeNotifier { class ScrollController extends ChangeNotifier {
ScrollController({ ScrollController({
...@@ -22,6 +25,12 @@ class ScrollController extends ChangeNotifier { ...@@ -22,6 +25,12 @@ class ScrollController extends ChangeNotifier {
/// controller will have their offset initialized to this value. /// controller will have their offset initialized to this value.
final double initialScrollOffset; final double initialScrollOffset;
/// The currently attached positions.
///
/// This should not be mutated directly. [ScrollPosition] objects can be added
/// and removed using [attach] and [detach].
@protected
Iterable<ScrollPosition> get positions => _positions;
final List<ScrollPosition> _positions = <ScrollPosition>[]; final List<ScrollPosition> _positions = <ScrollPosition>[];
/// Whether any [ScrollPosition] objects have attached themselves to the /// Whether any [ScrollPosition] objects have attached themselves to the
...@@ -32,6 +41,10 @@ class ScrollController extends ChangeNotifier { ...@@ -32,6 +41,10 @@ class ScrollController extends ChangeNotifier {
/// called. /// called.
bool get hasClients => _positions.isNotEmpty; bool get hasClients => _positions.isNotEmpty;
/// Returns the attached [ScrollPosition], from which the actual scroll offset
/// of the [ScrollView] can be obtained.
///
/// Calling this is only valid when only a single position is attached.
ScrollPosition get position { ScrollPosition get position {
assert(_positions.isNotEmpty, 'ScrollController not attached to any scroll views.'); assert(_positions.isNotEmpty, 'ScrollController not attached to any scroll views.');
assert(_positions.length == 1, 'ScrollController attached to multiple scroll views.'); assert(_positions.length == 1, 'ScrollController attached to multiple scroll views.');
...@@ -71,7 +84,7 @@ class ScrollController extends ChangeNotifier { ...@@ -71,7 +84,7 @@ class ScrollController extends ChangeNotifier {
}) { }) {
assert(_positions.isNotEmpty, 'ScrollController not attached to any scroll views.'); assert(_positions.isNotEmpty, 'ScrollController not attached to any scroll views.');
final List<Future<Null>> animations = new List<Future<Null>>(_positions.length); final List<Future<Null>> animations = new List<Future<Null>>(_positions.length);
for (int i = 0; i < _positions.length; i++) for (int i = 0; i < _positions.length; i += 1)
animations[i] = _positions[i].animateTo(offset, duration: duration, curve: curve); animations[i] = _positions[i].animateTo(offset, duration: duration, curve: curve);
return Future.wait<Null>(animations).then((List<Null> _) => null); return Future.wait<Null>(animations).then((List<Null> _) => null);
} }
...@@ -121,18 +134,22 @@ class ScrollController extends ChangeNotifier { ...@@ -121,18 +134,22 @@ class ScrollController extends ChangeNotifier {
super.dispose(); super.dispose();
} }
static ScrollPosition createDefaultScrollPosition(ScrollPhysics physics, AbstractScrollState state, ScrollPosition oldPosition) { static ScrollPosition createDefaultScrollPosition(ScrollPhysics physics, ScrollContext context, ScrollPosition oldPosition) {
return new ScrollPosition( return new ScrollPositionWithSingleContext(
physics: physics, physics: physics,
state: state, context: context,
oldPosition: oldPosition, oldPosition: oldPosition,
); );
} }
ScrollPosition createScrollPosition(ScrollPhysics physics, AbstractScrollState state, ScrollPosition oldPosition) { ScrollPosition createScrollPosition(
return new ScrollPosition( ScrollPhysics physics,
ScrollContext context,
ScrollPosition oldPosition,
) {
return new ScrollPositionWithSingleContext(
physics: physics, physics: physics,
state: state, context: context,
initialPixels: initialScrollOffset, initialPixels: initialScrollOffset,
oldPosition: oldPosition, oldPosition: oldPosition,
); );
...@@ -140,18 +157,22 @@ class ScrollController extends ChangeNotifier { ...@@ -140,18 +157,22 @@ class ScrollController extends ChangeNotifier {
@override @override
String toString() { String toString() {
final StringBuffer result = new StringBuffer(); final List<String> description = <String>[];
result.write('$runtimeType#$hashCode('); debugFillDescription(description);
return '$runtimeType#$hashCode(${description.join(", ")})';
}
@mustCallSuper
void debugFillDescription(List<String> description) {
if (initialScrollOffset != 0.0) if (initialScrollOffset != 0.0)
result.write('initialScrollOffset: ${initialScrollOffset.toStringAsFixed(1)}, '); description.add('initialScrollOffset: ${initialScrollOffset.toStringAsFixed(1)}, ');
if (_positions.isEmpty) { if (_positions.isEmpty) {
result.write('no clients'); description.add('no clients');
} else if (_positions.length == 1) { } else if (_positions.length == 1) {
result.write('one client, offset $offset'); // Don't actually list the client itself, since its toString may refer to us.
description.add('one client, offset ${offset.toStringAsFixed(1)}');
} else { } else {
result.write('${_positions.length} clients'); description.add('${_positions.length} clients');
} }
result.write(')');
return result.toString();
} }
} }
// 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 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
/// A description of a [Scrollable]'s contents, useful for modelling the state
/// of its viewport.
///
/// This class defines a current position, [pixels], and a range of values
/// considered "in bounds" for that position. The range has a minimum value at
/// [minScrollExtent] and a maximum value at [maxScrollExtent] (inclusive). The
/// viewport scrolls in the direction and axis described by [axisDirection]
/// and [axis].
///
/// The [outOfRange] getter will return true if [pixels] is outside this defined
/// range. The [atEdge] getter will return true if the [pixels] position equals
/// either the [minScrollExtent] or the [maxScrollExtent].
///
/// The dimensions of the viewport in the given [axis] are described by
/// [viewportDimension].
///
/// The above values are also exposed in terms of [extentBefore],
/// [extentInside], and [extentAfter], which may be more useful for use cases
/// such as scroll bars; for example, see [Scrollbar].
abstract class ScrollMetrics {
/// Creates a [ScrollMetrics] that has the same properties as this object.
///
/// This is useful if this object is mutable, but you want to get a snapshot
/// of the current state.
ScrollMetrics cloneMetrics() => new FixedScrollMetrics.clone(this);
double get minScrollExtent;
double get maxScrollExtent;
double get pixels;
double get viewportDimension;
AxisDirection get axisDirection;
Axis get axis => axisDirectionToAxis(axisDirection);
bool get outOfRange => pixels < minScrollExtent || pixels > maxScrollExtent;
bool get atEdge => pixels == minScrollExtent || pixels == maxScrollExtent;
/// The quantity of content conceptually "above" the currently visible content
/// of the viewport in the scrollable. This is the content above the content
/// described by [extentInside].
double get extentBefore => math.max(pixels - minScrollExtent, 0.0);
/// The quantity of visible content.
///
/// If [extentBefore] and [extentAfter] are non-zero, then this is typically
/// the height of the viewport. It could be less if there is less content
/// visible than the size of the viewport.
double get extentInside {
return math.min(pixels, maxScrollExtent) -
math.max(pixels, minScrollExtent) +
math.min(viewportDimension, maxScrollExtent - minScrollExtent);
}
/// The quantity of content conceptually "below" the currently visible content
/// of the viewport in the scrollable. This is the content below the content
/// described by [extentInside].
double get extentAfter => math.max(maxScrollExtent - pixels, 0.0);
@override
String toString() {
return '$runtimeType(${extentBefore.toStringAsFixed(1)}..[${extentInside.toStringAsFixed(1)}]..${extentAfter.toStringAsFixed(1)}})';
}
}
@immutable
class FixedScrollMetrics extends ScrollMetrics {
FixedScrollMetrics({
@required this.minScrollExtent,
@required this.maxScrollExtent,
@required this.pixels,
@required this.viewportDimension,
@required this.axisDirection,
});
FixedScrollMetrics.clone(ScrollMetrics parent) :
minScrollExtent = parent.minScrollExtent,
maxScrollExtent = parent.maxScrollExtent,
pixels = parent.pixels,
viewportDimension = parent.viewportDimension,
axisDirection = parent.axisDirection;
@override
final double minScrollExtent;
@override
final double maxScrollExtent;
@override
final double pixels;
@override
final double viewportDimension;
@override
final AxisDirection axisDirection;
}
\ No newline at end of file
...@@ -6,62 +6,9 @@ import 'package:flutter/foundation.dart'; ...@@ -6,62 +6,9 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'basic.dart';
import 'framework.dart'; import 'framework.dart';
import 'notification_listener.dart'; import 'notification_listener.dart';
import 'scrollable.dart' show Scrollable, ScrollableState; import 'scroll_metrics.dart';
/// A description of a [Scrollable]'s contents, useful for modelling the state
/// of the viewport, for example by a [Scrollbar].
///
/// The units used by the [extentBefore], [extentInside], and [extentAfter] are
/// not defined, but must be consistent. For example, they could be in pixels,
/// or in percentages, or in units of the [extentInside] (in the latter case,
/// [extentInside] would always be 1.0).
@immutable
class ScrollMetrics {
/// Create a description of the metrics of a [Scrollable]'s contents.
///
/// The three arguments must be present, non-null, finite, and non-negative.
const ScrollMetrics({
@required this.extentBefore,
@required this.extentInside,
@required this.extentAfter,
@required this.viewportDimension,
});
/// Creates a [ScrollMetrics] that has the same properties as the given
/// [ScrollMetrics].
ScrollMetrics.clone(ScrollMetrics other)
: extentBefore = other.extentBefore,
extentInside = other.extentInside,
extentAfter = other.extentAfter,
viewportDimension = other.viewportDimension;
/// The quantity of content conceptually "above" the currently visible content
/// of the viewport in the scrollable. This is the content above the content
/// described by [extentInside].
final double extentBefore;
/// The quantity of visible content.
///
/// If [extentBefore] and [extentAfter] are non-zero, then this is typically
/// the height of the viewport. It could be less if there is less content
/// visible than the size of the viewport.
final double extentInside;
/// The quantity of content conceptually "below" the currently visible content
/// of the viewport in the scrollable. This is the content below the content
/// described by [extentInside].
final double extentAfter;
final double viewportDimension;
@override
String toString() {
return '$runtimeType(${extentBefore.toStringAsFixed(1)}..[${extentInside.toStringAsFixed(1)}]..${extentAfter.toStringAsFixed(1)}})';
}
}
/// Mixin for [Notification]s that track how many [RenderAbstractViewport] they /// Mixin for [Notification]s that track how many [RenderAbstractViewport] they
/// have bubbled through. /// have bubbled through.
...@@ -95,19 +42,13 @@ abstract class ViewportNotificationMixin extends Notification { ...@@ -95,19 +42,13 @@ abstract class ViewportNotificationMixin extends Notification {
abstract class ScrollNotification extends LayoutChangedNotification with ViewportNotificationMixin { abstract class ScrollNotification extends LayoutChangedNotification with ViewportNotificationMixin {
/// Creates a notification about scrolling. /// Creates a notification about scrolling.
ScrollNotification({ ScrollNotification({
@required ScrollableState scrollable, @required this.metrics,
}) : axisDirection = scrollable.widget.axisDirection, @required this.context,
metrics = scrollable.position.getMetrics(), });
context = scrollable.context;
/// The direction that positive scroll offsets indicate.
final AxisDirection axisDirection;
Axis get axis => axisDirectionToAxis(axisDirection);
final ScrollMetrics metrics; final ScrollMetrics metrics;
/// The build context of the [Scrollable] that fired this notification. /// The build context of the widget that fired this notification.
/// ///
/// This can be used to find the scrollable's render objects to determine the /// This can be used to find the scrollable's render objects to determine the
/// size of the viewport, for instance. /// size of the viewport, for instance.
...@@ -116,16 +57,16 @@ abstract class ScrollNotification extends LayoutChangedNotification with Viewpor ...@@ -116,16 +57,16 @@ abstract class ScrollNotification extends LayoutChangedNotification with Viewpor
@override @override
void debugFillDescription(List<String> description) { void debugFillDescription(List<String> description) {
super.debugFillDescription(description); super.debugFillDescription(description);
description.add('$axisDirection'); description.add('$metrics');
description.add('metrics: $metrics');
} }
} }
class ScrollStartNotification extends ScrollNotification { class ScrollStartNotification extends ScrollNotification {
ScrollStartNotification({ ScrollStartNotification({
@required ScrollableState scrollable, @required ScrollMetrics metrics,
@required BuildContext context,
this.dragDetails, this.dragDetails,
}) : super(scrollable: scrollable); }) : super(metrics: metrics, context: context);
final DragStartDetails dragDetails; final DragStartDetails dragDetails;
...@@ -139,10 +80,11 @@ class ScrollStartNotification extends ScrollNotification { ...@@ -139,10 +80,11 @@ class ScrollStartNotification extends ScrollNotification {
class ScrollUpdateNotification extends ScrollNotification { class ScrollUpdateNotification extends ScrollNotification {
ScrollUpdateNotification({ ScrollUpdateNotification({
@required ScrollableState scrollable, @required ScrollMetrics metrics,
@required BuildContext context,
this.dragDetails, this.dragDetails,
this.scrollDelta, this.scrollDelta,
}) : super(scrollable: scrollable); }) : super(metrics: metrics, context: context);
final DragUpdateDetails dragDetails; final DragUpdateDetails dragDetails;
...@@ -160,11 +102,12 @@ class ScrollUpdateNotification extends ScrollNotification { ...@@ -160,11 +102,12 @@ class ScrollUpdateNotification extends ScrollNotification {
class OverscrollNotification extends ScrollNotification { class OverscrollNotification extends ScrollNotification {
OverscrollNotification({ OverscrollNotification({
@required ScrollableState scrollable, @required ScrollMetrics metrics,
@required BuildContext context,
this.dragDetails, this.dragDetails,
@required this.overscroll, @required this.overscroll,
this.velocity: 0.0, this.velocity: 0.0,
}) : super(scrollable: scrollable) { }) : super(metrics: metrics, context: context) {
assert(overscroll != null); assert(overscroll != null);
assert(overscroll.isFinite); assert(overscroll.isFinite);
assert(overscroll != 0.0); assert(overscroll != 0.0);
...@@ -199,9 +142,10 @@ class OverscrollNotification extends ScrollNotification { ...@@ -199,9 +142,10 @@ class OverscrollNotification extends ScrollNotification {
class ScrollEndNotification extends ScrollNotification { class ScrollEndNotification extends ScrollNotification {
ScrollEndNotification({ ScrollEndNotification({
@required ScrollableState scrollable, @required ScrollMetrics metrics,
@required BuildContext context,
this.dragDetails, this.dragDetails,
}) : super(scrollable: scrollable); }) : super(metrics: metrics, context: context);
final DragEndDetails dragDetails; final DragEndDetails dragDetails;
...@@ -215,9 +159,10 @@ class ScrollEndNotification extends ScrollNotification { ...@@ -215,9 +159,10 @@ class ScrollEndNotification extends ScrollNotification {
class UserScrollNotification extends ScrollNotification { class UserScrollNotification extends ScrollNotification {
UserScrollNotification({ UserScrollNotification({
@required ScrollableState scrollable, @required ScrollMetrics metrics,
@required BuildContext context,
this.direction, this.direction,
}) : super(scrollable: scrollable); }) : super(metrics: metrics, context: context);
final ScrollDirection direction; final ScrollDirection direction;
......
...@@ -3,17 +3,160 @@ ...@@ -3,17 +3,160 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:math' as math; import 'dart:math' as math;
import 'dart:ui' as ui;
import 'package:flutter/gestures.dart' show kMinFlingVelocity; import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/physics.dart'; import 'package:flutter/physics.dart';
import 'overscroll_indicator.dart'; import 'overscroll_indicator.dart';
import 'scroll_position.dart'; import 'scroll_metrics.dart';
import 'scroll_simulation.dart'; import 'scroll_simulation.dart';
// The ScrollPhysics base class is defined in scroll_position.dart because it export 'package:flutter/physics.dart' show Tolerance;
// has as circular dependency with ScrollPosition.
export 'scroll_position.dart' show ScrollPhysics; @immutable
abstract class ScrollPhysics {
const ScrollPhysics(this.parent);
final ScrollPhysics parent;
ScrollPhysics applyTo(ScrollPhysics parent);
/// Used by [DragScrollActivity] and other user-driven activities to
/// convert an offset in logical pixels as provided by the [DragUpdateDetails]
/// into a delta to apply using [setPixels].
///
/// This is used by some [ScrollPosition] subclasses to apply friction during
/// overscroll situations.
///
/// This method must not adjust parts of the offset that are entirely within
/// the bounds described by the given `position`.
///
/// The given `position` is only valid during this method call. Do not keep a
/// reference to it to use later, as the values may update, may not update, or
/// may update to reflect an entirely unrelated scrollable.
double applyPhysicsToUserOffset(ScrollMetrics position, double offset) {
if (parent == null)
return offset;
return parent.applyPhysicsToUserOffset(position, offset);
}
/// Whether the scrollable should let the user adjust the scroll offset, for
/// example by dragging.
///
/// By default, the user can manipulate the scroll offset if, and only if,
/// there is actually content outside the viewport to reveal.
///
/// The given `position` is only valid during this method call. Do not keep a
/// reference to it to use later, as the values may update, may not update, or
/// may update to reflect an entirely unrelated scrollable.
bool shouldAcceptUserOffset(ScrollMetrics position) {
if (parent == null)
return position.pixels != 0.0 || position.minScrollExtent != position.maxScrollExtent;
return parent.shouldAcceptUserOffset(position);
}
/// Determines the overscroll by applying the boundary conditions.
///
/// Called by [ScrollPositionWithSingleContext.applyBoundaryConditions], which
/// is called by [ScrollPositionWithSingleContext.setPixels] just before the
/// [ScrollPosition.pixels] value is updated, to determine how much of the
/// offset is to be clamped off and sent to
/// [ScrollPositionWithSingleContext.didOverscrollBy].
///
/// The `value` argument is guaranteed to not equal [pixels] when this is
/// called.
///
/// It is possible for this method to be called when the [position] describes
/// an already-out-of-bounds position. In that case, the boundary conditions
/// should usually only prevent a further increase in the extent to which the
/// position is out of bounds, allowing a decrease to be applied successfully,
/// so that (for instance) an animation can smoothly snap an out of bounds
/// position to the bounds. See [BallisticScrollActivity].
///
/// This method must not clamp parts of the offset that are entirely within
/// the bounds described by the given `position`.
///
/// The given `position` is only valid during this method call. Do not keep a
/// reference to it to use later, as the values may update, may not update, or
/// may update to reflect an entirely unrelated scrollable.
double applyBoundaryConditions(ScrollMetrics position, double value) {
if (parent == null)
return 0.0;
return parent.applyBoundaryConditions(position, value);
}
/// Returns a simulation for ballisitic scrolling starting from the given
/// position with the given velocity.
///
/// This is used by [ScrollPositionWithSingleContext] in the
/// [ScrollPositionWithSingleContext.goBallistic] method. If the result
/// is non-null, [ScrollPositionWithSingleContext] will begin a
/// [BallisticScrollActivity] with the returned value. Otherwise, it will
/// begin an idle activity instead.
///
/// The given `position` is only valid during this method call. Do not keep a
/// reference to it to use later, as the values may update, may not update, or
/// may update to reflect an entirely unrelated scrollable.
Simulation createBallisticSimulation(ScrollMetrics position, double velocity) {
if (parent == null)
return null;
return parent.createBallisticSimulation(position, velocity);
}
static final SpringDescription _kDefaultSpring = new SpringDescription.withDampingRatio(
mass: 0.5,
springConstant: 100.0,
ratio: 1.1,
);
SpringDescription get spring => parent?.spring ?? _kDefaultSpring;
/// The default accuracy to which scrolling is computed.
static final Tolerance _kDefaultTolerance = new Tolerance(
// TODO(ianh): Handle the case of the device pixel ratio changing.
// TODO(ianh): Get this from the local MediaQuery not dart:ui's window object.
velocity: 1.0 / (0.050 * ui.window.devicePixelRatio), // logical pixels per second
distance: 1.0 / ui.window.devicePixelRatio // logical pixels
);
Tolerance get tolerance => parent?.tolerance ?? _kDefaultTolerance;
/// The minimum distance an input pointer drag must have moved to
/// to be considered a scroll fling gesture.
///
/// This value is typically compared with the distance traveled along the
/// scrolling axis.
///
/// See also:
///
/// * [VelocityTracker.getVelocityEstimate], which computes the velocity
/// of a press-drag-release gesture.
double get minFlingDistance => parent?.minFlingDistance ?? kTouchSlop;
/// The minimum velocity for an input pointer drag to be considered a
/// scroll fling.
///
/// This value is typically compared with the magnitude of fling gesture's
/// velocity along the scrolling axis.
///
/// See also:
///
/// * [VelocityTracker.getVelocityEstimate], which computes the velocity
/// of a press-drag-release gesture.
double get minFlingVelocity => parent?.minFlingVelocity ?? kMinFlingVelocity;
/// Scroll fling velocity magnitudes will be clamped to this value.
double get maxFlingVelocity => parent?.maxFlingVelocity ?? kMaxFlingVelocity;
@override
String toString() {
if (parent == null)
return runtimeType.toString();
return '$runtimeType -> $parent';
}
}
/// Scroll physics for environments that allow the scroll offset to go beyond /// Scroll physics for environments that allow the scroll offset to go beyond
/// the bounds of the content, but then bounce the content back to the edge of /// the bounds of the content, but then bounce the content back to the edge of
...@@ -42,7 +185,7 @@ class BouncingScrollPhysics extends ScrollPhysics { ...@@ -42,7 +185,7 @@ class BouncingScrollPhysics extends ScrollPhysics {
double get frictionFactor => 0.5; double get frictionFactor => 0.5;
@override @override
double applyPhysicsToUserOffset(ScrollPosition position, double offset) { double applyPhysicsToUserOffset(ScrollMetrics position, double offset) {
assert(offset != 0.0); assert(offset != 0.0);
assert(position.minScrollExtent <= position.maxScrollExtent); assert(position.minScrollExtent <= position.maxScrollExtent);
if (offset > 0.0) if (offset > 0.0)
...@@ -66,10 +209,10 @@ class BouncingScrollPhysics extends ScrollPhysics { ...@@ -66,10 +209,10 @@ class BouncingScrollPhysics extends ScrollPhysics {
} }
@override @override
double applyBoundaryConditions(ScrollPosition position, double value) => 0.0; double applyBoundaryConditions(ScrollMetrics position, double value) => 0.0;
@override @override
Simulation createBallisticSimulation(ScrollPosition position, double velocity) { Simulation createBallisticSimulation(ScrollMetrics position, double velocity) {
final Tolerance tolerance = this.tolerance; final Tolerance tolerance = this.tolerance;
if (velocity.abs() >= tolerance.velocity || position.outOfRange) { if (velocity.abs() >= tolerance.velocity || position.outOfRange) {
return new BouncingScrollSimulation( return new BouncingScrollSimulation(
...@@ -78,7 +221,8 @@ class BouncingScrollPhysics extends ScrollPhysics { ...@@ -78,7 +221,8 @@ class BouncingScrollPhysics extends ScrollPhysics {
velocity: velocity * 0.91, // TODO(abarth): We should move this constant closer to the drag end. velocity: velocity * 0.91, // TODO(abarth): We should move this constant closer to the drag end.
leadingExtent: position.minScrollExtent, leadingExtent: position.minScrollExtent,
trailingExtent: position.maxScrollExtent, trailingExtent: position.maxScrollExtent,
)..tolerance = tolerance; tolerance: tolerance,
);
} }
return null; return null;
} }
...@@ -113,8 +257,23 @@ class ClampingScrollPhysics extends ScrollPhysics { ...@@ -113,8 +257,23 @@ class ClampingScrollPhysics extends ScrollPhysics {
ClampingScrollPhysics applyTo(ScrollPhysics parent) => new ClampingScrollPhysics(parent: parent); ClampingScrollPhysics applyTo(ScrollPhysics parent) => new ClampingScrollPhysics(parent: parent);
@override @override
double applyBoundaryConditions(ScrollPosition position, double value) { double applyBoundaryConditions(ScrollMetrics position, double value) {
assert(value != position.pixels); assert(() {
if (value == position.pixels) {
throw new FlutterError(
'$runtimeType.applyBoundaryConditions() was called redundantly.\n'
'The proposed new position, $value, is exactly equal to the current position of the '
'given ${position.runtimeType}, ${position.pixels}.\n'
'The applyBoundaryConditions method should only be called when the value is '
'going to actually change the pixels, otherwise it is redundant.\n'
'The physics object in question was:\n'
' $this\n'
'The position object in question was:\n'
' $position\n'
);
}
return true;
});
if (value < position.pixels && position.pixels <= position.minScrollExtent) // underscroll if (value < position.pixels && position.pixels <= position.minScrollExtent) // underscroll
return value - position.pixels; return value - position.pixels;
if (position.maxScrollExtent <= position.pixels && position.pixels < value) // overscroll if (position.maxScrollExtent <= position.pixels && position.pixels < value) // overscroll
...@@ -127,7 +286,7 @@ class ClampingScrollPhysics extends ScrollPhysics { ...@@ -127,7 +286,7 @@ class ClampingScrollPhysics extends ScrollPhysics {
} }
@override @override
Simulation createBallisticSimulation(ScrollPosition position, double velocity) { Simulation createBallisticSimulation(ScrollMetrics position, double velocity) {
final Tolerance tolerance = this.tolerance; final Tolerance tolerance = this.tolerance;
if (position.outOfRange) { if (position.outOfRange) {
double end; double end;
...@@ -144,14 +303,17 @@ class ClampingScrollPhysics extends ScrollPhysics { ...@@ -144,14 +303,17 @@ class ClampingScrollPhysics extends ScrollPhysics {
tolerance: tolerance tolerance: tolerance
); );
} }
if (!position.atEdge && velocity.abs() >= tolerance.velocity) { if (velocity.abs() < tolerance.velocity)
return new ClampingScrollSimulation( return null;
position: position.pixels, if (velocity > 0.0 && position.pixels >= position.maxScrollExtent)
velocity: velocity, return null;
tolerance: tolerance, if (velocity < 0.0 && position.pixels <= position.minScrollExtent)
); return null;
} return new ClampingScrollSimulation(
return null; position: position.pixels,
velocity: velocity,
tolerance: tolerance,
);
} }
} }
...@@ -175,5 +337,5 @@ class AlwaysScrollableScrollPhysics extends ScrollPhysics { ...@@ -175,5 +337,5 @@ class AlwaysScrollableScrollPhysics extends ScrollPhysics {
AlwaysScrollableScrollPhysics applyTo(ScrollPhysics parent) => new AlwaysScrollableScrollPhysics(parent: parent); AlwaysScrollableScrollPhysics applyTo(ScrollPhysics parent) => new AlwaysScrollableScrollPhysics(parent: parent);
@override @override
bool shouldAcceptUserOffset(ScrollPosition position) => true; bool shouldAcceptUserOffset(ScrollMetrics position) => true;
} }
...@@ -3,278 +3,70 @@ ...@@ -3,278 +3,70 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async'; import 'dart:async';
import 'dart:math' as math;
import 'dart:ui' as ui show window;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/physics.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'basic.dart'; import 'basic.dart';
import 'framework.dart'; import 'framework.dart';
import 'gesture_detector.dart'; import 'gesture_detector.dart';
import 'notification_listener.dart'; import 'scroll_metrics.dart';
import 'scroll_notification.dart'; import 'scroll_physics.dart';
import 'scrollable.dart';
import 'ticker_provider.dart';
export 'package:flutter/physics.dart' show Tolerance; abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
abstract class AbstractScrollState {
BuildContext get context;
TickerProvider get vsync;
void setIgnorePointer(bool value);
void setCanDrag(bool value);
void didEndDrag();
void dispatchNotification(Notification notification);
}
@immutable
abstract class ScrollPhysics {
const ScrollPhysics(this.parent);
final ScrollPhysics parent;
ScrollPhysics applyTo(ScrollPhysics parent);
/// Used by [DragScrollActivity] and other user-driven activities to
/// convert an offset in logical pixels as provided by the [DragUpdateDetails]
/// into a delta to apply using [setPixels].
///
/// This is used by some [ScrollPosition] subclasses to apply friction during
/// overscroll situations.
double applyPhysicsToUserOffset(ScrollPosition position, double offset) {
if (parent == null)
return offset;
return parent.applyPhysicsToUserOffset(position, offset);
}
/// Whether the scrollable should let the user adjust the scroll offset, for
/// example by dragging.
///
/// By default, the user can manipulate the scroll offset if, and only if,
/// there is actually content outside the viewport to reveal.
bool shouldAcceptUserOffset(ScrollPosition position) {
if (parent == null)
return position.pixels != 0.0 || position.minScrollExtent != position.maxScrollExtent;
return parent.shouldAcceptUserOffset(position);
}
/// Determines the overscroll by applying the boundary conditions.
///
/// Called by [ScrollPosition.setPixels] just before the [pixels] value is
/// updated, to determine how much of the offset is to be clamped off and sent
/// to [ScrollPosition.reportOverscroll].
///
/// The `value` argument is guaranteed to not equal [pixels] when this is
/// called.
double applyBoundaryConditions(ScrollPosition position, double value) {
if (parent == null)
return 0.0;
return parent.applyBoundaryConditions(position, value);
}
/// Returns a simulation for ballisitic scrolling starting from the given
/// position with the given velocity.
///
/// If the result is non-null, the [ScrollPosition] will begin an
/// [BallisticScrollActivity] with the returned value. Otherwise, the
/// [ScrollPosition] will begin an idle activity instead.
Simulation createBallisticSimulation(ScrollPosition position, double velocity) {
if (parent == null)
return null;
return parent.createBallisticSimulation(position, velocity);
}
static final SpringDescription _kDefaultSpring = new SpringDescription.withDampingRatio(
mass: 0.5,
springConstant: 100.0,
ratio: 1.1,
);
SpringDescription get spring => parent?.spring ?? _kDefaultSpring;
/// The default accuracy to which scrolling is computed.
static final Tolerance _kDefaultTolerance = new Tolerance(
// TODO(ianh): Handle the case of the device pixel ratio changing.
// TODO(ianh): Get this from the local MediaQuery not dart:ui's window object.
velocity: 1.0 / (0.050 * ui.window.devicePixelRatio), // logical pixels per second
distance: 1.0 / ui.window.devicePixelRatio // logical pixels
);
Tolerance get tolerance => parent?.tolerance ?? _kDefaultTolerance;
/// The minimum distance an input pointer drag must have moved to
/// to be considered a scroll fling gesture.
///
/// This value is typically compared with the distance traveled along the
/// scrolling axis.
///
/// See also:
///
/// * [VelocityTracker.getVelocityEstimate], which computes the velocity
/// of a press-drag-release gesture.
double get minFlingDistance => parent?.minFlingDistance ?? kTouchSlop;
/// The minimum velocity for an input pointer drag to be considered a
/// scroll fling.
///
/// This value is typically compared with the magnitude of fling gesture's
/// velocity along the scrolling axis.
///
/// See also:
///
/// * [VelocityTracker.getVelocityEstimate], which computes the velocity
/// of a press-drag-release gesture.
double get minFlingVelocity => parent?.minFlingVelocity ?? kMinFlingVelocity;
/// Scroll fling velocity magnitudes will be clamped to this value.
double get maxFlingVelocity => parent?.maxFlingVelocity ?? kMaxFlingVelocity;
@override
String toString() {
if (parent == null)
return runtimeType.toString();
return '$runtimeType -> $parent';
}
}
class ScrollPosition extends ViewportOffset {
ScrollPosition({ ScrollPosition({
@required this.physics, @required this.physics,
@required this.state,
double initialPixels: 0.0,
ScrollPosition oldPosition, ScrollPosition oldPosition,
}) : _pixels = initialPixels { }) {
assert(physics != null);
assert(state != null);
assert(state.vsync != null);
if (oldPosition != null) if (oldPosition != null)
absorb(oldPosition); absorb(oldPosition);
if (activity == null)
beginIdleActivity();
assert(activity != null);
assert(activity.position == this);
} }
final ScrollPhysics physics; final ScrollPhysics physics;
final AbstractScrollState state; @override
double get minScrollExtent => _minScrollExtent;
double _minScrollExtent;
@override
double get maxScrollExtent => _maxScrollExtent;
double _maxScrollExtent;
@override @override
double get pixels => _pixels; double get pixels => _pixels;
double _pixels; double _pixels;
Future<Null> ensureVisible(RenderObject object, { @override
double alignment: 0.0, double get viewportDimension => _viewportDimension;
Duration duration: Duration.ZERO, double _viewportDimension;
Curve curve: Curves.ease,
}) {
assert(object.attached);
final RenderAbstractViewport viewport = RenderAbstractViewport.of(object);
assert(viewport != null);
final double target = viewport.getOffsetToReveal(object, alignment).clamp(minScrollExtent, maxScrollExtent);
if (target == pixels)
return new Future<Null>.value();
if (duration == Duration.ZERO) {
jumpTo(target);
return new Future<Null>.value();
}
return animateTo(target, duration: duration, curve: curve);
}
/// Animates the position from its current value to the given value.
///
/// Any active animation is canceled. If the user is currently scrolling, that
/// action is canceled.
///
/// The returned [Future] will complete when the animation ends, whether it
/// completed successfully or whether it was interrupted prematurely.
///
/// An animation will be interrupted whenever the user attempts to scroll
/// manually, or whenever another activity is started, or whenever the
/// animation reaches the edge of the viewport and attempts to overscroll. (If
/// the [ScrollPosition] does not overscroll but instead allows scrolling
/// beyond the extents, then going beyond the extents will not interrupt the
/// animation.)
///
/// The animation is indifferent to changes to the viewport or content
/// dimensions.
///
/// Once the animation has completed, the scroll position will attempt to
/// begin a ballistic activity in case its value is not stable (for example,
/// if it is scrolled beyond the extents and in that situation the scroll
/// position would normally bounce back).
///
/// The duration must not be zero. To jump to a particular value without an
/// animation, use [jumpTo].
///
/// The animation is handled by an [DrivenScrollActivity].
Future<Null> animateTo(double to, {
@required Duration duration,
@required Curve curve,
}) {
final DrivenScrollActivity activity = new DrivenScrollActivity(
this,
from: pixels,
to: to,
duration: duration,
curve: curve,
vsync: state.vsync,
);
beginActivity(activity);
return activity.done;
}
/// Jumps the scroll position from its current value to the given value, /// Whether [viewportDimension], [minScrollExtent], [maxScrollExtent],
/// without animation, and without checking if the new value is in range. /// [outOfRange], and [atEdge] are available yet.
///
/// Any active animation is canceled. If the user is currently scrolling, that
/// action is canceled.
/// ///
/// If this method changes the scroll position, a sequence of start/update/end /// Set to true just before the first time that [applyNewDimensions] is
/// scroll notifications will be dispatched. No overscroll notifications can /// called.
/// be generated by this method. bool get haveDimensions => _haveDimensions;
/// bool _haveDimensions = false;
/// If settle is true then, immediately after the jump, a ballistic activity
/// is started, in case the value was out of range.
void jumpTo(double value, { bool settle: true }) {
beginIdleActivity();
if (_pixels != value) {
final double oldPixels = _pixels;
_pixels = value;
notifyListeners();
state.dispatchNotification(activity.createScrollStartNotification(state));
state.dispatchNotification(activity.createScrollUpdateNotification(state, _pixels - oldPixels));
state.dispatchNotification(activity.createScrollEndNotification(state));
}
if (settle)
beginBallisticActivity(0.0);
}
/// Returns a description of the [Scrollable]. /// Take any current applicable state from the given [ScrollPosition].
/// ///
/// Accurately describing the metrics typicaly requires using information /// This method is called by the constructor if it is given an `oldPosition`.
/// provided by the viewport to the [applyViewportDimension] and
/// [applyContentDimensions] methods.
/// ///
/// The metrics do not need to be in absolute (pixel) units, but they must be /// This method can be destructive to the other [ScrollPosition]. The other
/// in consistent units (so that they can be compared over time or used to /// object must be disposed immediately after this call (in the same call
/// drive diagrammatic user interfaces such as scrollbars). /// stack, before microtask resolution, by whomever called this object's
ScrollMetrics getMetrics() { /// constructor).
return new ScrollMetrics( @protected
extentBefore: math.max(pixels - minScrollExtent, 0.0), @mustCallSuper
extentInside: math.min(pixels, maxScrollExtent) - math.max(pixels, minScrollExtent) + math.min(viewportDimension, maxScrollExtent - minScrollExtent), void absorb(ScrollPosition other) {
extentAfter: math.max(maxScrollExtent - pixels, 0.0), assert(other != null);
viewportDimension: viewportDimension, assert(_pixels == null);
); _minScrollExtent = other.minScrollExtent;
_maxScrollExtent = other.maxScrollExtent;
_pixels = other._pixels;
_viewportDimension = other.viewportDimension;
} }
/// Update the scroll position ([pixels]) to a given pixel value. /// Update the scroll position ([pixels]) to a given pixel value.
...@@ -289,47 +81,65 @@ class ScrollPosition extends ViewportOffset { ...@@ -289,47 +81,65 @@ class ScrollPosition extends ViewportOffset {
/// greater than the requested `value` by the given amount (underscroll past /// greater than the requested `value` by the given amount (underscroll past
/// the min extent). /// the min extent).
/// ///
/// Implementations of this method must dispatch scroll update notifications /// The amount of overscroll is computed by [applyBoundaryConditions].
/// (using [dispatchNotification] and ///
/// [ScrollActivity.createScrollUpdateNotification]) after applying the new /// The amount of the change that is applied is reported using [didUpdateScrollPositionBy].
/// value (so after [pixels] changes). If the entire change is not applied, /// If there is any overscroll, it is reported using [didOverscrollBy].
/// the overscroll should be reported by subsequently also dispatching an double setPixels(double newPixels) {
/// overscroll notification using assert(_pixels != null);
/// [ScrollActivity.createOverscrollNotification].
double setPixels(double value) {
assert(SchedulerBinding.instance.schedulerPhase.index <= SchedulerPhase.transientCallbacks.index); assert(SchedulerBinding.instance.schedulerPhase.index <= SchedulerPhase.transientCallbacks.index);
assert(activity.isScrolling); if (newPixels != pixels) {
if (value != pixels) { final double overScroll = applyBoundaryConditions(newPixels);
final double overScroll = physics.applyBoundaryConditions(this, value);
assert(() { assert(() {
final double delta = value - pixels; final double delta = newPixels - pixels;
if (overScroll.abs() > delta.abs()) { if (overScroll.abs() > delta.abs()) {
throw new FlutterError( throw new FlutterError(
'${physics.runtimeType}.applyBoundaryConditions returned invalid overscroll value.\n' '$runtimeType.applyBoundaryConditions returned invalid overscroll value.\n'
'setPixels() was called to change the scroll offset from $pixels to $value.\n' 'setPixels() was called to change the scroll offset from $pixels to $newPixels.\n'
'That is a delta of $delta units.\n' 'That is a delta of $delta units.\n'
'${physics.runtimeType}.applyBoundaryConditions reported an overscroll of $overScroll units.\n' '$runtimeType.applyBoundaryConditions reported an overscroll of $overScroll units.'
'The scroll extents are $minScrollExtent .. $maxScrollExtent, and the '
'viewport dimension is $viewportDimension.'
); );
} }
return true; return true;
}); });
final double oldPixels = _pixels; final double oldPixels = _pixels;
_pixels = value - overScroll; _pixels = newPixels - overScroll;
if (_pixels != oldPixels) { if (_pixels != oldPixels) {
notifyListeners(); notifyListeners();
state.dispatchNotification(activity.createScrollUpdateNotification(state, _pixels - oldPixels)); didUpdateScrollPositionBy(_pixels - oldPixels);
} }
if (overScroll != 0.0) { if (overScroll != 0.0) {
reportOverscroll(overScroll); didOverscrollBy(overScroll);
return overScroll; return overScroll;
} }
} }
return 0.0; return 0.0;
} }
@protected /// Change the value of [pixels] to the new value, without notifying any
/// customers.
///
/// This is used to adjust the position while doing layout. In particular,
/// this is typically called as a response to [applyViewportDimension] or
/// [applyContentDimensions] (in both cases, if this method is called, those
/// methods should then return false to indicate that the position has been
/// adjusted).
///
/// Calling this is rarely correct in other contexts. It will not immediately
/// cause the rendering to change, since it does not notify the widgets or
/// render objects that might be listening to this object: they will only
/// change when they next read the value, which could be arbitrarily later. It
/// is generally only appropriate in the very specific case of the value being
/// corrected during layout (since then the value is immediately read), in the
/// specific case of a [ScrollPosition] with a single viewport customer.
///
/// To cause the position to jump or animate to a new value, consider [jumpTo]
/// or [animateTo], which will honor the normal conventions for changing the
/// scroll offset.
///
/// To force the [pixels] to a particular value without honoring the normal
/// conventions for changing the scroll offset, consider [forcePixels]. (But
/// see the discussion there for why that might still be a bad idea.)
void correctPixels(double value) { void correctPixels(double value) {
_pixels = value; _pixels = value;
} }
...@@ -339,24 +149,55 @@ class ScrollPosition extends ViewportOffset { ...@@ -339,24 +149,55 @@ class ScrollPosition extends ViewportOffset {
_pixels += correction; _pixels += correction;
} }
/// Change the value of [pixels] to the new value, and notify any customers,
/// but without honoring normal conventions for changing the scroll offset.
///
/// This is used to implement [jumpTo]. It can also be used adjust the
/// position when the dimensions of the viewport change. It should only be
/// used when manually implementing the logic for honoring the relevant
/// conventions of the class. For example, [ScrollPositionWithSingleContext]
/// introduces [ScrollActivity] objects and uses [forcePixels] in conjunction
/// with adjusting the activity, e.g. by calling
/// [ScrollPositionWithSingleContext.goIdle], so that the activity does
/// not immediately set the value back. (Consider, for instance, a case where
/// one is using a [DrivenScrollActivity]. That object will ignore any calls
/// to [forcePixels], which would result in the rendering stuttering: changing
/// in response to [forcePixels], and then changing back to the next value
/// derived from the animation.)
///
/// To cause the position to jump or animate to a new value, consider [jumpTo]
/// or [animateTo].
///
/// This should not be called during layout. Consider [correctPixels] if you
/// find you need to adjust the position during layout.
@protected @protected
void reportOverscroll(double value) { void forcePixels(double value) {
assert(activity.isScrolling); assert(_pixels != null);
state.dispatchNotification(activity.createOverscrollNotification(state, value)); _pixels = value;
notifyListeners();
} }
double get viewportDimension => _viewportDimension; @protected
double _viewportDimension; double applyBoundaryConditions(double value) {
final double result = physics.applyBoundaryConditions(this, value);
double get minScrollExtent => _minScrollExtent; assert(() {
double _minScrollExtent; final double delta = value - pixels;
if (result.abs() > delta.abs()) {
double get maxScrollExtent => _maxScrollExtent; throw new FlutterError(
double _maxScrollExtent; '${physics.runtimeType}.applyBoundaryConditions returned invalid overscroll value.\n'
'The method was called to consider a change from $pixels to $value, which is a '
bool get outOfRange => pixels < minScrollExtent || pixels > maxScrollExtent; 'delta of ${delta.toStringAsFixed(1)} units. However, it returned an overscroll of '
'${result.toStringAsFixed(1)} units, which has a greater magnitude than the delta. '
bool get atEdge => pixels == minScrollExtent || pixels == maxScrollExtent; 'The applyBoundaryConditions method is only supposed to reduce the possible range '
'of movement, not increase it.\n'
'The scroll extents are $minScrollExtent .. $maxScrollExtent, and the '
'viewport dimension is $viewportDimension.'
);
}
return true;
});
return result;
}
bool _didChangeViewportDimension = true; bool _didChangeViewportDimension = true;
...@@ -379,94 +220,40 @@ class ScrollPosition extends ViewportOffset { ...@@ -379,94 +220,40 @@ class ScrollPosition extends ViewportOffset {
_didChangeViewportDimension) { _didChangeViewportDimension) {
_minScrollExtent = minScrollExtent; _minScrollExtent = minScrollExtent;
_maxScrollExtent = maxScrollExtent; _maxScrollExtent = maxScrollExtent;
activity.applyNewDimensions(); _haveDimensions = true;
applyNewDimensions();
_didChangeViewportDimension = false; _didChangeViewportDimension = false;
} }
state.setCanDrag(physics.shouldAcceptUserOffset(this));
return true; return true;
} }
/// Take any current applicable state from the given [ScrollPosition].
///
/// This method is called by the constructor, instead of calling
/// [beginIdleActivity], if it is given an `oldPosition`. It adopts the old
/// position's current [activity] as its own.
///
/// This method is destructive to the other [ScrollPosition]. The other
/// object must be disposed immediately after this call (in the same call
/// stack, before microtask resolution, by whomever called this object's
/// constructor).
///
/// If the old [ScrollPosition] object is a different [runtimeType] than this
/// one, the [ScrollActivity.resetActivity] method is invoked on the newly
/// adopted [ScrollActivity].
///
/// When overriding this method, call `super.absorb` after setting any
/// metrics-related or activity-related state, since this method may restart
/// the activity and scroll activities tend to use those metrics when being
/// restarted.
@protected @protected
@mustCallSuper void applyNewDimensions();
void absorb(ScrollPosition other) {
assert(activity == null);
assert(other != this);
assert(other.state == state);
assert(other.activity != null);
_pixels = other._pixels; /// Animates the position such that the given object is as visible as possible
_viewportDimension = other.viewportDimension; /// by just scrolling this position.
_minScrollExtent = other.minScrollExtent; Future<Null> ensureVisible(RenderObject object, {
_maxScrollExtent = other.maxScrollExtent; double alignment: 0.0,
_userScrollDirection = other._userScrollDirection; Duration duration: Duration.ZERO,
Curve curve: Curves.ease,
final bool oldIgnorePointer = shouldIgnorePointer; }) {
other.activity._position = this; assert(object.attached);
_activity = other.activity; final RenderAbstractViewport viewport = RenderAbstractViewport.of(object);
other._activity = null; assert(viewport != null);
if (oldIgnorePointer != shouldIgnorePointer)
state.setIgnorePointer(shouldIgnorePointer);
if (other.runtimeType != runtimeType)
activity.resetActivity();
}
bool get shouldIgnorePointer => activity?.shouldIgnorePointer;
void touched() { final double target = viewport.getOffsetToReveal(object, alignment).clamp(minScrollExtent, maxScrollExtent);
_activity.touched();
}
/// The direction that the user most recently began scrolling in. if (target == pixels)
@override return new Future<Null>.value();
ScrollDirection get userScrollDirection => _userScrollDirection;
ScrollDirection _userScrollDirection = ScrollDirection.idle;
/// Set [userScrollDirection] to the given value. if (duration == Duration.ZERO) {
/// jumpTo(target);
/// If this changes the value, then a [UserScrollNotification] is dispatched. return new Future<Null>.value();
/// }
/// This should only be set from the current [ScrollActivity] (see [activity]).
void updateUserScrollDirection(ScrollDirection value) {
assert(value != null);
if (userScrollDirection == value)
return;
_userScrollDirection = value;
state.dispatchNotification(new UserScrollNotification(scrollable: state, direction: value));
}
@override return animateTo(target, duration: duration, curve: curve);
void dispose() {
activity?.dispose(); // it will be null if it got absorbed by another ScrollPosition
_activity = null;
super.dispose();
} }
// SCROLL ACTIVITIES
ScrollActivity get activity => _activity;
ScrollActivity _activity;
/// This notifier's value is true if a scroll is underway and false if the scroll /// This notifier's value is true if a scroll is underway and false if the scroll
/// position is idle. /// position is idle.
/// ///
...@@ -474,353 +261,31 @@ class ScrollPosition extends ViewportOffset { ...@@ -474,353 +261,31 @@ class ScrollPosition extends ViewportOffset {
/// [State.dispose] method. /// [State.dispose] method.
final ValueNotifier<bool> isScrollingNotifier = new ValueNotifier<bool>(false); final ValueNotifier<bool> isScrollingNotifier = new ValueNotifier<bool>(false);
/// Change the current [activity], disposing of the old one and Future<Null> animateTo(double to, {
/// sending scroll notifications as necessary.
///
/// If the argument is null, this method has no effect. This is convenient for
/// cases where the new activity is obtained from another method, and that
/// method might return null, since it means the caller does not have to
/// explictly null-check the argument.
void beginActivity(ScrollActivity newActivity) {
if (newActivity == null)
return;
assert(newActivity.position == this);
final bool oldIgnorePointer = shouldIgnorePointer;
bool wasScrolling;
if (activity != null) {
wasScrolling = activity.isScrolling;
if (wasScrolling && !newActivity.isScrolling)
state.dispatchNotification(activity.createScrollEndNotification(state));
activity.dispose();
} else {
wasScrolling = false;
}
_activity = newActivity;
if (oldIgnorePointer != shouldIgnorePointer)
state.setIgnorePointer(shouldIgnorePointer);
isScrollingNotifier.value = _activity?.isScrolling ?? false;
if (!activity.isScrolling)
updateUserScrollDirection(ScrollDirection.idle);
if (!wasScrolling && activity.isScrolling)
state.dispatchNotification(activity.createScrollStartNotification(state));
}
void beginIdleActivity() {
beginActivity(new IdleScrollActivity(this));
}
DragScrollActivity beginDragActivity(DragStartDetails details) {
beginActivity(new DragScrollActivity(this, details));
return activity;
}
// ///
// /// The velocity should be in logical pixels per second.
void beginBallisticActivity(double velocity) {
final Simulation simulation = physics.createBallisticSimulation(this, velocity);
if (simulation != null) {
beginActivity(new BallisticScrollActivity(this, simulation, state.vsync));
} else {
beginIdleActivity();
}
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('$activity');
description.add('$userScrollDirection');
description.add('range: ${minScrollExtent?.toStringAsFixed(1)}..${maxScrollExtent?.toStringAsFixed(1)}');
description.add('viewport: ${viewportDimension?.toStringAsFixed(1)}');
}
}
/// Base class for scrolling activities like dragging, and flinging.
abstract class ScrollActivity {
ScrollActivity(this._position);
@protected
ScrollPosition get position => _position;
ScrollPosition _position;
/// Called by the [ScrollPosition] when it has changed type (for example, when
/// changing from an Android-style scroll position to an iOS-style scroll
/// position). If this activity can differ between the two modes, then it
/// should tell the position to restart that activity appropriately.
///
/// For example, [BallisticScrollActivity]'s implementation calls
/// [ScrollPosition.beginBallisticActivity].
void resetActivity() { }
Notification createScrollStartNotification(AbstractScrollState scrollable) {
return new ScrollStartNotification(scrollable: scrollable);
}
Notification createScrollUpdateNotification(AbstractScrollState scrollable, double scrollDelta) {
return new ScrollUpdateNotification(scrollable: scrollable, scrollDelta: scrollDelta);
}
Notification createOverscrollNotification(AbstractScrollState scrollable, double overscroll) {
return new OverscrollNotification(scrollable: scrollable, overscroll: overscroll);
}
Notification createScrollEndNotification(AbstractScrollState scrollable) {
return new ScrollEndNotification(scrollable: scrollable);
}
void touched() { }
void applyNewDimensions() { }
bool get shouldIgnorePointer;
bool get isScrolling;
@mustCallSuper
void dispose() {
_position = null;
}
@override
String toString() => '$runtimeType';
}
class IdleScrollActivity extends ScrollActivity {
IdleScrollActivity(ScrollPosition position) : super(position);
@override
void applyNewDimensions() {
position.beginBallisticActivity(0.0);
}
@override
bool get shouldIgnorePointer => false;
@override
bool get isScrolling => false;
}
class DragScrollActivity extends ScrollActivity {
DragScrollActivity(
ScrollPosition position,
DragStartDetails details,
) : _lastDetails = details, super(position);
@override
void touched() {
assert(false);
}
void update(DragUpdateDetails details, { bool reverse }) {
assert(details.primaryDelta != null);
_lastDetails = details;
double offset = details.primaryDelta;
if (offset == 0.0)
return;
if (reverse) // e.g. an AxisDirection.up scrollable
offset = -offset;
position.updateUserScrollDirection(offset > 0.0 ? ScrollDirection.forward : ScrollDirection.reverse);
position.setPixels(position.pixels - position.physics.applyPhysicsToUserOffset(position, offset));
// We ignore any reported overscroll returned by setPixels,
// because it gets reported via the reportOverscroll path.
}
void end(DragEndDetails details, { bool reverse }) {
assert(details.primaryVelocity != null);
double velocity = details.primaryVelocity;
if (reverse) // e.g. an AxisDirection.up scrollable
velocity = -velocity;
_lastDetails = details;
// We negate the velocity here because if the touch is moving downwards,
// the scroll has to move upwards. It's the same reason that update()
// above negates the delta before applying it to the scroll offset.
position.beginBallisticActivity(-velocity);
}
@override
void dispose() {
_lastDetails = null;
position.state.didEndDrag();
super.dispose();
}
dynamic _lastDetails;
@override
Notification createScrollStartNotification(AbstractScrollState scrollable) {
assert(_lastDetails is DragStartDetails);
return new ScrollStartNotification(scrollable: scrollable, dragDetails: _lastDetails);
}
@override
Notification createScrollUpdateNotification(AbstractScrollState scrollable, double scrollDelta) {
assert(_lastDetails is DragUpdateDetails);
return new ScrollUpdateNotification(scrollable: scrollable, scrollDelta: scrollDelta, dragDetails: _lastDetails);
}
@override
Notification createOverscrollNotification(AbstractScrollState scrollable, double overscroll) {
assert(_lastDetails is DragUpdateDetails);
return new OverscrollNotification(scrollable: scrollable, overscroll: overscroll, dragDetails: _lastDetails);
}
@override
Notification createScrollEndNotification(AbstractScrollState scrollable) {
// We might not have DragEndDetails yet if we're being called from beginActivity.
return new ScrollEndNotification(
scrollable: scrollable,
dragDetails: _lastDetails is DragEndDetails ? _lastDetails : null
);
}
@override
bool get shouldIgnorePointer => true;
@override
bool get isScrolling => true;
}
class BallisticScrollActivity extends ScrollActivity {
///
/// The velocity should be in logical pixels per second.
BallisticScrollActivity(
ScrollPosition position,
Simulation simulation,
TickerProvider vsync,
) : super(position) {
_controller = new AnimationController.unbounded(
value: position.pixels,
debugLabel: '$runtimeType',
vsync: vsync,
)
..addListener(_tick)
..animateWith(simulation)
.whenComplete(_end); // won't trigger if we dispose _controller first
}
@override
ScrollPosition get position => super.position;
double get velocity => _controller.velocity;
AnimationController _controller;
@override
void resetActivity() {
position.beginBallisticActivity(velocity);
}
@override
void touched() {
position.beginIdleActivity();
}
@override
void applyNewDimensions() {
position.beginBallisticActivity(velocity);
}
void _tick() {
if (position.setPixels(_controller.value) != 0.0)
position.beginIdleActivity();
}
void _end() {
position?.beginIdleActivity();
}
@override
Notification createOverscrollNotification(AbstractScrollState scrollable, double overscroll) {
return new OverscrollNotification(scrollable: scrollable, overscroll: overscroll, velocity: velocity);
}
@override
bool get shouldIgnorePointer => true;
@override
bool get isScrolling => true;
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
String toString() {
return '$runtimeType($_controller)';
}
}
class DrivenScrollActivity extends ScrollActivity {
DrivenScrollActivity(
ScrollPosition position, {
@required double from,
@required double to,
@required Duration duration, @required Duration duration,
@required Curve curve, @required Curve curve,
@required TickerProvider vsync, });
}) : super(position) {
assert(from != null);
assert(to != null);
assert(duration != null);
assert(duration > Duration.ZERO);
assert(curve != null);
_completer = new Completer<Null>();
_controller = new AnimationController.unbounded(
value: from,
debugLabel: '$runtimeType',
vsync: vsync,
)
..addListener(_tick)
..animateTo(to, duration: duration, curve: curve)
.whenComplete(_end); // won't trigger if we dispose _controller first
}
@override
ScrollPosition get position => super.position;
Completer<Null> _completer;
AnimationController _controller;
Future<Null> get done => _completer.future; void jumpTo(double value);
double get velocity => _controller.velocity; /// Deprecated. Use [jumpTo] or a custom [ScrollPosition] instead.
@Deprecated('This will lead to bugs.')
void jumpToWithoutSettling(double value);
@override void didTouch();
void touched() {
position.beginIdleActivity();
}
void _tick() {
if (position.setPixels(_controller.value) != 0.0)
position.beginIdleActivity();
}
void _end() {
position?.beginBallisticActivity(velocity);
}
@override Drag drag(DragStartDetails details, VoidCallback dragCancelCallback);
Notification createOverscrollNotification(AbstractScrollState scrollable, double overscroll) {
return new OverscrollNotification(scrollable: scrollable, overscroll: overscroll, velocity: velocity);
}
@override
bool get shouldIgnorePointer => true;
@override @protected
bool get isScrolling => true; void didUpdateScrollPositionBy(double delta);
@override @protected
void dispose() { void didOverscrollBy(double value);
_completer.complete();
_controller.dispose();
super.dispose();
}
@override @override
String toString() { void debugFillDescription(List<String> description) {
return '$runtimeType($_controller)'; super.debugFillDescription(description);
description.add('range: ${minScrollExtent?.toStringAsFixed(1)}..${maxScrollExtent?.toStringAsFixed(1)}');
description.add('viewport: ${viewportDimension?.toStringAsFixed(1)}');
} }
} }
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/physics.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:meta/meta.dart';
import 'basic.dart';
import 'framework.dart';
import 'gesture_detector.dart';
import 'scroll_activity.dart';
import 'scroll_context.dart';
import 'scroll_notification.dart';
import 'scroll_physics.dart';
import 'scroll_position.dart';
/// A scroll position that manages scroll activities for a single
/// [ScrollContext].
///
/// This class is a concrete subclass of [ScrollPosition] logic that handles a
/// single [ScrollContext], such as a [Scrollable]. An instance of this class
/// manages [ScrollActivity] instances, which change what content is visible in
/// the [Scrollable]'s [Viewport].
///
/// See also:
///
/// * [ScrollPosition], which defines the underlying model for a position
/// within a [Scrollable] but is agnositic as to how that position is
/// changed.
/// * [ScrollView] and its subclasses such as [ListView], which use
/// [ScrollPositionWithSingleContext] to manage their scroll position.
/// * [ScrollController], which can manipulate one or more [ScrollPosition]s,
/// and which uses [ScrollPositionWithSingleContext] as its default class for
/// scroll positions.
class ScrollPositionWithSingleContext extends ScrollPosition implements ScrollActivityDelegate {
/// Create a [ScrollPosition] object that manages its behavior using
/// [ScrollActivity] objects.
///
/// The `initialPixels` argument can be null, but in that case it is
/// imperative that the value be set, using [correctPixels], as soon as
/// [applyNewDimensions] is invoked, before calling the inherited
/// implementation of that method.
ScrollPositionWithSingleContext({
@required ScrollPhysics physics,
@required this.context,
double initialPixels: 0.0,
ScrollPosition oldPosition,
}) : super(physics: physics, oldPosition: oldPosition) {
// If oldPosition is not null, the superclass will first call absorb(),
// which may set _pixels and _activity.
assert(physics != null);
assert(context != null);
assert(context.vsync != null);
if (pixels == null && initialPixels != null)
correctPixels(initialPixels);
if (activity == null)
goIdle();
assert(activity != null);
}
final ScrollContext context;
@override
AxisDirection get axisDirection => context.axisDirection;
@override
double setPixels(double newPixels) {
assert(activity.isScrolling);
return super.setPixels(newPixels);
}
@override
void correctBy(double correction) {
correctPixels(pixels + correction);
}
/// Take any current applicable state from the given [ScrollPosition].
///
/// This method is called by the constructor, before calling [ensureActivity],
/// if it is given an `oldPosition`. It adopts the old position's current
/// [activity] as its own.
///
/// This method is destructive to the other [ScrollPosition]. The other
/// object must be disposed immediately after this call (in the same call
/// stack, before microtask resolution, by whomever called this object's
/// constructor).
///
/// If the old [ScrollPosition] object is a different [runtimeType] than this
/// one, the [ScrollActivity.resetActivity] method is invoked on the newly
/// adopted [ScrollActivity].
///
/// When overriding this method, call `super.absorb` after setting any
/// metrics-related or activity-related state, since this method may restart
/// the activity and scroll activities tend to use those metrics when being
/// restarted.
@override
void absorb(ScrollPosition otherPosition) {
assert(otherPosition != null);
if (otherPosition is! ScrollPositionWithSingleContext) {
super.absorb(otherPosition);
goIdle();
return;
}
final ScrollPositionWithSingleContext other = otherPosition;
assert(other != this);
assert(other.context == context);
super.absorb(other);
_userScrollDirection = other._userScrollDirection;
assert(activity == null);
assert(other.activity != null);
other.activity.updateDelegate(this);
_activity = other.activity;
other._activity = null;
if (other.runtimeType != runtimeType)
activity.resetActivity();
context.setIgnorePointer(shouldIgnorePointer);
isScrollingNotifier.value = _activity.isScrolling;
}
/// Notifies the activity that the dimensions of the underlying viewport or
/// contents have changed.
///
/// When this method is called, it should be called _after_ any corrections
/// are applied to [pixels] using [correctPixels], not before.
///
/// See also:
///
/// * [ScrollPosition.applyViewportDimension], which is called when new
/// viewport dimensions are established.
/// * [ScrollPosition.applyContentDimensions], which is called after new
/// viewport dimensions are established, and also if new content dimensions
/// are established, and which calls [ScrollPosition.applyNewDimensions].
@mustCallSuper
@override
void applyNewDimensions() {
assert(pixels != null);
activity.applyNewDimensions();
context.setCanDrag(physics.shouldAcceptUserOffset(this));
}
// SCROLL ACTIVITIES
@protected
ScrollActivity get activity => _activity;
ScrollActivity _activity;
@protected
bool get shouldIgnorePointer => activity?.shouldIgnorePointer;
/// Change the current [activity], disposing of the old one and
/// sending scroll notifications as necessary.
///
/// If the argument is null, this method has no effect. This is convenient for
/// cases where the new activity is obtained from another method, and that
/// method might return null, since it means the caller does not have to
/// explictly null-check the argument.
void beginActivity(ScrollActivity newActivity) {
if (newActivity == null)
return;
assert(newActivity.delegate == this);
bool wasScrolling, oldIgnorePointer;
if (_activity != null) {
oldIgnorePointer = _activity.shouldIgnorePointer;
wasScrolling = _activity.isScrolling;
if (wasScrolling && !newActivity.isScrolling)
_didEndScroll();
_activity.dispose();
} else {
oldIgnorePointer = false;
wasScrolling = false;
}
_activity = newActivity;
isScrollingNotifier.value = activity.isScrolling;
if (!activity.isScrolling)
updateUserScrollDirection(ScrollDirection.idle);
if (oldIgnorePointer != shouldIgnorePointer)
context.setIgnorePointer(shouldIgnorePointer);
if (!wasScrolling && _activity.isScrolling)
_didStartScroll();
}
@override
double applyUserOffset(double delta) {
updateUserScrollDirection(delta > 0.0 ? ScrollDirection.forward : ScrollDirection.reverse);
return setPixels(pixels - physics.applyPhysicsToUserOffset(this, delta));
}
/// End the current [ScrollActivity], replacing it with an
/// [IdleScrollActivity].
@override
void goIdle() {
beginActivity(new IdleScrollActivity(this));
}
/// Start a physics-driven simulation that settles the [pixels] position,
/// starting at a particular velocity.
///
/// This method defers to [ScrollPhysics.createBallisticSimulation], which
/// typically provides a bounce simulation when the current position is out of
/// bounds and a friction simulation when the position is in bounds but has a
/// non-zero velocity.
///
/// The velocity should be in logical pixels per second.
@override
void goBallistic(double velocity) {
assert(pixels != null);
final Simulation simulation = physics.createBallisticSimulation(this, velocity);
if (simulation != null) {
beginActivity(new BallisticScrollActivity(this, simulation, context.vsync));
} else {
goIdle();
}
}
/// The direction that the user most recently began scrolling in.
///
/// If the user is not scrolling, this will return [ScrollDirection.idle] even
/// if there is an [activity] currently animating the position.
@override
ScrollDirection get userScrollDirection => _userScrollDirection;
ScrollDirection _userScrollDirection = ScrollDirection.idle;
/// Set [userScrollDirection] to the given value.
///
/// If this changes the value, then a [UserScrollNotification] is dispatched.
@visibleForTesting
void updateUserScrollDirection(ScrollDirection value) {
assert(value != null);
if (userScrollDirection == value)
return;
_userScrollDirection = value;
_didUpdateScrollDirection(value);
}
// FEATURES USED BY SCROLL CONTROLLERS
/// Animates the position from its current value to the given value.
///
/// Any active animation is canceled. If the user is currently scrolling, that
/// action is canceled.
///
/// The returned [Future] will complete when the animation ends, whether it
/// completed successfully or whether it was interrupted prematurely.
///
/// An animation will be interrupted whenever the user attempts to scroll
/// manually, or whenever another activity is started, or whenever the
/// animation reaches the edge of the viewport and attempts to overscroll. (If
/// the [ScrollPosition] does not overscroll but instead allows scrolling
/// beyond the extents, then going beyond the extents will not interrupt the
/// animation.)
///
/// The animation is indifferent to changes to the viewport or content
/// dimensions.
///
/// Once the animation has completed, the scroll position will attempt to
/// begin a ballistic activity in case its value is not stable (for example,
/// if it is scrolled beyond the extents and in that situation the scroll
/// position would normally bounce back).
///
/// The duration must not be zero. To jump to a particular value without an
/// animation, use [jumpTo].
///
/// The animation is handled by an [DrivenScrollActivity].
@override
Future<Null> animateTo(double to, {
@required Duration duration,
@required Curve curve,
}) {
final DrivenScrollActivity activity = new DrivenScrollActivity(
this,
from: pixels,
to: to,
duration: duration,
curve: curve,
vsync: context.vsync,
);
beginActivity(activity);
return activity.done;
}
/// Jumps the scroll position from its current value to the given value,
/// without animation, and without checking if the new value is in range.
///
/// Any active animation is canceled. If the user is currently scrolling, that
/// action is canceled.
///
/// If this method changes the scroll position, a sequence of start/update/end
/// scroll notifications will be dispatched. No overscroll notifications can
/// be generated by this method.
///
/// If settle is true then, immediately after the jump, a ballistic activity
/// is started, in case the value was out of range.
@override
void jumpTo(double value) {
goIdle();
if (pixels != value) {
final double oldPixels = pixels;
forcePixels(value);
notifyListeners();
_didStartScroll();
didUpdateScrollPositionBy(pixels - oldPixels);
_didEndScroll();
}
goBallistic(0.0);
}
/// Deprecated. Use [jumpTo] or a custom [ScrollPosition] instead.
@Deprecated('This will lead to bugs.')
@override
void jumpToWithoutSettling(double value) {
goIdle();
if (pixels != value) {
final double oldPixels = pixels;
forcePixels(value);
notifyListeners();
_didStartScroll();
didUpdateScrollPositionBy(pixels - oldPixels);
_didEndScroll();
}
}
/// Inform the current activity that the user touched the area to which this
/// object relates.
@override
void didTouch() {
assert(activity != null);
activity.didTouch();
}
/// Start a drag activity corresponding to the given [DragStartDetails].
///
/// The `dragCancelCallback` argument will be invoked if the drag is ended
/// prematurely (e.g. from another activity taking over). See
/// [DragScrollActivity.onDragCanceled] for details.
@override
DragScrollActivity drag(DragStartDetails details, VoidCallback dragCancelCallback) {
beginActivity(new DragScrollActivity(this, details, dragCancelCallback));
return activity;
}
@override
void dispose() {
assert(pixels != null);
activity?.dispose(); // it will be null if it got absorbed by another ScrollPosition
_activity = null;
super.dispose();
}
// NOTIFICATION DISPATCH
/// Called by [beginActivity] to report when an activity has started.
void _didStartScroll() {
activity.dispatchScrollStartNotification(cloneMetrics(), context.notificationContext);
}
/// Called by [setPixels] to report a change to the [pixels] position.
@override
void didUpdateScrollPositionBy(double delta) {
activity.dispatchScrollUpdateNotification(cloneMetrics(), context.notificationContext, delta);
}
/// Called by [beginActivity] to report when an activity has ended.
void _didEndScroll() {
activity.dispatchScrollEndNotification(cloneMetrics(), context.notificationContext);
}
/// Called by [setPixels] to report overscroll when an attempt is made to
/// change the [pixels] position. Overscroll is the amount of change that was
/// not applied to the [pixels] value.
@override
void didOverscrollBy(double value) {
assert(activity.isScrolling);
activity.dispatchOverscrollNotification(cloneMetrics(), context.notificationContext, value);
}
/// Called by [updateUserScrollDirection] to report that the
/// [userScrollDirection] has changed.
void _didUpdateScrollDirection(ScrollDirection direction) {
new UserScrollNotification(metrics: cloneMetrics(), context: context.notificationContext, direction: direction).dispatch(context.notificationContext);
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('${context.runtimeType}');
description.add('$physics');
description.add('$activity');
description.add('$userScrollDirection');
}
}
...@@ -10,7 +10,6 @@ import 'framework.dart'; ...@@ -10,7 +10,6 @@ import 'framework.dart';
import 'primary_scroll_controller.dart'; import 'primary_scroll_controller.dart';
import 'scroll_controller.dart'; import 'scroll_controller.dart';
import 'scroll_physics.dart'; import 'scroll_physics.dart';
import 'scroll_position.dart';
import 'scrollable.dart'; import 'scrollable.dart';
import 'sliver.dart'; import 'sliver.dart';
import 'viewport.dart'; import 'viewport.dart';
......
...@@ -14,8 +14,9 @@ import 'framework.dart'; ...@@ -14,8 +14,9 @@ import 'framework.dart';
import 'gesture_detector.dart'; import 'gesture_detector.dart';
import 'notification_listener.dart'; import 'notification_listener.dart';
import 'scroll_configuration.dart'; import 'scroll_configuration.dart';
import 'scroll_context.dart';
import 'scroll_controller.dart'; import 'scroll_controller.dart';
import 'scroll_notification.dart'; import 'scroll_physics.dart';
import 'scroll_position.dart'; import 'scroll_position.dart';
import 'ticker_provider.dart'; import 'ticker_provider.dart';
import 'viewport.dart'; import 'viewport.dart';
...@@ -68,7 +69,8 @@ class Scrollable extends StatefulWidget { ...@@ -68,7 +69,8 @@ class Scrollable extends StatefulWidget {
return widget?.scrollable; return widget?.scrollable;
} }
/// Scrolls the closest enclosing scrollable to make the given context visible. /// Scrolls the scrollables that enclose the given context so as to make the
/// given context visible.
static Future<Null> ensureVisible(BuildContext context, { static Future<Null> ensureVisible(BuildContext context, {
double alignment: 0.0, double alignment: 0.0,
Duration duration: Duration.ZERO, Duration duration: Duration.ZERO,
...@@ -91,7 +93,7 @@ class Scrollable extends StatefulWidget { ...@@ -91,7 +93,7 @@ class Scrollable extends StatefulWidget {
if (futures.isEmpty || duration == Duration.ZERO) if (futures.isEmpty || duration == Duration.ZERO)
return new Future<Null>.value(); return new Future<Null>.value();
if (futures.length == 1) if (futures.length == 1)
return futures.first; return futures.single;
return Future.wait<Null>(futures); return Future.wait<Null>(futures);
} }
} }
...@@ -128,16 +130,18 @@ class _ScrollableScope extends InheritedWidget { ...@@ -128,16 +130,18 @@ class _ScrollableScope extends InheritedWidget {
/// This class is not intended to be subclassed. To specialize the behavior of a /// This class is not intended to be subclassed. To specialize the behavior of a
/// [Scrollable], provide it with a [ScrollPhysics]. /// [Scrollable], provide it with a [ScrollPhysics].
class ScrollableState extends State<Scrollable> with TickerProviderStateMixin class ScrollableState extends State<Scrollable> with TickerProviderStateMixin
implements AbstractScrollState { implements ScrollContext {
/// The controller for this [Scrollable] widget's viewport position. /// The manager for this [Scrollable] widget's viewport position.
/// ///
/// To control what kind of [ScrollPosition] is created for a [Scrollable], /// To control what kind of [ScrollPosition] is created for a [Scrollable],
/// provide it with custom [ScrollPhysics] that creates the appropriate /// provide it with custom [ScrollController] that creates the appropriate
/// [ScrollPosition] controller in its [ScrollPhysics.createScrollPosition] /// [ScrollPosition] in its [ScrollController.createScrollPosition] method.
/// method.
ScrollPosition get position => _position; ScrollPosition get position => _position;
ScrollPosition _position; ScrollPosition _position;
@override
AxisDirection get axisDirection => widget.axisDirection;
ScrollBehavior _configuration; ScrollBehavior _configuration;
ScrollPhysics _physics; ScrollPhysics _physics;
...@@ -224,6 +228,7 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin ...@@ -224,6 +228,7 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin
..onStart = _handleDragStart ..onStart = _handleDragStart
..onUpdate = _handleDragUpdate ..onUpdate = _handleDragUpdate
..onEnd = _handleDragEnd ..onEnd = _handleDragEnd
..onCancel = _handleDragCancel
..minFlingDistance = _physics?.minFlingDistance ..minFlingDistance = _physics?.minFlingDistance
..minFlingVelocity = _physics?.minFlingVelocity ..minFlingVelocity = _physics?.minFlingVelocity
..maxFlingVelocity = _physics?.maxFlingVelocity; ..maxFlingVelocity = _physics?.maxFlingVelocity;
...@@ -238,6 +243,7 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin ...@@ -238,6 +243,7 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin
..onStart = _handleDragStart ..onStart = _handleDragStart
..onUpdate = _handleDragUpdate ..onUpdate = _handleDragUpdate
..onEnd = _handleDragEnd ..onEnd = _handleDragEnd
..onCancel = _handleDragCancel
..minFlingDistance = _physics?.minFlingDistance ..minFlingDistance = _physics?.minFlingDistance
..minFlingVelocity = _physics?.minFlingVelocity ..minFlingVelocity = _physics?.minFlingVelocity
..maxFlingVelocity = _physics?.maxFlingVelocity; ..maxFlingVelocity = _physics?.maxFlingVelocity;
...@@ -268,54 +274,41 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin ...@@ -268,54 +274,41 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin
} }
@override @override
@protected BuildContext get notificationContext => _gestureDetectorKey.currentContext;
void dispatchNotification(Notification notification) {
assert(mounted);
notification.dispatch(_gestureDetectorKey.currentContext);
}
// TOUCH HANDLERS // TOUCH HANDLERS
DragScrollActivity _drag; Drag _drag;
bool get _reverseDirection {
assert(widget.axisDirection != null);
switch (widget.axisDirection) {
case AxisDirection.up:
case AxisDirection.left:
return true;
case AxisDirection.down:
case AxisDirection.right:
return false;
}
return null;
}
void _handleDragDown(DragDownDetails details) { void _handleDragDown(DragDownDetails details) {
assert(_drag == null); assert(_drag == null);
position.touched(); position.didTouch();
} }
void _handleDragStart(DragStartDetails details) { void _handleDragStart(DragStartDetails details) {
assert(_drag == null); assert(_drag == null);
_drag = position.beginDragActivity(details); _drag = position.drag(details, _disposeDrag);
assert(_drag != null); assert(_drag != null);
} }
void _handleDragUpdate(DragUpdateDetails details) { void _handleDragUpdate(DragUpdateDetails details) {
// _drag might be null if the drag activity ended and called didEndDrag. // _drag might be null if the drag activity ended and called _disposeDrag.
_drag?.update(details, reverse: _reverseDirection); _drag?.update(details);
} }
void _handleDragEnd(DragEndDetails details) { void _handleDragEnd(DragEndDetails details) {
// _drag might be null if the drag activity ended and called didEndDrag. // _drag might be null if the drag activity ended and called _disposeDrag.
_drag?.end(details, reverse: _reverseDirection); _drag?.end(details);
assert(_drag == null); assert(_drag == null);
} }
@override void _handleDragCancel() {
@protected // _drag might be null if the drag activity ended and called _disposeDrag.
void didEndDrag() { _drag?.cancel();
assert(_drag == null);
}
void _disposeDrag() {
_drag = null; _drag = null;
} }
......
...@@ -48,11 +48,15 @@ export 'src/widgets/preferred_size.dart'; ...@@ -48,11 +48,15 @@ export 'src/widgets/preferred_size.dart';
export 'src/widgets/primary_scroll_controller.dart'; export 'src/widgets/primary_scroll_controller.dart';
export 'src/widgets/raw_keyboard_listener.dart'; export 'src/widgets/raw_keyboard_listener.dart';
export 'src/widgets/routes.dart'; export 'src/widgets/routes.dart';
export 'src/widgets/scroll_activity.dart';
export 'src/widgets/scroll_configuration.dart'; export 'src/widgets/scroll_configuration.dart';
export 'src/widgets/scroll_context.dart';
export 'src/widgets/scroll_controller.dart'; export 'src/widgets/scroll_controller.dart';
export 'src/widgets/scroll_metrics.dart';
export 'src/widgets/scroll_notification.dart'; export 'src/widgets/scroll_notification.dart';
export 'src/widgets/scroll_physics.dart'; export 'src/widgets/scroll_physics.dart';
export 'src/widgets/scroll_position.dart'; export 'src/widgets/scroll_position.dart';
export 'src/widgets/scroll_position_with_single_context.dart';
export 'src/widgets/scroll_simulation.dart'; export 'src/widgets/scroll_simulation.dart';
export 'src/widgets/scroll_view.dart'; export 'src/widgets/scroll_view.dart';
export 'src/widgets/scrollable.dart'; export 'src/widgets/scrollable.dart';
......
...@@ -25,7 +25,7 @@ void main() { ...@@ -25,7 +25,7 @@ void main() {
testWidgets('Inherited ScrollConfiguration changed', (WidgetTester tester) async { testWidgets('Inherited ScrollConfiguration changed', (WidgetTester tester) async {
final GlobalKey key = new GlobalKey(debugLabel: 'scrollable'); final GlobalKey key = new GlobalKey(debugLabel: 'scrollable');
TestScrollBehavior behavior; TestScrollBehavior behavior;
ScrollPosition position; ScrollPositionWithSingleContext position;
final Widget scrollView = new SingleChildScrollView( final Widget scrollView = new SingleChildScrollView(
key: key, key: key,
...@@ -48,7 +48,7 @@ void main() { ...@@ -48,7 +48,7 @@ void main() {
expect(behavior, isNotNull); expect(behavior, isNotNull);
expect(behavior.flag, isTrue); expect(behavior.flag, isTrue);
expect(position.physics, const isInstanceOf<ClampingScrollPhysics>()); expect(position.physics, const isInstanceOf<ClampingScrollPhysics>());
ScrollMetrics metrics = position.getMetrics(); ScrollMetrics metrics = position.cloneMetrics();
expect(metrics.extentAfter, equals(400.0)); expect(metrics.extentAfter, equals(400.0));
expect(metrics.viewportDimension, equals(600.0)); expect(metrics.viewportDimension, equals(600.0));
...@@ -64,7 +64,7 @@ void main() { ...@@ -64,7 +64,7 @@ void main() {
expect(behavior.flag, isFalse); expect(behavior.flag, isFalse);
expect(position.physics, const isInstanceOf<BouncingScrollPhysics>()); expect(position.physics, const isInstanceOf<BouncingScrollPhysics>());
// Regression test for https://github.com/flutter/flutter/issues/5856 // Regression test for https://github.com/flutter/flutter/issues/5856
metrics = position.getMetrics(); metrics = position.cloneMetrics();
expect(metrics.extentAfter, equals(400.0)); expect(metrics.extentAfter, equals(400.0));
expect(metrics.viewportDimension, equals(600.0)); expect(metrics.viewportDimension, equals(600.0));
}); });
......
// Copyright 2017 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_test/flutter_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class TestScrollPosition extends ScrollPosition {
TestScrollPosition({
ScrollPhysics physics,
AbstractScrollState state,
ScrollPosition oldPosition,
}) : _pixels = 100.0, super(
physics: physics,
state: state,
oldPosition: oldPosition,
) {
assert(physics is TestScrollPhysics);
}
@override
TestScrollPhysics get physics => super.physics;
double _pixels;
@override
double get pixels => _pixels;
@override
double setPixels(double value) {
final double oldPixels = _pixels;
_pixels = value;
state.dispatchNotification(activity.createScrollUpdateNotification(state, _pixels - oldPixels));
return 0.0;
}
@override
void correctBy(double correction) {
_pixels += correction;
}
@override
ScrollMetrics getMetrics() {
final double insideExtent = viewportDimension;
final double beforeExtent = _pixels - minScrollExtent;
final double afterExtent = maxScrollExtent - _pixels;
if (insideExtent > 0.0) {
return new ScrollMetrics(
extentBefore: physics.extentMultiplier * beforeExtent / insideExtent,
extentInside: physics.extentMultiplier,
extentAfter: physics.extentMultiplier * afterExtent / insideExtent,
viewportDimension: viewportDimension,
);
} else {
return new ScrollMetrics(
extentBefore: 0.0,
extentInside: 0.0,
extentAfter: 0.0,
viewportDimension: viewportDimension,
);
}
}
@override
Future<Null> ensureVisible(RenderObject object, {
double alignment: 0.0,
Duration duration: Duration.ZERO,
Curve curve: Curves.ease,
}) {
return new Future<Null>.value();
}
}
class TestScrollPhysics extends ScrollPhysics {
const TestScrollPhysics({ this.extentMultiplier, ScrollPhysics parent }) : super(parent);
final double extentMultiplier;
@override
ScrollPhysics applyTo(ScrollPhysics parent) {
return new TestScrollPhysics(
extentMultiplier: extentMultiplier,
parent: parent,
);
}
}
class TestScrollController extends ScrollController {
@override
ScrollPosition createScrollPosition(ScrollPhysics physics, AbstractScrollState state, ScrollPosition oldPosition) {
return new TestScrollPosition(physics: physics, state: state, oldPosition: oldPosition);
}
}
class TestScrollBehavior extends ScrollBehavior {
const TestScrollBehavior(this.extentMultiplier);
final double extentMultiplier;
@override
ScrollPhysics getScrollPhysics(BuildContext context) {
return new TestScrollPhysics(
extentMultiplier: extentMultiplier
).applyTo(super.getScrollPhysics(context));
}
@override
Widget buildViewportChrome(BuildContext context, Widget child, AxisDirection axisDirection) => child;
@override
bool shouldNotify(TestScrollBehavior oldDelegate) {
return extentMultiplier != oldDelegate.extentMultiplier;
}
}
void main() {
testWidgets('Changing the scroll behavior dynamically', (WidgetTester tester) async {
await tester.pumpWidget(new ScrollConfiguration(
behavior: const TestScrollBehavior(1.0),
child: new CustomScrollView(
controller: new TestScrollController(),
slivers: <Widget>[
const SliverToBoxAdapter(child: const SizedBox(height: 2000.0)),
],
),
));
final ScrollableState state = tester.state(find.byType(Scrollable));
expect(state.position.getMetrics().extentInside, 1.0);
await tester.pumpWidget(new ScrollConfiguration(
behavior: const TestScrollBehavior(2.0),
child: new CustomScrollView(
controller: new TestScrollController(),
slivers: <Widget>[
const SliverToBoxAdapter(child: const SizedBox(height: 2000.0)),
],
),
));
expect(state.position.getMetrics().extentInside, 2.0);
});
}
...@@ -5,15 +5,15 @@ ...@@ -5,15 +5,15 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
class TestScrollPosition extends ScrollPosition { class TestScrollPosition extends ScrollPositionWithSingleContext {
TestScrollPosition({ TestScrollPosition({
ScrollPhysics physics, ScrollPhysics physics,
AbstractScrollState state, ScrollContext state,
double initialPixels: 0.0, double initialPixels: 0.0,
ScrollPosition oldPosition, ScrollPosition oldPosition,
}) : super( }) : super(
physics: physics, physics: physics,
state: state, context: state,
initialPixels: initialPixels, initialPixels: initialPixels,
oldPosition: oldPosition, oldPosition: oldPosition,
); );
...@@ -21,10 +21,10 @@ class TestScrollPosition extends ScrollPosition { ...@@ -21,10 +21,10 @@ class TestScrollPosition extends ScrollPosition {
class TestScrollController extends ScrollController { class TestScrollController extends ScrollController {
@override @override
ScrollPosition createScrollPosition(ScrollPhysics physics, AbstractScrollState state, ScrollPosition oldPosition) { ScrollPosition createScrollPosition(ScrollPhysics physics, ScrollContext context, ScrollPosition oldPosition) {
return new TestScrollPosition( return new TestScrollPosition(
physics: physics, physics: physics,
state: state, state: context,
initialPixels: initialScrollOffset, initialPixels: initialScrollOffset,
oldPosition: oldPosition, oldPosition: oldPosition,
); );
......
...@@ -162,7 +162,7 @@ void main() { ...@@ -162,7 +162,7 @@ void main() {
], ],
), ),
); );
final ScrollPosition position = tester.state<ScrollableState>(find.byType(Scrollable)).position; final ScrollPositionWithSingleContext position = tester.state<ScrollableState>(find.byType(Scrollable)).position;
verifyPaintPosition(key1, const Offset(0.0, 0.0), true); verifyPaintPosition(key1, const Offset(0.0, 0.0), true);
verifyPaintPosition(key2, const Offset(0.0, 600.0), false); verifyPaintPosition(key2, const Offset(0.0, 600.0), false);
...@@ -175,7 +175,7 @@ void main() { ...@@ -175,7 +175,7 @@ void main() {
verifyPaintPosition(key3, const Offset(0.0, 0.0), true); verifyPaintPosition(key3, const Offset(0.0, 0.0), true);
position.animateTo(bigHeight + delegate.maxExtent * 1.9, curve: Curves.linear, duration: const Duration(minutes: 1)); position.animateTo(bigHeight + delegate.maxExtent * 1.9, curve: Curves.linear, duration: const Duration(minutes: 1));
position.updateUserScrollDirection(ScrollDirection.forward); // ignore: INVALID_USE_OF_PROTECTED_MEMBER, since this is using a protected method for testing purposes position.updateUserScrollDirection(ScrollDirection.forward);
await tester.pumpAndSettle(const Duration(milliseconds: 1000)); await tester.pumpAndSettle(const Duration(milliseconds: 1000));
verifyPaintPosition(key1, const Offset(0.0, 0.0), false); verifyPaintPosition(key1, const Offset(0.0, 0.0), false);
verifyPaintPosition(key2, const Offset(0.0, 0.0), true); verifyPaintPosition(key2, const Offset(0.0, 0.0), true);
......
...@@ -53,12 +53,12 @@ class TestScrollPhysics extends ClampingScrollPhysics { ...@@ -53,12 +53,12 @@ class TestScrollPhysics extends ClampingScrollPhysics {
Tolerance get tolerance => const Tolerance(velocity: 20.0, distance: 1.0); Tolerance get tolerance => const Tolerance(velocity: 20.0, distance: 1.0);
} }
class TestViewportScrollPosition extends ScrollPosition { class TestViewportScrollPosition extends ScrollPositionWithSingleContext {
TestViewportScrollPosition({ TestViewportScrollPosition({
ScrollPhysics physics, ScrollPhysics physics,
AbstractScrollState state, ScrollContext context,
ScrollPosition oldPosition, ScrollPosition oldPosition,
}) : super(physics: physics, state: state, oldPosition: oldPosition); }) : super(physics: physics, context: context, oldPosition: oldPosition);
@override @override
bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) { bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
......
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