mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
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.
This commit is contained in:
parent
57648ba0e4
commit
c288c7064f
@ -411,7 +411,7 @@ class _AnimationDemoHomeState extends State<AnimationDemoHome> {
|
||||
if (notification.depth == 0 && notification is ScrollUpdateNotification) {
|
||||
selectedIndex.value = 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;
|
||||
}
|
||||
|
@ -87,7 +87,7 @@ class ContactsDemo extends StatefulWidget {
|
||||
ContactsDemoState createState() => new ContactsDemoState();
|
||||
}
|
||||
|
||||
enum AppBarBehavior { normal, pinned, floating }
|
||||
enum AppBarBehavior { normal, pinned, floating, snapping }
|
||||
|
||||
class ContactsDemoState extends State<ContactsDemo> {
|
||||
static final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
|
||||
@ -110,7 +110,8 @@ class ContactsDemoState extends State<ContactsDemo> {
|
||||
new SliverAppBar(
|
||||
expandedHeight: _appBarHeight,
|
||||
pinned: _appBarBehavior == AppBarBehavior.pinned,
|
||||
floating: _appBarBehavior == AppBarBehavior.floating,
|
||||
floating: _appBarBehavior == AppBarBehavior.floating || _appBarBehavior == AppBarBehavior.snapping,
|
||||
snap: _appBarBehavior == AppBarBehavior.snapping,
|
||||
actions: <Widget>[
|
||||
new IconButton(
|
||||
icon: const Icon(Icons.create),
|
||||
@ -140,6 +141,10 @@ class ContactsDemoState extends State<ContactsDemo> {
|
||||
value: AppBarBehavior.floating,
|
||||
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 {
|
||||
@required this.flexibleSpace,
|
||||
@required this.bottom,
|
||||
@required this.elevation,
|
||||
@required this.forceElevated,
|
||||
@required this.backgroundColor,
|
||||
@required this.brightness,
|
||||
@required this.iconTheme,
|
||||
@ -565,6 +566,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
|
||||
final Widget flexibleSpace;
|
||||
final PreferredSizeWidget bottom;
|
||||
final int elevation;
|
||||
final bool forceElevated;
|
||||
final Color backgroundColor;
|
||||
final Brightness brightness;
|
||||
final IconThemeData iconTheme;
|
||||
@ -604,7 +606,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
|
||||
actions: actions,
|
||||
flexibleSpace: flexibleSpace,
|
||||
bottom: bottom,
|
||||
elevation: overlapsContent || (pinned && shrinkOffset > maxExtent - minExtent) ? elevation ?? 4 : 0,
|
||||
elevation: forceElevated || overlapsContent || (pinned && shrinkOffset > maxExtent - minExtent) ? elevation ?? 4 : 0,
|
||||
backgroundColor: backgroundColor,
|
||||
brightness: brightness,
|
||||
iconTheme: iconTheme,
|
||||
@ -685,6 +687,7 @@ class SliverAppBar extends StatefulWidget {
|
||||
this.flexibleSpace,
|
||||
this.bottom,
|
||||
this.elevation,
|
||||
this.forceElevated: false,
|
||||
this.backgroundColor,
|
||||
this.brightness,
|
||||
this.iconTheme,
|
||||
@ -695,11 +698,13 @@ class SliverAppBar extends StatefulWidget {
|
||||
this.floating: false,
|
||||
this.pinned: false,
|
||||
this.snap: false,
|
||||
}) : assert(primary != null),
|
||||
}) : assert(forceElevated != null),
|
||||
assert(primary != null),
|
||||
assert(floating != 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(floating || !snap, 'The "snap" argument only makes sense for floating app bars.'),
|
||||
super(key: key);
|
||||
|
||||
/// A widget to display before the [title].
|
||||
@ -765,17 +770,30 @@ class SliverAppBar extends StatefulWidget {
|
||||
/// * [PreferredSize], which can be used to give an arbitrary widget a preferred size.
|
||||
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
|
||||
///
|
||||
/// Defaults to 4, the appropriate elevation for app bars.
|
||||
///
|
||||
/// The elevation is ignored when the app bar has no content underneath it.
|
||||
/// For example, if the app bar is [pinned] but no content is scrolled under
|
||||
/// it, or if it scrolls with the content.
|
||||
/// If [forceElevated] is false, the elevation is ignored when the app bar has
|
||||
/// no content underneath it. For example, if the app bar is [pinned] but no
|
||||
/// 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;
|
||||
|
||||
/// 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
|
||||
/// along with [brightness], [iconTheme], [textTheme].
|
||||
///
|
||||
@ -829,12 +847,9 @@ class SliverAppBar extends StatefulWidget {
|
||||
/// Otherwise, the user will need to scroll near the top of the scroll view to
|
||||
/// 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 dismisses the app bar, the animation will slide it completely
|
||||
/// out of view.
|
||||
/// 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
|
||||
/// dismisses the app bar, the animation will slide it completely out of view.
|
||||
final bool floating;
|
||||
|
||||
/// 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
|
||||
flexibleSpace: widget.flexibleSpace,
|
||||
bottom: widget.bottom,
|
||||
elevation: widget.elevation,
|
||||
forceElevated: widget.forceElevated,
|
||||
backgroundColor: widget.backgroundColor,
|
||||
brightness: widget.brightness,
|
||||
iconTheme: widget.iconTheme,
|
||||
|
@ -177,14 +177,14 @@ class RefreshIndicatorState extends State<RefreshIndicator> with TickerProviderS
|
||||
if (notification.depth != 0)
|
||||
return false;
|
||||
if (notification is ScrollStartNotification && notification.metrics.extentBefore == 0.0 &&
|
||||
_mode == null && _start(notification.axisDirection)) {
|
||||
_mode == null && _start(notification.metrics.axisDirection)) {
|
||||
setState(() {
|
||||
_mode = _RefreshIndicatorMode.drag;
|
||||
});
|
||||
return false;
|
||||
}
|
||||
bool indicatorAtTopNow;
|
||||
switch (notification.axisDirection) {
|
||||
switch (notification.metrics.axisDirection) {
|
||||
case AxisDirection.down:
|
||||
indicatorAtTopNow = true;
|
||||
break;
|
||||
|
@ -55,7 +55,7 @@ class _ScrollbarState extends State<Scrollbar> with TickerProviderStateMixin {
|
||||
bool _handleScrollNotification(ScrollNotification notification) {
|
||||
if (notification is ScrollUpdateNotification ||
|
||||
notification is OverscrollNotification)
|
||||
_controller.update(notification.metrics, notification.axisDirection);
|
||||
_controller.update(notification.metrics, notification.metrics.axisDirection);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
// only compute the scroll position's initial scroll offset (the "correct"
|
||||
// pixels value) after the TabBar viewport width and scroll limits are known.
|
||||
class _TabBarScrollPosition extends ScrollPosition {
|
||||
class _TabBarScrollPosition extends ScrollPositionWithSingleContext {
|
||||
_TabBarScrollPosition({
|
||||
ScrollPhysics physics,
|
||||
AbstractScrollState state,
|
||||
ScrollContext context,
|
||||
ScrollPosition oldPosition,
|
||||
this.tabBar,
|
||||
}) : super(
|
||||
physics: physics,
|
||||
state: state,
|
||||
context: context,
|
||||
initialPixels: null,
|
||||
oldPosition: oldPosition,
|
||||
);
|
||||
@ -372,10 +372,10 @@ class _TabBarScrollController extends ScrollController {
|
||||
final _TabBarState tabBar;
|
||||
|
||||
@override
|
||||
ScrollPosition createScrollPosition(ScrollPhysics physics, AbstractScrollState state, ScrollPosition oldPosition) {
|
||||
ScrollPosition createScrollPosition(ScrollPhysics physics, ScrollContext context, ScrollPosition oldPosition) {
|
||||
return new _TabBarScrollPosition(
|
||||
physics: physics,
|
||||
state: state,
|
||||
context: context,
|
||||
oldPosition: oldPosition,
|
||||
tabBar: tabBar,
|
||||
);
|
||||
|
@ -109,6 +109,24 @@ AxisDirection flipAxisDirection(AxisDirection axisDirection) {
|
||||
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].
|
||||
///
|
||||
/// Specifically, returns `axisDirection` if `growthDirection` is
|
||||
|
@ -164,7 +164,7 @@ abstract class ViewportOffset extends ChangeNotifier {
|
||||
String toString() {
|
||||
final List<String> description = <String>[];
|
||||
debugFillDescription(description);
|
||||
return '$runtimeType(${description.join(", ")})';
|
||||
return '$runtimeType#$hashCode(${description.join(", ")})';
|
||||
}
|
||||
|
||||
/// Add additional information to the given description for use by [toString].
|
||||
|
@ -1129,7 +1129,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
|
||||
onPointerUp: _handlePointerUpOrCancel,
|
||||
onPointerCancel: _handlePointerUpOrCancel,
|
||||
child: new AbsorbPointer(
|
||||
absorbing: false,
|
||||
absorbing: false, // it's mutated directly by _cancelActivePointers above
|
||||
child: new FocusScope(
|
||||
node: focusScopeNode,
|
||||
autofocus: true,
|
||||
|
@ -152,7 +152,7 @@ class _GlowingOverscrollIndicatorState extends State<GlowingOverscrollIndicator>
|
||||
_accepted[isLeading] = confirmationNotification._accepted;
|
||||
}
|
||||
assert(controller != null);
|
||||
assert(notification.axis == widget.axis);
|
||||
assert(notification.metrics.axis == widget.axis);
|
||||
if (_accepted[isLeading]) {
|
||||
if (notification.velocity != 0.0) {
|
||||
assert(notification.dragDetails == null);
|
||||
@ -166,7 +166,7 @@ class _GlowingOverscrollIndicatorState extends State<GlowingOverscrollIndicator>
|
||||
assert(renderer.hasSize);
|
||||
final Size size = renderer.size;
|
||||
final Offset position = renderer.globalToLocal(notification.dragDetails.globalPosition);
|
||||
switch (notification.axis) {
|
||||
switch (notification.metrics.axis) {
|
||||
case Axis.horizontal:
|
||||
controller.pull(notification.overscroll.abs(), size.width, position.dy.clamp(0.0, size.height), size.height);
|
||||
break;
|
||||
|
@ -12,10 +12,13 @@ import 'package:flutter/rendering.dart';
|
||||
import 'basic.dart';
|
||||
import 'framework.dart';
|
||||
import 'notification_listener.dart';
|
||||
import 'scroll_context.dart';
|
||||
import 'scroll_controller.dart';
|
||||
import 'scroll_metrics.dart';
|
||||
import 'scroll_notification.dart';
|
||||
import 'scroll_physics.dart';
|
||||
import 'scroll_position.dart';
|
||||
import 'scroll_position_with_single_context.dart';
|
||||
import 'scroll_view.dart';
|
||||
import 'scrollable.dart';
|
||||
import 'sliver.dart';
|
||||
@ -70,7 +73,11 @@ class PageController extends ScrollController {
|
||||
@required Curve curve,
|
||||
}) {
|
||||
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].
|
||||
@ -105,10 +112,10 @@ class PageController extends ScrollController {
|
||||
}
|
||||
|
||||
@override
|
||||
ScrollPosition createScrollPosition(ScrollPhysics physics, AbstractScrollState state, ScrollPosition oldPosition) {
|
||||
ScrollPosition createScrollPosition(ScrollPhysics physics, ScrollContext context, ScrollPosition oldPosition) {
|
||||
return new _PagePosition(
|
||||
physics: physics,
|
||||
state: state,
|
||||
context: context,
|
||||
initialPage: initialPage,
|
||||
viewportFraction: viewportFraction,
|
||||
oldPosition: oldPosition,
|
||||
@ -127,7 +134,7 @@ class PageController extends ScrollController {
|
||||
///
|
||||
/// The metrics are available on [ScrollNotification]s generated from
|
||||
/// [PageView]s.
|
||||
class PageMetrics extends ScrollMetrics {
|
||||
class PageMetrics extends FixedScrollMetrics {
|
||||
/// Creates page metrics that add the given information to the `parent`
|
||||
/// metrics.
|
||||
PageMetrics({
|
||||
@ -139,16 +146,16 @@ class PageMetrics extends ScrollMetrics {
|
||||
final double page;
|
||||
}
|
||||
|
||||
class _PagePosition extends ScrollPosition {
|
||||
class _PagePosition extends ScrollPositionWithSingleContext {
|
||||
_PagePosition({
|
||||
ScrollPhysics physics,
|
||||
AbstractScrollState state,
|
||||
ScrollContext context,
|
||||
this.initialPage: 0,
|
||||
double viewportFraction: 1.0,
|
||||
ScrollPosition oldPosition,
|
||||
}) : _viewportFraction = viewportFraction, super(
|
||||
physics: physics,
|
||||
state: state,
|
||||
context: context,
|
||||
initialPixels: null,
|
||||
oldPosition: oldPosition,
|
||||
) {
|
||||
@ -167,7 +174,7 @@ class _PagePosition extends ScrollPosition {
|
||||
final double oldPage = page;
|
||||
_viewportFraction = value;
|
||||
if (oldPage != null)
|
||||
correctPixels(getPixelsFromPage(oldPage));
|
||||
forcePixels(getPixelsFromPage(oldPage));
|
||||
}
|
||||
|
||||
double getPageFromPixels(double pixels, double viewportDimension) {
|
||||
@ -195,9 +202,9 @@ class _PagePosition extends ScrollPosition {
|
||||
}
|
||||
|
||||
@override
|
||||
PageMetrics getMetrics() {
|
||||
PageMetrics cloneMetrics() {
|
||||
return new PageMetrics(
|
||||
parent: super.getMetrics(),
|
||||
parent: this,
|
||||
page: page,
|
||||
);
|
||||
}
|
||||
@ -235,7 +242,7 @@ class PageScrollPhysics extends ScrollPhysics {
|
||||
}
|
||||
|
||||
@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
|
||||
// ballistics, which should put us back in range at a page boundary.
|
||||
if ((velocity <= 0.0 && position.pixels <= position.minScrollExtent) ||
|
||||
@ -243,7 +250,9 @@ class PageScrollPhysics extends ScrollPhysics {
|
||||
return super.createBallisticSimulation(position, velocity);
|
||||
final Tolerance tolerance = this.tolerance;
|
||||
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> {
|
||||
axisDirection: axisDirection,
|
||||
controller: widget.controller,
|
||||
physics: widget.physics == null ? _kPagePhysics : _kPagePhysics.applyTo(widget.physics),
|
||||
viewportBuilder: (BuildContext context, ViewportOffset offset) {
|
||||
viewportBuilder: (BuildContext context, ViewportOffset position) {
|
||||
return new Viewport(
|
||||
axisDirection: axisDirection,
|
||||
offset: offset,
|
||||
offset: position,
|
||||
slivers: <Widget>[
|
||||
new SliverFillViewport(
|
||||
viewportFraction: widget.controller.viewportFraction,
|
||||
|
@ -34,6 +34,6 @@ class PrimaryScrollController extends InheritedWidget {
|
||||
@override
|
||||
void debugFillDescription(List<String> description) {
|
||||
super.debugFillDescription(description);
|
||||
description.add('${controller ?? 'no controller'}');
|
||||
description.add('${controller ?? "no controller"}');
|
||||
}
|
||||
}
|
||||
|
351
packages/flutter/lib/src/widgets/scroll_activity.dart
Normal file
351
packages/flutter/lib/src/widgets/scroll_activity.dart
Normal file
@ -0,0 +1,351 @@
|
||||
// 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)';
|
||||
}
|
||||
}
|
18
packages/flutter/lib/src/widgets/scroll_context.dart
Normal file
18
packages/flutter/lib/src/widgets/scroll_context.dart
Normal file
@ -0,0 +1,18 @@
|
||||
// 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';
|
||||
import 'package:flutter/animation.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'scroll_context.dart';
|
||||
import 'scroll_physics.dart';
|
||||
import 'scroll_position.dart';
|
||||
import 'scroll_position_with_single_context.dart';
|
||||
|
||||
class ScrollController extends ChangeNotifier {
|
||||
ScrollController({
|
||||
@ -22,6 +25,12 @@ class ScrollController extends ChangeNotifier {
|
||||
/// controller will have their offset initialized to this value.
|
||||
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>[];
|
||||
|
||||
/// Whether any [ScrollPosition] objects have attached themselves to the
|
||||
@ -32,6 +41,10 @@ class ScrollController extends ChangeNotifier {
|
||||
/// called.
|
||||
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 {
|
||||
assert(_positions.isNotEmpty, 'ScrollController not attached to any scroll views.');
|
||||
assert(_positions.length == 1, 'ScrollController attached to multiple scroll views.');
|
||||
@ -71,7 +84,7 @@ class ScrollController extends ChangeNotifier {
|
||||
}) {
|
||||
assert(_positions.isNotEmpty, 'ScrollController not attached to any scroll views.');
|
||||
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);
|
||||
return Future.wait<Null>(animations).then((List<Null> _) => null);
|
||||
}
|
||||
@ -121,18 +134,22 @@ class ScrollController extends ChangeNotifier {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
static ScrollPosition createDefaultScrollPosition(ScrollPhysics physics, AbstractScrollState state, ScrollPosition oldPosition) {
|
||||
return new ScrollPosition(
|
||||
static ScrollPosition createDefaultScrollPosition(ScrollPhysics physics, ScrollContext context, ScrollPosition oldPosition) {
|
||||
return new ScrollPositionWithSingleContext(
|
||||
physics: physics,
|
||||
state: state,
|
||||
context: context,
|
||||
oldPosition: oldPosition,
|
||||
);
|
||||
}
|
||||
|
||||
ScrollPosition createScrollPosition(ScrollPhysics physics, AbstractScrollState state, ScrollPosition oldPosition) {
|
||||
return new ScrollPosition(
|
||||
ScrollPosition createScrollPosition(
|
||||
ScrollPhysics physics,
|
||||
ScrollContext context,
|
||||
ScrollPosition oldPosition,
|
||||
) {
|
||||
return new ScrollPositionWithSingleContext(
|
||||
physics: physics,
|
||||
state: state,
|
||||
context: context,
|
||||
initialPixels: initialScrollOffset,
|
||||
oldPosition: oldPosition,
|
||||
);
|
||||
@ -140,18 +157,22 @@ class ScrollController extends ChangeNotifier {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final StringBuffer result = new StringBuffer();
|
||||
result.write('$runtimeType#$hashCode(');
|
||||
final List<String> description = <String>[];
|
||||
debugFillDescription(description);
|
||||
return '$runtimeType#$hashCode(${description.join(", ")})';
|
||||
}
|
||||
|
||||
@mustCallSuper
|
||||
void debugFillDescription(List<String> description) {
|
||||
if (initialScrollOffset != 0.0)
|
||||
result.write('initialScrollOffset: ${initialScrollOffset.toStringAsFixed(1)}, ');
|
||||
description.add('initialScrollOffset: ${initialScrollOffset.toStringAsFixed(1)}, ');
|
||||
if (_positions.isEmpty) {
|
||||
result.write('no clients');
|
||||
description.add('no clients');
|
||||
} 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 {
|
||||
result.write('${_positions.length} clients');
|
||||
description.add('${_positions.length} clients');
|
||||
}
|
||||
result.write(')');
|
||||
return result.toString();
|
||||
}
|
||||
}
|
||||
|
106
packages/flutter/lib/src/widgets/scroll_metrics.dart
Normal file
106
packages/flutter/lib/src/widgets/scroll_metrics.dart
Normal file
@ -0,0 +1,106 @@
|
||||
// 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;
|
||||
}
|
@ -6,62 +6,9 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
import 'basic.dart';
|
||||
import 'framework.dart';
|
||||
import 'notification_listener.dart';
|
||||
import 'scrollable.dart' show Scrollable, ScrollableState;
|
||||
|
||||
/// 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)}})';
|
||||
}
|
||||
}
|
||||
import 'scroll_metrics.dart';
|
||||
|
||||
/// Mixin for [Notification]s that track how many [RenderAbstractViewport] they
|
||||
/// have bubbled through.
|
||||
@ -95,19 +42,13 @@ abstract class ViewportNotificationMixin extends Notification {
|
||||
abstract class ScrollNotification extends LayoutChangedNotification with ViewportNotificationMixin {
|
||||
/// Creates a notification about scrolling.
|
||||
ScrollNotification({
|
||||
@required ScrollableState scrollable,
|
||||
}) : axisDirection = scrollable.widget.axisDirection,
|
||||
metrics = scrollable.position.getMetrics(),
|
||||
context = scrollable.context;
|
||||
|
||||
/// The direction that positive scroll offsets indicate.
|
||||
final AxisDirection axisDirection;
|
||||
|
||||
Axis get axis => axisDirectionToAxis(axisDirection);
|
||||
@required this.metrics,
|
||||
@required this.context,
|
||||
});
|
||||
|
||||
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
|
||||
/// size of the viewport, for instance.
|
||||
@ -116,16 +57,16 @@ abstract class ScrollNotification extends LayoutChangedNotification with Viewpor
|
||||
@override
|
||||
void debugFillDescription(List<String> description) {
|
||||
super.debugFillDescription(description);
|
||||
description.add('$axisDirection');
|
||||
description.add('metrics: $metrics');
|
||||
description.add('$metrics');
|
||||
}
|
||||
}
|
||||
|
||||
class ScrollStartNotification extends ScrollNotification {
|
||||
ScrollStartNotification({
|
||||
@required ScrollableState scrollable,
|
||||
@required ScrollMetrics metrics,
|
||||
@required BuildContext context,
|
||||
this.dragDetails,
|
||||
}) : super(scrollable: scrollable);
|
||||
}) : super(metrics: metrics, context: context);
|
||||
|
||||
final DragStartDetails dragDetails;
|
||||
|
||||
@ -139,10 +80,11 @@ class ScrollStartNotification extends ScrollNotification {
|
||||
|
||||
class ScrollUpdateNotification extends ScrollNotification {
|
||||
ScrollUpdateNotification({
|
||||
@required ScrollableState scrollable,
|
||||
@required ScrollMetrics metrics,
|
||||
@required BuildContext context,
|
||||
this.dragDetails,
|
||||
this.scrollDelta,
|
||||
}) : super(scrollable: scrollable);
|
||||
}) : super(metrics: metrics, context: context);
|
||||
|
||||
final DragUpdateDetails dragDetails;
|
||||
|
||||
@ -160,11 +102,12 @@ class ScrollUpdateNotification extends ScrollNotification {
|
||||
|
||||
class OverscrollNotification extends ScrollNotification {
|
||||
OverscrollNotification({
|
||||
@required ScrollableState scrollable,
|
||||
@required ScrollMetrics metrics,
|
||||
@required BuildContext context,
|
||||
this.dragDetails,
|
||||
@required this.overscroll,
|
||||
this.velocity: 0.0,
|
||||
}) : super(scrollable: scrollable) {
|
||||
}) : super(metrics: metrics, context: context) {
|
||||
assert(overscroll != null);
|
||||
assert(overscroll.isFinite);
|
||||
assert(overscroll != 0.0);
|
||||
@ -199,9 +142,10 @@ class OverscrollNotification extends ScrollNotification {
|
||||
|
||||
class ScrollEndNotification extends ScrollNotification {
|
||||
ScrollEndNotification({
|
||||
@required ScrollableState scrollable,
|
||||
@required ScrollMetrics metrics,
|
||||
@required BuildContext context,
|
||||
this.dragDetails,
|
||||
}) : super(scrollable: scrollable);
|
||||
}) : super(metrics: metrics, context: context);
|
||||
|
||||
final DragEndDetails dragDetails;
|
||||
|
||||
@ -215,9 +159,10 @@ class ScrollEndNotification extends ScrollNotification {
|
||||
|
||||
class UserScrollNotification extends ScrollNotification {
|
||||
UserScrollNotification({
|
||||
@required ScrollableState scrollable,
|
||||
@required ScrollMetrics metrics,
|
||||
@required BuildContext context,
|
||||
this.direction,
|
||||
}) : super(scrollable: scrollable);
|
||||
}) : super(metrics: metrics, context: context);
|
||||
|
||||
final ScrollDirection direction;
|
||||
|
||||
|
@ -3,17 +3,160 @@
|
||||
// found in the LICENSE file.
|
||||
|
||||
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 'overscroll_indicator.dart';
|
||||
import 'scroll_position.dart';
|
||||
import 'scroll_metrics.dart';
|
||||
import 'scroll_simulation.dart';
|
||||
|
||||
// The ScrollPhysics base class is defined in scroll_position.dart because it
|
||||
// has as circular dependency with ScrollPosition.
|
||||
export 'scroll_position.dart' show ScrollPhysics;
|
||||
export 'package:flutter/physics.dart' show Tolerance;
|
||||
|
||||
@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
|
||||
/// the bounds of the content, but then bounce the content back to the edge of
|
||||
@ -42,7 +185,7 @@ class BouncingScrollPhysics extends ScrollPhysics {
|
||||
double get frictionFactor => 0.5;
|
||||
|
||||
@override
|
||||
double applyPhysicsToUserOffset(ScrollPosition position, double offset) {
|
||||
double applyPhysicsToUserOffset(ScrollMetrics position, double offset) {
|
||||
assert(offset != 0.0);
|
||||
assert(position.minScrollExtent <= position.maxScrollExtent);
|
||||
if (offset > 0.0)
|
||||
@ -66,10 +209,10 @@ class BouncingScrollPhysics extends ScrollPhysics {
|
||||
}
|
||||
|
||||
@override
|
||||
double applyBoundaryConditions(ScrollPosition position, double value) => 0.0;
|
||||
double applyBoundaryConditions(ScrollMetrics position, double value) => 0.0;
|
||||
|
||||
@override
|
||||
Simulation createBallisticSimulation(ScrollPosition position, double velocity) {
|
||||
Simulation createBallisticSimulation(ScrollMetrics position, double velocity) {
|
||||
final Tolerance tolerance = this.tolerance;
|
||||
if (velocity.abs() >= tolerance.velocity || position.outOfRange) {
|
||||
return new BouncingScrollSimulation(
|
||||
@ -78,7 +221,8 @@ class BouncingScrollPhysics extends ScrollPhysics {
|
||||
velocity: velocity * 0.91, // TODO(abarth): We should move this constant closer to the drag end.
|
||||
leadingExtent: position.minScrollExtent,
|
||||
trailingExtent: position.maxScrollExtent,
|
||||
)..tolerance = tolerance;
|
||||
tolerance: tolerance,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@ -113,8 +257,23 @@ class ClampingScrollPhysics extends ScrollPhysics {
|
||||
ClampingScrollPhysics applyTo(ScrollPhysics parent) => new ClampingScrollPhysics(parent: parent);
|
||||
|
||||
@override
|
||||
double applyBoundaryConditions(ScrollPosition position, double value) {
|
||||
assert(value != position.pixels);
|
||||
double applyBoundaryConditions(ScrollMetrics position, double value) {
|
||||
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
|
||||
return value - position.pixels;
|
||||
if (position.maxScrollExtent <= position.pixels && position.pixels < value) // overscroll
|
||||
@ -127,7 +286,7 @@ class ClampingScrollPhysics extends ScrollPhysics {
|
||||
}
|
||||
|
||||
@override
|
||||
Simulation createBallisticSimulation(ScrollPosition position, double velocity) {
|
||||
Simulation createBallisticSimulation(ScrollMetrics position, double velocity) {
|
||||
final Tolerance tolerance = this.tolerance;
|
||||
if (position.outOfRange) {
|
||||
double end;
|
||||
@ -144,14 +303,17 @@ class ClampingScrollPhysics extends ScrollPhysics {
|
||||
tolerance: tolerance
|
||||
);
|
||||
}
|
||||
if (!position.atEdge && velocity.abs() >= tolerance.velocity) {
|
||||
return new ClampingScrollSimulation(
|
||||
position: position.pixels,
|
||||
velocity: velocity,
|
||||
tolerance: tolerance,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
if (velocity.abs() < tolerance.velocity)
|
||||
return null;
|
||||
if (velocity > 0.0 && position.pixels >= position.maxScrollExtent)
|
||||
return null;
|
||||
if (velocity < 0.0 && position.pixels <= position.minScrollExtent)
|
||||
return null;
|
||||
return new ClampingScrollSimulation(
|
||||
position: position.pixels,
|
||||
velocity: velocity,
|
||||
tolerance: tolerance,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -175,5 +337,5 @@ class AlwaysScrollableScrollPhysics extends ScrollPhysics {
|
||||
AlwaysScrollableScrollPhysics applyTo(ScrollPhysics parent) => new AlwaysScrollableScrollPhysics(parent: parent);
|
||||
|
||||
@override
|
||||
bool shouldAcceptUserOffset(ScrollPosition position) => true;
|
||||
bool shouldAcceptUserOffset(ScrollMetrics position) => true;
|
||||
}
|
||||
|
@ -3,278 +3,70 @@
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui' as ui show window;
|
||||
|
||||
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 'notification_listener.dart';
|
||||
import 'scroll_notification.dart';
|
||||
import 'scrollable.dart';
|
||||
import 'ticker_provider.dart';
|
||||
import 'scroll_metrics.dart';
|
||||
import 'scroll_physics.dart';
|
||||
|
||||
export 'package:flutter/physics.dart' show Tolerance;
|
||||
|
||||
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 {
|
||||
abstract class ScrollPosition extends ViewportOffset with ScrollMetrics {
|
||||
ScrollPosition({
|
||||
@required this.physics,
|
||||
@required this.state,
|
||||
double initialPixels: 0.0,
|
||||
ScrollPosition oldPosition,
|
||||
}) : _pixels = initialPixels {
|
||||
assert(physics != null);
|
||||
assert(state != null);
|
||||
assert(state.vsync != null);
|
||||
}) {
|
||||
if (oldPosition != null)
|
||||
absorb(oldPosition);
|
||||
if (activity == null)
|
||||
beginIdleActivity();
|
||||
assert(activity != null);
|
||||
assert(activity.position == this);
|
||||
}
|
||||
|
||||
final ScrollPhysics physics;
|
||||
|
||||
final AbstractScrollState state;
|
||||
@override
|
||||
double get minScrollExtent => _minScrollExtent;
|
||||
double _minScrollExtent;
|
||||
|
||||
@override
|
||||
double get maxScrollExtent => _maxScrollExtent;
|
||||
double _maxScrollExtent;
|
||||
|
||||
@override
|
||||
double get pixels => _pixels;
|
||||
double _pixels;
|
||||
|
||||
Future<Null> ensureVisible(RenderObject object, {
|
||||
double alignment: 0.0,
|
||||
Duration duration: Duration.ZERO,
|
||||
Curve curve: Curves.ease,
|
||||
}) {
|
||||
assert(object.attached);
|
||||
final RenderAbstractViewport viewport = RenderAbstractViewport.of(object);
|
||||
assert(viewport != null);
|
||||
@override
|
||||
double get viewportDimension => _viewportDimension;
|
||||
double _viewportDimension;
|
||||
|
||||
final double target = viewport.getOffsetToReveal(object, alignment).clamp(minScrollExtent, maxScrollExtent);
|
||||
/// Whether [viewportDimension], [minScrollExtent], [maxScrollExtent],
|
||||
/// [outOfRange], and [atEdge] are available yet.
|
||||
///
|
||||
/// Set to true just before the first time that [applyNewDimensions] is
|
||||
/// called.
|
||||
bool get haveDimensions => _haveDimensions;
|
||||
bool _haveDimensions = false;
|
||||
|
||||
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.
|
||||
/// Take any current applicable state from the given [ScrollPosition].
|
||||
///
|
||||
/// Any active animation is canceled. If the user is currently scrolling, that
|
||||
/// action is canceled.
|
||||
/// This method is called by the constructor if it is given an `oldPosition`.
|
||||
///
|
||||
/// 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,
|
||||
/// 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.
|
||||
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].
|
||||
///
|
||||
/// Accurately describing the metrics typicaly requires using information
|
||||
/// 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
|
||||
/// in consistent units (so that they can be compared over time or used to
|
||||
/// drive diagrammatic user interfaces such as scrollbars).
|
||||
ScrollMetrics getMetrics() {
|
||||
return new ScrollMetrics(
|
||||
extentBefore: math.max(pixels - minScrollExtent, 0.0),
|
||||
extentInside: math.min(pixels, maxScrollExtent) - math.max(pixels, minScrollExtent) + math.min(viewportDimension, maxScrollExtent - minScrollExtent),
|
||||
extentAfter: math.max(maxScrollExtent - pixels, 0.0),
|
||||
viewportDimension: viewportDimension,
|
||||
);
|
||||
/// This method can be 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).
|
||||
@protected
|
||||
@mustCallSuper
|
||||
void absorb(ScrollPosition other) {
|
||||
assert(other != null);
|
||||
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.
|
||||
@ -289,47 +81,65 @@ class ScrollPosition extends ViewportOffset {
|
||||
/// greater than the requested `value` by the given amount (underscroll past
|
||||
/// the min extent).
|
||||
///
|
||||
/// Implementations of this method must dispatch scroll update notifications
|
||||
/// (using [dispatchNotification] and
|
||||
/// [ScrollActivity.createScrollUpdateNotification]) after applying the new
|
||||
/// value (so after [pixels] changes). If the entire change is not applied,
|
||||
/// the overscroll should be reported by subsequently also dispatching an
|
||||
/// overscroll notification using
|
||||
/// [ScrollActivity.createOverscrollNotification].
|
||||
double setPixels(double value) {
|
||||
/// The amount of overscroll is computed by [applyBoundaryConditions].
|
||||
///
|
||||
/// The amount of the change that is applied is reported using [didUpdateScrollPositionBy].
|
||||
/// If there is any overscroll, it is reported using [didOverscrollBy].
|
||||
double setPixels(double newPixels) {
|
||||
assert(_pixels != null);
|
||||
assert(SchedulerBinding.instance.schedulerPhase.index <= SchedulerPhase.transientCallbacks.index);
|
||||
assert(activity.isScrolling);
|
||||
if (value != pixels) {
|
||||
final double overScroll = physics.applyBoundaryConditions(this, value);
|
||||
if (newPixels != pixels) {
|
||||
final double overScroll = applyBoundaryConditions(newPixels);
|
||||
assert(() {
|
||||
final double delta = value - pixels;
|
||||
final double delta = newPixels - pixels;
|
||||
if (overScroll.abs() > delta.abs()) {
|
||||
throw new FlutterError(
|
||||
'${physics.runtimeType}.applyBoundaryConditions returned invalid overscroll value.\n'
|
||||
'setPixels() was called to change the scroll offset from $pixels to $value.\n'
|
||||
'$runtimeType.applyBoundaryConditions returned invalid overscroll value.\n'
|
||||
'setPixels() was called to change the scroll offset from $pixels to $newPixels.\n'
|
||||
'That is a delta of $delta units.\n'
|
||||
'${physics.runtimeType}.applyBoundaryConditions reported an overscroll of $overScroll units.\n'
|
||||
'The scroll extents are $minScrollExtent .. $maxScrollExtent, and the '
|
||||
'viewport dimension is $viewportDimension.'
|
||||
'$runtimeType.applyBoundaryConditions reported an overscroll of $overScroll units.'
|
||||
);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
final double oldPixels = _pixels;
|
||||
_pixels = value - overScroll;
|
||||
_pixels = newPixels - overScroll;
|
||||
if (_pixels != oldPixels) {
|
||||
notifyListeners();
|
||||
state.dispatchNotification(activity.createScrollUpdateNotification(state, _pixels - oldPixels));
|
||||
didUpdateScrollPositionBy(_pixels - oldPixels);
|
||||
}
|
||||
if (overScroll != 0.0) {
|
||||
reportOverscroll(overScroll);
|
||||
didOverscrollBy(overScroll);
|
||||
return overScroll;
|
||||
}
|
||||
}
|
||||
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) {
|
||||
_pixels = value;
|
||||
}
|
||||
@ -339,24 +149,55 @@ class ScrollPosition extends ViewportOffset {
|
||||
_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
|
||||
void reportOverscroll(double value) {
|
||||
assert(activity.isScrolling);
|
||||
state.dispatchNotification(activity.createOverscrollNotification(state, value));
|
||||
void forcePixels(double value) {
|
||||
assert(_pixels != null);
|
||||
_pixels = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
double get viewportDimension => _viewportDimension;
|
||||
double _viewportDimension;
|
||||
|
||||
double get minScrollExtent => _minScrollExtent;
|
||||
double _minScrollExtent;
|
||||
|
||||
double get maxScrollExtent => _maxScrollExtent;
|
||||
double _maxScrollExtent;
|
||||
|
||||
bool get outOfRange => pixels < minScrollExtent || pixels > maxScrollExtent;
|
||||
|
||||
bool get atEdge => pixels == minScrollExtent || pixels == maxScrollExtent;
|
||||
@protected
|
||||
double applyBoundaryConditions(double value) {
|
||||
final double result = physics.applyBoundaryConditions(this, value);
|
||||
assert(() {
|
||||
final double delta = value - pixels;
|
||||
if (result.abs() > delta.abs()) {
|
||||
throw new FlutterError(
|
||||
'${physics.runtimeType}.applyBoundaryConditions returned invalid overscroll value.\n'
|
||||
'The method was called to consider a change from $pixels to $value, which is a '
|
||||
'delta of ${delta.toStringAsFixed(1)} units. However, it returned an overscroll of '
|
||||
'${result.toStringAsFixed(1)} units, which has a greater magnitude than the delta. '
|
||||
'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;
|
||||
|
||||
@ -379,94 +220,40 @@ class ScrollPosition extends ViewportOffset {
|
||||
_didChangeViewportDimension) {
|
||||
_minScrollExtent = minScrollExtent;
|
||||
_maxScrollExtent = maxScrollExtent;
|
||||
activity.applyNewDimensions();
|
||||
_haveDimensions = true;
|
||||
applyNewDimensions();
|
||||
_didChangeViewportDimension = false;
|
||||
}
|
||||
state.setCanDrag(physics.shouldAcceptUserOffset(this));
|
||||
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
|
||||
@mustCallSuper
|
||||
void absorb(ScrollPosition other) {
|
||||
assert(activity == null);
|
||||
assert(other != this);
|
||||
assert(other.state == state);
|
||||
assert(other.activity != null);
|
||||
void applyNewDimensions();
|
||||
|
||||
_pixels = other._pixels;
|
||||
_viewportDimension = other.viewportDimension;
|
||||
_minScrollExtent = other.minScrollExtent;
|
||||
_maxScrollExtent = other.maxScrollExtent;
|
||||
_userScrollDirection = other._userScrollDirection;
|
||||
/// Animates the position such that the given object is as visible as possible
|
||||
/// by just scrolling this position.
|
||||
Future<Null> ensureVisible(RenderObject object, {
|
||||
double alignment: 0.0,
|
||||
Duration duration: Duration.ZERO,
|
||||
Curve curve: Curves.ease,
|
||||
}) {
|
||||
assert(object.attached);
|
||||
final RenderAbstractViewport viewport = RenderAbstractViewport.of(object);
|
||||
assert(viewport != null);
|
||||
|
||||
final bool oldIgnorePointer = shouldIgnorePointer;
|
||||
other.activity._position = this;
|
||||
_activity = other.activity;
|
||||
other._activity = null;
|
||||
final double target = viewport.getOffsetToReveal(object, alignment).clamp(minScrollExtent, maxScrollExtent);
|
||||
|
||||
if (oldIgnorePointer != shouldIgnorePointer)
|
||||
state.setIgnorePointer(shouldIgnorePointer);
|
||||
if (target == pixels)
|
||||
return new Future<Null>.value();
|
||||
|
||||
if (other.runtimeType != runtimeType)
|
||||
activity.resetActivity();
|
||||
if (duration == Duration.ZERO) {
|
||||
jumpTo(target);
|
||||
return new Future<Null>.value();
|
||||
}
|
||||
|
||||
return animateTo(target, duration: duration, curve: curve);
|
||||
}
|
||||
|
||||
bool get shouldIgnorePointer => activity?.shouldIgnorePointer;
|
||||
|
||||
void touched() {
|
||||
_activity.touched();
|
||||
}
|
||||
|
||||
/// The direction that the user most recently began scrolling in.
|
||||
@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.
|
||||
///
|
||||
/// 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
|
||||
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
|
||||
/// position is idle.
|
||||
///
|
||||
@ -474,353 +261,31 @@ class ScrollPosition extends ViewportOffset {
|
||||
/// [State.dispose] method.
|
||||
final ValueNotifier<bool> isScrollingNotifier = new ValueNotifier<bool>(false);
|
||||
|
||||
/// 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.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));
|
||||
}
|
||||
Future<Null> animateTo(double to, {
|
||||
@required Duration duration,
|
||||
@required Curve curve,
|
||||
});
|
||||
|
||||
void beginIdleActivity() {
|
||||
beginActivity(new IdleScrollActivity(this));
|
||||
}
|
||||
void jumpTo(double value);
|
||||
|
||||
DragScrollActivity beginDragActivity(DragStartDetails details) {
|
||||
beginActivity(new DragScrollActivity(this, details));
|
||||
return activity;
|
||||
}
|
||||
/// Deprecated. Use [jumpTo] or a custom [ScrollPosition] instead.
|
||||
@Deprecated('This will lead to bugs.')
|
||||
void jumpToWithoutSettling(double value);
|
||||
|
||||
// ///
|
||||
// /// 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();
|
||||
}
|
||||
}
|
||||
void didTouch();
|
||||
|
||||
Drag drag(DragStartDetails details, VoidCallback dragCancelCallback);
|
||||
|
||||
@protected
|
||||
void didUpdateScrollPositionBy(double delta);
|
||||
|
||||
@protected
|
||||
void didOverscrollBy(double value);
|
||||
|
||||
@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 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;
|
||||
|
||||
double get velocity => _controller.velocity;
|
||||
|
||||
@override
|
||||
void touched() {
|
||||
position.beginIdleActivity();
|
||||
}
|
||||
|
||||
void _tick() {
|
||||
if (position.setPixels(_controller.value) != 0.0)
|
||||
position.beginIdleActivity();
|
||||
}
|
||||
|
||||
void _end() {
|
||||
position?.beginBallisticActivity(velocity);
|
||||
}
|
||||
|
||||
@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() {
|
||||
_completer.complete();
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '$runtimeType($_controller)';
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,398 @@
|
||||
// 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';
|
||||
import 'primary_scroll_controller.dart';
|
||||
import 'scroll_controller.dart';
|
||||
import 'scroll_physics.dart';
|
||||
import 'scroll_position.dart';
|
||||
import 'scrollable.dart';
|
||||
import 'sliver.dart';
|
||||
import 'viewport.dart';
|
||||
|
@ -14,8 +14,9 @@ import 'framework.dart';
|
||||
import 'gesture_detector.dart';
|
||||
import 'notification_listener.dart';
|
||||
import 'scroll_configuration.dart';
|
||||
import 'scroll_context.dart';
|
||||
import 'scroll_controller.dart';
|
||||
import 'scroll_notification.dart';
|
||||
import 'scroll_physics.dart';
|
||||
import 'scroll_position.dart';
|
||||
import 'ticker_provider.dart';
|
||||
import 'viewport.dart';
|
||||
@ -68,7 +69,8 @@ class Scrollable extends StatefulWidget {
|
||||
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, {
|
||||
double alignment: 0.0,
|
||||
Duration duration: Duration.ZERO,
|
||||
@ -91,7 +93,7 @@ class Scrollable extends StatefulWidget {
|
||||
if (futures.isEmpty || duration == Duration.ZERO)
|
||||
return new Future<Null>.value();
|
||||
if (futures.length == 1)
|
||||
return futures.first;
|
||||
return futures.single;
|
||||
return Future.wait<Null>(futures);
|
||||
}
|
||||
}
|
||||
@ -128,16 +130,18 @@ class _ScrollableScope extends InheritedWidget {
|
||||
/// This class is not intended to be subclassed. To specialize the behavior of a
|
||||
/// [Scrollable], provide it with a [ScrollPhysics].
|
||||
class ScrollableState extends State<Scrollable> with TickerProviderStateMixin
|
||||
implements AbstractScrollState {
|
||||
/// The controller for this [Scrollable] widget's viewport position.
|
||||
implements ScrollContext {
|
||||
/// The manager for this [Scrollable] widget's viewport position.
|
||||
///
|
||||
/// To control what kind of [ScrollPosition] is created for a [Scrollable],
|
||||
/// provide it with custom [ScrollPhysics] that creates the appropriate
|
||||
/// [ScrollPosition] controller in its [ScrollPhysics.createScrollPosition]
|
||||
/// method.
|
||||
/// provide it with custom [ScrollController] that creates the appropriate
|
||||
/// [ScrollPosition] in its [ScrollController.createScrollPosition] method.
|
||||
ScrollPosition get position => _position;
|
||||
ScrollPosition _position;
|
||||
|
||||
@override
|
||||
AxisDirection get axisDirection => widget.axisDirection;
|
||||
|
||||
ScrollBehavior _configuration;
|
||||
ScrollPhysics _physics;
|
||||
|
||||
@ -224,6 +228,7 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin
|
||||
..onStart = _handleDragStart
|
||||
..onUpdate = _handleDragUpdate
|
||||
..onEnd = _handleDragEnd
|
||||
..onCancel = _handleDragCancel
|
||||
..minFlingDistance = _physics?.minFlingDistance
|
||||
..minFlingVelocity = _physics?.minFlingVelocity
|
||||
..maxFlingVelocity = _physics?.maxFlingVelocity;
|
||||
@ -238,6 +243,7 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin
|
||||
..onStart = _handleDragStart
|
||||
..onUpdate = _handleDragUpdate
|
||||
..onEnd = _handleDragEnd
|
||||
..onCancel = _handleDragCancel
|
||||
..minFlingDistance = _physics?.minFlingDistance
|
||||
..minFlingVelocity = _physics?.minFlingVelocity
|
||||
..maxFlingVelocity = _physics?.maxFlingVelocity;
|
||||
@ -268,54 +274,41 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin
|
||||
}
|
||||
|
||||
@override
|
||||
@protected
|
||||
void dispatchNotification(Notification notification) {
|
||||
assert(mounted);
|
||||
notification.dispatch(_gestureDetectorKey.currentContext);
|
||||
}
|
||||
BuildContext get notificationContext => _gestureDetectorKey.currentContext;
|
||||
|
||||
// TOUCH HANDLERS
|
||||
|
||||
DragScrollActivity _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;
|
||||
}
|
||||
Drag _drag;
|
||||
|
||||
void _handleDragDown(DragDownDetails details) {
|
||||
assert(_drag == null);
|
||||
position.touched();
|
||||
position.didTouch();
|
||||
}
|
||||
|
||||
void _handleDragStart(DragStartDetails details) {
|
||||
assert(_drag == null);
|
||||
_drag = position.beginDragActivity(details);
|
||||
_drag = position.drag(details, _disposeDrag);
|
||||
assert(_drag != null);
|
||||
}
|
||||
|
||||
void _handleDragUpdate(DragUpdateDetails details) {
|
||||
// _drag might be null if the drag activity ended and called didEndDrag.
|
||||
_drag?.update(details, reverse: _reverseDirection);
|
||||
// _drag might be null if the drag activity ended and called _disposeDrag.
|
||||
_drag?.update(details);
|
||||
}
|
||||
|
||||
void _handleDragEnd(DragEndDetails details) {
|
||||
// _drag might be null if the drag activity ended and called didEndDrag.
|
||||
_drag?.end(details, reverse: _reverseDirection);
|
||||
// _drag might be null if the drag activity ended and called _disposeDrag.
|
||||
_drag?.end(details);
|
||||
assert(_drag == null);
|
||||
}
|
||||
|
||||
@override
|
||||
@protected
|
||||
void didEndDrag() {
|
||||
void _handleDragCancel() {
|
||||
// _drag might be null if the drag activity ended and called _disposeDrag.
|
||||
_drag?.cancel();
|
||||
assert(_drag == null);
|
||||
}
|
||||
|
||||
void _disposeDrag() {
|
||||
_drag = null;
|
||||
}
|
||||
|
||||
|
@ -48,11 +48,15 @@ export 'src/widgets/preferred_size.dart';
|
||||
export 'src/widgets/primary_scroll_controller.dart';
|
||||
export 'src/widgets/raw_keyboard_listener.dart';
|
||||
export 'src/widgets/routes.dart';
|
||||
export 'src/widgets/scroll_activity.dart';
|
||||
export 'src/widgets/scroll_configuration.dart';
|
||||
export 'src/widgets/scroll_context.dart';
|
||||
export 'src/widgets/scroll_controller.dart';
|
||||
export 'src/widgets/scroll_metrics.dart';
|
||||
export 'src/widgets/scroll_notification.dart';
|
||||
export 'src/widgets/scroll_physics.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_view.dart';
|
||||
export 'src/widgets/scrollable.dart';
|
||||
|
@ -25,7 +25,7 @@ void main() {
|
||||
testWidgets('Inherited ScrollConfiguration changed', (WidgetTester tester) async {
|
||||
final GlobalKey key = new GlobalKey(debugLabel: 'scrollable');
|
||||
TestScrollBehavior behavior;
|
||||
ScrollPosition position;
|
||||
ScrollPositionWithSingleContext position;
|
||||
|
||||
final Widget scrollView = new SingleChildScrollView(
|
||||
key: key,
|
||||
@ -48,7 +48,7 @@ void main() {
|
||||
expect(behavior, isNotNull);
|
||||
expect(behavior.flag, isTrue);
|
||||
expect(position.physics, const isInstanceOf<ClampingScrollPhysics>());
|
||||
ScrollMetrics metrics = position.getMetrics();
|
||||
ScrollMetrics metrics = position.cloneMetrics();
|
||||
expect(metrics.extentAfter, equals(400.0));
|
||||
expect(metrics.viewportDimension, equals(600.0));
|
||||
|
||||
@ -64,7 +64,7 @@ void main() {
|
||||
expect(behavior.flag, isFalse);
|
||||
expect(position.physics, const isInstanceOf<BouncingScrollPhysics>());
|
||||
// Regression test for https://github.com/flutter/flutter/issues/5856
|
||||
metrics = position.getMetrics();
|
||||
metrics = position.cloneMetrics();
|
||||
expect(metrics.extentAfter, equals(400.0));
|
||||
expect(metrics.viewportDimension, equals(600.0));
|
||||
});
|
||||
|
@ -1,142 +0,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 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class TestScrollPosition extends ScrollPosition {
|
||||
class TestScrollPosition extends ScrollPositionWithSingleContext {
|
||||
TestScrollPosition({
|
||||
ScrollPhysics physics,
|
||||
AbstractScrollState state,
|
||||
ScrollContext state,
|
||||
double initialPixels: 0.0,
|
||||
ScrollPosition oldPosition,
|
||||
}) : super(
|
||||
physics: physics,
|
||||
state: state,
|
||||
context: state,
|
||||
initialPixels: initialPixels,
|
||||
oldPosition: oldPosition,
|
||||
);
|
||||
@ -21,10 +21,10 @@ class TestScrollPosition extends ScrollPosition {
|
||||
|
||||
class TestScrollController extends ScrollController {
|
||||
@override
|
||||
ScrollPosition createScrollPosition(ScrollPhysics physics, AbstractScrollState state, ScrollPosition oldPosition) {
|
||||
ScrollPosition createScrollPosition(ScrollPhysics physics, ScrollContext context, ScrollPosition oldPosition) {
|
||||
return new TestScrollPosition(
|
||||
physics: physics,
|
||||
state: state,
|
||||
state: context,
|
||||
initialPixels: initialScrollOffset,
|
||||
oldPosition: oldPosition,
|
||||
);
|
||||
|
@ -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(key2, const Offset(0.0, 600.0), false);
|
||||
@ -175,7 +175,7 @@ void main() {
|
||||
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.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));
|
||||
verifyPaintPosition(key1, const Offset(0.0, 0.0), false);
|
||||
verifyPaintPosition(key2, const Offset(0.0, 0.0), true);
|
||||
|
@ -53,12 +53,12 @@ class TestScrollPhysics extends ClampingScrollPhysics {
|
||||
Tolerance get tolerance => const Tolerance(velocity: 20.0, distance: 1.0);
|
||||
}
|
||||
|
||||
class TestViewportScrollPosition extends ScrollPosition {
|
||||
class TestViewportScrollPosition extends ScrollPositionWithSingleContext {
|
||||
TestViewportScrollPosition({
|
||||
ScrollPhysics physics,
|
||||
AbstractScrollState state,
|
||||
ScrollContext context,
|
||||
ScrollPosition oldPosition,
|
||||
}) : super(physics: physics, state: state, oldPosition: oldPosition);
|
||||
}) : super(physics: physics, context: context, oldPosition: oldPosition);
|
||||
|
||||
@override
|
||||
bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
|
||||
|
Loading…
Reference in New Issue
Block a user