Commit 072cce88 authored by Adam Barth's avatar Adam Barth Committed by GitHub

Reparameterize Scrollable2 (#7853)

This patch makes a number of changes to how you can configure a
Scrollable2:

 - The ScrollPhysics is now responsible for creating the ScrollPosition.
   You can override the ScrollPhysics by supplying a `physics` argument
   to `Scrollable`, and the new physics you supply will be applied to
   the default physics inherited from the ScrollBehavior.

 - This patch removes the ScrollPosition/AbsoluteScrollPosition split as
   all clients were operating in pixels anyway and the split made the
   code very difficult to follow.

 - ScrollPosition no longer depends directly on Scrollable2State.
   Instead, it depends on an abstract interface that Scrollable2State
   implements. This change has two benefits:

    a) It removes the circular dependency between ScrollPosition and
       Scrollable2State, which lets us split the code for these classes
       (and several other classes that got wrapped up in that cycle) into
       separate libraries for easier maintenance.

    b) ScrollPosition is no longer bound to Scrollable2, which means you
       could use the behavior machinery to drive other sorts of widgets.
       For example, we could use it to drive Scrollabe1 if we wanted.
parent 475e7ce9
......@@ -214,7 +214,7 @@ class _ScrollLikeMountainViewDelegate extends ScrollConfigurationDelegate {
bool updateShouldNotify(ScrollConfigurationDelegate old) => false;
}
class _MaterialScrollBehavior extends ViewportScrollBehavior {
class _MaterialScrollBehavior extends ScrollBehavior2 {
@override
TargetPlatform getPlatform(BuildContext context) {
return Theme.of(context).platform;
......@@ -308,7 +308,7 @@ class _MaterialAppState extends State<MaterialApp> {
);
return new ScrollConfiguration2(
delegate: new _MaterialScrollBehavior(),
behavior: new _MaterialScrollBehavior(),
child: result
);
}
......
......@@ -2,7 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
////////////////////////////////////////////////////////////////////////////////
// DELETE THIS FILE WHEN REMOVING LEGACY SCROLLING CODE
////////////////////////////////////////////////////////////////////////////////
import 'dart:async' show Timer;
import 'dart:math' as math;
......
......@@ -180,8 +180,9 @@ class _ScrollbarPainter extends CustomPainter {
}
}
////////////////////////////////////////////////////////////////////////////////
// DELETE EVERYTHING BELOW THIS LINE WHEN REMOVING LEGACY SCROLLING CODE
////////////////////////////////////////////////////////////////////////////////
const double _kMinScrollbarThumbExtent = 18.0;
const double _kScrollbarThumbGirth = 6.0;
......
......@@ -30,6 +30,8 @@ import 'tolerance.dart';
/// should establish a convention and use that convention consistently with all
/// related objects.
abstract class Simulation {
Simulation({ this.tolerance: Tolerance.defaultTolerance });
/// The position of the object in the simulation at the given time.
double x(double time);
......@@ -46,7 +48,7 @@ abstract class Simulation {
/// but once the difference from the value at a particular time and the
/// asymptote itself could not be seen, it would be pointless to continue. The
/// tolerance defines how to determine if the difference could not be seen.
Tolerance tolerance = Tolerance.defaultTolerance;
Tolerance tolerance;
@override
String toString() => '$runtimeType';
......
......@@ -5,6 +5,7 @@
import 'dart:math' as math;
import 'simulation.dart';
import 'tolerance.dart';
import 'utils.dart';
/// Structure that describes a spring's constants.
......@@ -86,12 +87,14 @@ class SpringSimulation extends Simulation {
/// arbitrary unit of length, and T is the time unit used for driving the
/// [SpringSimulation].
SpringSimulation(
SpringDescription desc,
SpringDescription spring,
double start,
double end,
double velocity
) : _endPosition = end,
_solution = new _SpringSolution(desc, start - end, velocity);
double velocity, {
Tolerance tolerance: Tolerance.defaultTolerance,
}) : _endPosition = end,
_solution = new _SpringSolution(spring, start - end, velocity),
super(tolerance: tolerance);
final double _endPosition;
final _SpringSolution _solution;
......@@ -124,11 +127,12 @@ class ScrollSpringSimulation extends SpringSimulation {
/// See the [new SpringSimulation] constructor on the superclass for a
/// discussion of the arguments' units.
ScrollSpringSimulation(
SpringDescription desc,
SpringDescription spring,
double start,
double end,
double velocity
) : super(desc, start, end, velocity);
double velocity, {
Tolerance tolerance: Tolerance.defaultTolerance,
}) : super(spring, start, end, velocity, tolerance: tolerance);
@override
double x(double time) => isDone(time) ? _endPosition : super.x(time);
......@@ -139,22 +143,22 @@ class ScrollSpringSimulation extends SpringSimulation {
abstract class _SpringSolution {
factory _SpringSolution(
SpringDescription desc,
SpringDescription spring,
double initialPosition,
double initialVelocity
) {
assert(desc != null);
assert(desc.mass != null);
assert(desc.springConstant != null);
assert(desc.damping != null);
assert(spring != null);
assert(spring.mass != null);
assert(spring.springConstant != null);
assert(spring.damping != null);
assert(initialPosition != null);
assert(initialVelocity != null);
double cmk = desc.damping * desc.damping - 4 * desc.mass * desc.springConstant;
double cmk = spring.damping * spring.damping - 4 * spring.mass * spring.springConstant;
if (cmk == 0.0)
return new _CriticalSolution(desc, initialPosition, initialVelocity);
return new _CriticalSolution(spring, initialPosition, initialVelocity);
if (cmk > 0.0)
return new _OverdampedSolution(desc, initialPosition, initialVelocity);
return new _UnderdampedSolution(desc, initialPosition, initialVelocity);
return new _OverdampedSolution(spring, initialPosition, initialVelocity);
return new _UnderdampedSolution(spring, initialPosition, initialVelocity);
}
double x(double time);
......@@ -164,11 +168,11 @@ abstract class _SpringSolution {
class _CriticalSolution implements _SpringSolution {
factory _CriticalSolution(
SpringDescription desc,
SpringDescription spring,
double distance,
double velocity
) {
final double r = -desc.damping / (2.0 * desc.mass);
final double r = -spring.damping / (2.0 * spring.mass);
final double c1 = distance;
final double c2 = velocity / (r * distance);
return new _CriticalSolution.withArgs(r, c1, c2);
......@@ -198,13 +202,13 @@ class _CriticalSolution implements _SpringSolution {
class _OverdampedSolution implements _SpringSolution {
factory _OverdampedSolution(
SpringDescription desc,
SpringDescription spring,
double distance,
double velocity
) {
final double cmk = desc.damping * desc.damping - 4 * desc.mass * desc.springConstant;
final double r1 = (-desc.damping - math.sqrt(cmk)) / (2.0 * desc.mass);
final double r2 = (-desc.damping + math.sqrt(cmk)) / (2.0 * desc.mass);
final double cmk = spring.damping * spring.damping - 4 * spring.mass * spring.springConstant;
final double r1 = (-spring.damping - math.sqrt(cmk)) / (2.0 * spring.mass);
final double r2 = (-spring.damping + math.sqrt(cmk)) / (2.0 * spring.mass);
final double c2 = (velocity - r1 * distance) / (r2 - r1);
final double c1 = distance - c2;
return new _OverdampedSolution.withArgs(r1, r2, c1, c2);
......@@ -236,13 +240,13 @@ class _OverdampedSolution implements _SpringSolution {
class _UnderdampedSolution implements _SpringSolution {
factory _UnderdampedSolution(
SpringDescription desc,
SpringDescription spring,
double distance,
double velocity
) {
final double w = math.sqrt(4.0 * desc.mass * desc.springConstant -
desc.damping * desc.damping) / (2.0 * desc.mass);
final double r = -(desc.damping / 2.0 * desc.mass);
final double w = math.sqrt(4.0 * spring.mass * spring.springConstant -
spring.damping * spring.damping) / (2.0 * spring.mass);
final double r = -(spring.damping / 2.0 * spring.mass);
final double c1 = distance;
final double c2 = (velocity - r * distance) / w;
return new _UnderdampedSolution.withArgs(w, r, c1, c2);
......
......@@ -62,8 +62,8 @@ class GlowingOverscrollIndicator extends StatefulWidget {
/// widget.
///
/// Typically a [GlowingOverscrollIndicator] is created by a
/// [ScrollBehavior2.wrap] method, in which case the child is usually the one
/// provided as an argument to that method.
/// [ScrollBehavior2.buildViewportChrome] method, in which case
/// the child is usually the one provided as an argument to that method.
final Widget child;
@override
......
// 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/physics.dart';
import 'scroll_absolute.dart';
class PageScrollPhysics extends ScrollPhysicsProxy {
const PageScrollPhysics({
ScrollPhysics parent,
this.springDescription,
}) : super(parent);
final SpringDescription springDescription;
@override
PageScrollPhysics applyTo(ScrollPhysics parent) {
return new PageScrollPhysics(
parent: parent,
springDescription: springDescription,
);
}
double _roundToPage(AbsoluteScrollPosition position, double pixels, double pageSize) {
final int index = (pixels + pageSize / 2.0) ~/ pageSize;
return (pageSize * index).clamp(position.minScrollExtent, position.maxScrollExtent);
}
double _getTargetPixels(AbsoluteScrollPosition position, double velocity) {
final double pageSize = position.viewportDimension;
if (velocity < -position.scrollTolerances.velocity)
return _roundToPage(position, position.pixels - pageSize / 2.0, pageSize);
if (velocity > position.scrollTolerances.velocity)
return _roundToPage(position, position.pixels + pageSize / 2.0, pageSize);
return _roundToPage(position, position.pixels, pageSize);
}
@override
Simulation createBallisticSimulation(AbsoluteScrollPosition 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) ||
(velocity >= 0.0 && position.pixels >= position.maxScrollExtent))
return super.createBallisticSimulation(position, velocity);
final double target = _getTargetPixels(position, velocity);
return new ScrollSpringSimulation(scrollSpring, position.pixels, target, velocity);
}
}
......@@ -2,7 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
////////////////////////////////////////////////////////////////////////////////
// DELETE THIS FILE WHEN REMOVING LEGACY SCROLLING CODE
////////////////////////////////////////////////////////////////////////////////
import 'dart:math' as math;
......
......@@ -2,12 +2,86 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// DELETE THIS FILE WHEN REMOVING LEGACY SCROLLING CODE
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'framework.dart';
import 'scroll_behavior.dart';
import 'scroll_physics.dart';
import 'overscroll_indicator.dart';
class ScrollBehavior2 {
const ScrollBehavior2();
/// The platform whose scroll physics should be implemented.
///
/// Defaults to the current platform.
TargetPlatform getPlatform(BuildContext context) => defaultTargetPlatform;
/// The color to use for the glow effect when [platform] indicates a platform
/// that uses a [GlowingOverscrollIndicator].
///
/// Defaults to white.
Color getGlowColor(BuildContext context) => const Color(0xFFFFFFFF);
Widget buildViewportChrome(BuildContext context, Widget child, AxisDirection axisDirection) {
switch (getPlatform(context)) {
case TargetPlatform.iOS:
return child;
case TargetPlatform.android:
case TargetPlatform.fuchsia:
return new GlowingOverscrollIndicator(
child: child,
axisDirection: axisDirection,
color: getGlowColor(context),
);
}
return null;
}
/// The scroll physics to use for the given platform.
///
/// Used by [createScrollPosition] to get the scroll physics for newly created
/// scroll positions.
ScrollPhysics getScrollPhysics(BuildContext context) {
switch (getPlatform(context)) {
case TargetPlatform.iOS:
return const BouncingScrollPhysics();
case TargetPlatform.android:
case TargetPlatform.fuchsia:
return const ClampingScrollPhysics();
}
return null;
}
bool shouldNotify(@checked ScrollBehavior2 oldDelegate) => false;
}
class ScrollConfiguration2 extends InheritedWidget {
const ScrollConfiguration2({
Key key,
@required this.behavior,
@required Widget child,
}) : super(key: key, child: child);
final ScrollBehavior2 behavior;
static ScrollBehavior2 of(BuildContext context) {
final ScrollConfiguration2 configuration = context.inheritFromWidgetOfExactType(ScrollConfiguration2);
return configuration?.behavior ?? const ScrollBehavior2();
}
@override
bool updateShouldNotify(ScrollConfiguration2 old) {
assert(behavior != null);
return behavior.runtimeType != old.behavior.runtimeType
|| behavior.shouldNotify(old.behavior);
}
}
////////////////////////////////////////////////////////////////////////////////
// DELETE EVERYTHING BELOW THIS LINE WHEN REMOVING LEGACY SCROLLING CODE
////////////////////////////////////////////////////////////////////////////////
/// Controls how [Scrollable] widgets in a subtree behave.
///
......
// 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:math' as math;
import 'package:flutter/physics.dart';
import 'overscroll_indicator.dart';
import 'scroll_simulation.dart';
import 'scroll_position.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;
/// 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
/// those bounds.
///
/// This is the behavior typically seen on iOS.
///
/// See also:
///
/// * [ViewportScrollBehavior], which uses this to provide the iOS component of
/// its scroll behavior.
/// * [ClampingScrollPhysics], which is the analogous physics for Android's
/// clamping behavior.
class BouncingScrollPhysics extends ScrollPhysics {
const BouncingScrollPhysics({ ScrollPhysics parent }) : super(parent);
@override
BouncingScrollPhysics applyTo(ScrollPhysics parent) => new BouncingScrollPhysics(parent: parent);
/// The multiple applied to overscroll to make it appear that scrolling past
/// the edge of the scrollable contents is harder than scrolling the list.
///
/// By default this is 0.5, meaning that overscroll is twice as hard as normal
/// scroll.
double get frictionFactor => 0.5;
@override
double applyPhysicsToUserOffset(ScrollPosition position, double offset) {
assert(offset != 0.0);
assert(position.minScrollExtent <= position.maxScrollExtent);
if (offset > 0.0)
return _applyFriction(position.pixels, position.minScrollExtent, position.maxScrollExtent, offset, frictionFactor);
return -_applyFriction(-position.pixels, -position.maxScrollExtent, -position.minScrollExtent, -offset, frictionFactor);
}
static double _applyFriction(double start, double lowLimit, double highLimit, double delta, double gamma) {
assert(lowLimit <= highLimit);
assert(delta > 0.0);
double total = 0.0;
if (start < lowLimit) {
double distanceToLimit = lowLimit - start;
double deltaToLimit = distanceToLimit / gamma;
if (delta < deltaToLimit)
return total + delta * gamma;
total += distanceToLimit;
delta -= deltaToLimit;
}
return total + delta;
}
@override
Simulation createBallisticSimulation(ScrollPosition position, double velocity) {
final Tolerance tolerance = this.tolerance;
if (velocity.abs() >= tolerance.velocity || position.outOfRange) {
return new BouncingScrollSimulation(
spring: spring,
position: position.pixels,
velocity: velocity,
leadingExtent: position.minScrollExtent,
trailingExtent: position.maxScrollExtent,
)..tolerance = tolerance;
}
return null;
}
}
/// Scroll physics for environments that prevent the scroll offset from reaching
/// beyond the bounds of the content.
///
/// This is the behavior typically seen on Android.
///
/// See also:
///
/// * [ViewportScrollBehavior], which uses this to provide the Android component
/// of its scroll behavior.
/// * [BouncingScrollPhysics], which is the analogous physics for iOS' bouncing
/// behavior.
/// * [GlowingOverscrollIndicator], which is used by [ViewportScrollBehavior] to
/// provide the glowing effect that is usually found with this clamping effect
/// on Android.
class ClampingScrollPhysics extends ScrollPhysics {
const ClampingScrollPhysics({ ScrollPhysics parent }) : super(parent);
@override
ClampingScrollPhysics applyTo(ScrollPhysics parent) => new ClampingScrollPhysics(parent: parent);
@override
double applyBoundaryConditions(ScrollPosition position, double value) {
assert(value != position.pixels);
if (value < position.pixels && position.pixels <= position.minScrollExtent) // underscroll
return value - position.pixels;
if (position.maxScrollExtent <= position.pixels && position.pixels < value) // overscroll
return value - position.pixels;
if (value < position.minScrollExtent && position.minScrollExtent < position.pixels) // hit top edge
return value - position.minScrollExtent;
if (position.pixels < position.maxScrollExtent && position.maxScrollExtent < value) // hit bottom edge
return value - position.maxScrollExtent;
return 0.0;
}
@override
Simulation createBallisticSimulation(ScrollPosition position, double velocity) {
final Tolerance tolerance = this.tolerance;
if (position.outOfRange) {
double end;
if (position.pixels > position.maxScrollExtent)
end = position.maxScrollExtent;
if (position.pixels < position.minScrollExtent)
end = position.minScrollExtent;
assert(end != null);
return new ScrollSpringSimulation(
spring,
position.pixels,
position.maxScrollExtent,
math.min(0.0, velocity),
tolerance: tolerance
);
}
if (!position.atEdge && velocity.abs() >= tolerance.velocity) {
return new ClampingScrollSimulation(
position: position.pixels,
velocity: velocity,
tolerance: tolerance,
);
}
return null;
}
}
class PageScrollPhysics extends ScrollPhysics {
const PageScrollPhysics({ ScrollPhysics parent }) : super(parent);
@override
PageScrollPhysics applyTo(ScrollPhysics parent) => new PageScrollPhysics(parent: parent);
double _roundToPage(ScrollPosition position, double pixels, double pageSize) {
final int index = (pixels + pageSize / 2.0) ~/ pageSize;
return (pageSize * index).clamp(position.minScrollExtent, position.maxScrollExtent);
}
double _getTargetPixels(ScrollPosition position, Tolerance tolerance, double velocity) {
final double pageSize = position.viewportDimension;
if (velocity < -tolerance.velocity)
return _roundToPage(position, position.pixels - pageSize / 2.0, pageSize);
if (velocity > tolerance.velocity)
return _roundToPage(position, position.pixels + pageSize / 2.0, pageSize);
return _roundToPage(position, position.pixels, pageSize);
}
@override
Simulation createBallisticSimulation(ScrollPosition 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) ||
(velocity >= 0.0 && position.pixels >= position.maxScrollExtent))
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);
}
}
......@@ -15,202 +15,238 @@ import 'package:flutter/scheduler.dart';
import 'basic.dart';
import 'framework.dart';
import 'gesture_detector.dart';
import 'overscroll_indicator.dart';
import 'scroll_simulation.dart';
import 'notification_listener.dart';
import 'scroll_notification.dart';
import 'scrollable.dart';
import 'viewport.dart';
/// Scrolling logic delegate for lists and other unremarkable scrollable
/// viewports.
///
/// See also:
///
/// * [BouncingAbsoluteScrollPositionMixIn], which is used by this class to
/// implement the scroll behavior for iOS.
/// * [ClampingAbsoluteScrollPositionMixIn] and [GlowingOverscrollIndicator],
/// which are used by this class to implement the scroll behavior for Android.
class ViewportScrollBehavior extends ScrollBehavior2 {
ViewportScrollBehavior({
Tolerance scrollTolerances,
}) : scrollTolerances = scrollTolerances ?? defaultScrollTolerances;
/// The accuracy to which scrolling is computed.
///
/// Defaults to [defaultScrollTolerances].
final Tolerance scrollTolerances;
import 'ticker_provider.dart';
/// The accuracy to which scrolling is computed by default.
///
/// This is the default value for [scrollTolerances].
static final Tolerance defaultScrollTolerances = 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
);
export 'package:flutter/physics.dart' show Tolerance;
/// The platform whose scroll physics should be implemented.
///
/// Defaults to the current platform.
TargetPlatform getPlatform(BuildContext context) => defaultTargetPlatform;
abstract class AbstractScrollState {
BuildContext get context;
TickerProvider get vsync;
/// The color to use for the glow effect when [platform] indicates a platform
/// that uses a [GlowingOverscrollIndicator].
///
/// Defaults to white.
Color getGlowColor(BuildContext context) => const Color(0xFFFFFFFF);
@override
Widget wrap(BuildContext context, Widget child, AxisDirection axisDirection) {
switch (getPlatform(context)) {
case TargetPlatform.iOS:
return child;
case TargetPlatform.android:
case TargetPlatform.fuchsia:
return new GlowingOverscrollIndicator(
child: child,
axisDirection: axisDirection,
color: getGlowColor(context),
);
}
return null;
}
void setIgnorePointer(bool value);
void setCanDrag(bool value);
void didEndDrag();
void dispatchNotification(Notification notification);
}
/// The scroll physics to use for the given platform.
///
/// Used by [createScrollPosition] to get the scroll physics for newly created
/// scroll positions.
ScrollPhysics getScrollPhysics(TargetPlatform platform) {
assert(platform != null);
switch (platform) {
case TargetPlatform.iOS:
return const BouncingScrollPhysics();
case TargetPlatform.android:
case TargetPlatform.fuchsia:
return const ClampingScrollPhysics();
}
return null;
}
abstract class ScrollPhysics {
const ScrollPhysics(this.parent);
ScrollPhysics _getEffectiveScrollPhysics(BuildContext context, ScrollPhysics physics) {
final ScrollPhysics defaultPhysics = getScrollPhysics(getPlatform(context));
if (physics != null)
return physics.applyTo(defaultPhysics);
return defaultPhysics;
}
final ScrollPhysics parent;
@override
ScrollPosition createScrollPosition(BuildContext context, Scrollable2State state, ScrollPosition oldPosition, ScrollPhysics physics) {
return new AbsoluteScrollPosition(state, scrollTolerances, oldPosition, _getEffectiveScrollPhysics(context, physics));
}
ScrollPhysics applyTo(ScrollPhysics parent);
@override
bool shouldNotify(ViewportScrollBehavior oldDelegate) {
return scrollTolerances != oldDelegate.scrollTolerances;
ScrollPosition createScrollPosition(ScrollPhysics physics, AbstractScrollState state, ScrollPosition oldPosition) {
if (parent == null)
return new ScrollPosition(physics, state, oldPosition);
return parent.createScrollPosition(physics, state, oldPosition);
}
}
abstract class ScrollPhysics {
const ScrollPhysics();
ScrollPhysicsProxy applyTo(ScrollPhysics parent) => this;
/// Used by [AbsoluteDragScrollActivity] and other user-driven activities to
/// 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(AbsoluteScrollPosition position, double offset) => offset;
double applyPhysicsToUserOffset(ScrollPosition position, double offset) {
if (parent == null)
return offset;
return parent.applyPhysicsToUserOffset(position, offset);
}
/// Determines the overscroll by applying the boundary conditions.
///
/// Called by [AbsoluteScrollPosition.setPixels] just before the [pixels] value is updated, to
/// determine how much of the offset is to be clamped off and sent to
/// [AbsoluteScrollPosition.reportOverscroll].
/// 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(AbsoluteScrollPosition position, double value) => 0.0;
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
/// [AbsoluteBallisticScrollActivity] with the returned value. Otherwise, the
/// [BallisticScrollActivity] with the returned value. Otherwise, the
/// [ScrollPosition] will begin an idle activity instead.
Simulation createBallisticSimulation(AbsoluteScrollPosition position, double velocity) => null;
Simulation createBallisticSimulation(ScrollPosition position, double velocity) {
if (parent == null)
return null;
return parent.createBallisticSimulation(position, velocity);
}
static final SpringDescription _kDefaultScrollSpring = new SpringDescription.withDampingRatio(
static final SpringDescription _kDefaultSpring = new SpringDescription.withDampingRatio(
mass: 0.5,
springConstant: 100.0,
ratio: 1.1,
);
SpringDescription get scrollSpring => _kDefaultScrollSpring;
}
abstract class ScrollPhysicsProxy extends ScrollPhysics {
const ScrollPhysicsProxy(this.parent);
SpringDescription get spring => parent?.spring ?? _kDefaultSpring;
final ScrollPhysics parent;
@override
ScrollPhysicsProxy applyTo(ScrollPhysics parent) {
throw new FlutterError(
'$runtimeType must override applyTo.\n'
'The default implementation of applyTo is not appropriate for subclasses '
'of ScrollPhysicsProxy because they should return an instance of themselves '
'with their parent property replaced with the given ScrollPhysics instance.'
);
}
@override
double applyPhysicsToUserOffset(AbsoluteScrollPosition position, double offset) {
if (parent == null)
return super.applyPhysicsToUserOffset(position, offset);
return parent.applyPhysicsToUserOffset(position, offset);
}
/// 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
);
@override
double applyBoundaryConditions(AbsoluteScrollPosition position, double value) {
if (parent == null)
return super.applyBoundaryConditions(position, value);
return parent.applyBoundaryConditions(position, value);
}
Tolerance get tolerance => parent?.tolerance ?? _kDefaultTolerance;
@override
Simulation createBallisticSimulation(AbsoluteScrollPosition position, double velocity) {
if (parent == null)
return super.createBallisticSimulation(position, velocity);
return parent.createBallisticSimulation(position, velocity);
@mustCallSuper
bool shouldUpdateScrollPosition(@checked ScrollPhysics other) {
if ((parent == null) != (other.parent == null))
return true;
if (parent == null) {
assert(other.parent == null);
return false;
}
return parent.runtimeType != other.parent.runtimeType
|| parent.shouldUpdateScrollPosition(other.parent);
}
@override
SpringDescription get scrollSpring {
String toString() {
if (parent == null)
return super.scrollSpring;
return parent.scrollSpring;
return runtimeType.toString();
return '$runtimeType -> $parent';
}
}
class AbsoluteScrollPosition extends ScrollPosition {
AbsoluteScrollPosition(
Scrollable2State state,
Tolerance scrollTolerances,
ScrollPosition oldPosition,
this.physics,
) : super(state, scrollTolerances, oldPosition);
class ScrollPosition extends ViewportOffset {
ScrollPosition(this.physics, this.state, ScrollPosition oldPosition) {
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 pixels => _pixels;
double _pixels = 0.0;
@override
/// Animates the position from its current value to the given value `to`.
///
/// 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 [setPixels].
///
/// The animation is handled by an [DrivenScrollActivity].
Future<Null> animate({
@required 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.
///
/// Immediately after the jump, a ballistic activity is started, in case the
/// value was out of range.
void jumpTo(double value) {
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));
}
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).
ScrollableMetrics getMetrics() {
return new ScrollableMetrics(
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),
);
}
/// Update the scroll position ([pixels]) to a given pixel value.
///
/// This should only be called by the current [ScrollActivity], either during
/// the transient callback phase or in response to user input.
///
/// Returns the overscroll, if any. If the return value is 0.0, that means
/// that [pixels] now returns the given `value`. If the return value is
/// positive, then [pixels] is less than the requested `value` by the given
/// amount (overscroll past the max extent), and if it is negative, it is
/// 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) {
assert(SchedulerBinding.instance.schedulerPhase.index <= SchedulerPhase.transientCallbacks.index);
assert(activity.isScrolling);
......@@ -234,7 +270,7 @@ class AbsoluteScrollPosition extends ScrollPosition {
_pixels = value - overScroll;
if (_pixels != oldPixels) {
notifyListeners();
dispatchNotification(activity.createScrollUpdateNotification(state, _pixels - oldPixels));
state.dispatchNotification(activity.createScrollUpdateNotification(state, _pixels - oldPixels));
}
if (overScroll != 0.0) {
reportOverscroll(overScroll);
......@@ -244,10 +280,15 @@ class AbsoluteScrollPosition extends ScrollPosition {
return 0.0;
}
@override
void correctBy(double correction) {
_pixels += correction;
}
@protected
void reportOverscroll(double value) {
assert(activity.isScrolling);
dispatchNotification(activity.createOverscrollNotification(state, value));
state.dispatchNotification(activity.createOverscrollNotification(state, value));
}
double get viewportDimension => _viewportDimension;
......@@ -274,7 +315,7 @@ class AbsoluteScrollPosition extends ScrollPosition {
// soon afterwards in the same layout phase. So we put all the logic that
// relies on both values being computed into applyContentDimensions.
}
super.applyViewportDimension(viewportDimension);
state.setCanDrag(canDrag);
}
@override
......@@ -287,259 +328,229 @@ class AbsoluteScrollPosition extends ScrollPosition {
activity.applyNewDimensions();
_didChangeViewportDimension = false;
}
return super.applyContentDimensions(minScrollExtent, maxScrollExtent);
state.setCanDrag(canDrag);
return true;
}
@override
ScrollableMetrics getMetrics() {
return new ScrollableMetrics(
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),
);
/// 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);
_pixels = other._pixels;
_viewportDimension = other.viewportDimension;
_minScrollExtent = other.minScrollExtent;
_maxScrollExtent = other.maxScrollExtent;
_userScrollDirection = other._userScrollDirection;
final bool oldIgnorePointer = shouldIgnorePointer;
other.activity._position = this;
_activity = other.activity;
other._activity = null;
if (oldIgnorePointer != shouldIgnorePointer)
state.setIgnorePointer(shouldIgnorePointer);
if (other.runtimeType != runtimeType)
activity.resetActivity();
}
@override
bool get canDrag => true;
@override
bool get shouldIgnorePointer => activity?.shouldIgnorePointer;
void touched() {
_activity.touched();
}
/// The direction that the user most recently began scrolling in.
@override
void correctBy(double correction) {
_pixels += correction;
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 absorb(ScrollPosition other) {
if (other is AbsoluteScrollPosition) {
final AbsoluteScrollPosition typedOther = other;
_pixels = typedOther._pixels;
_viewportDimension = typedOther.viewportDimension;
_minScrollExtent = typedOther.minScrollExtent;
_maxScrollExtent = typedOther.maxScrollExtent;
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;
/// 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;
}
super.absorb(other);
_activity = newActivity;
if (oldIgnorePointer != shouldIgnorePointer)
state.setIgnorePointer(shouldIgnorePointer);
if (!activity.isScrolling)
updateUserScrollDirection(ScrollDirection.idle);
if (!wasScrolling && activity.isScrolling)
state.dispatchNotification(activity.createScrollStartNotification(state));
}
void beginIdleActivity() {
beginActivity(new IdleScrollActivity(this));
}
@override
DragScrollActivity beginDragActivity(DragStartDetails details) {
beginActivity(new AbsoluteDragScrollActivity(this, details, scrollTolerances));
beginActivity(new DragScrollActivity(this, details));
return activity;
}
@override
// ///
// /// The velocity should be in logical pixels per second.
void beginBallisticActivity(double velocity) {
final Simulation simulation = physics.createBallisticSimulation(this, velocity);
if (simulation != null) {
simulation.tolerance = scrollTolerances;
beginActivity(new AbsoluteBallisticScrollActivity(this, simulation, vsync));
beginActivity(new BallisticScrollActivity(this, simulation, state.vsync));
} else {
beginIdleActivity();
}
}
/// Animates the position from its current value to the given value `to`.
///
/// 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 [setPixels].
///
/// The animation is handled by an [AbsoluteDrivenScrollActivity].
Future<Null> animate({
@required double to,
@required Duration duration,
@required Curve curve,
}) {
final AbsoluteDrivenScrollActivity activity = new AbsoluteDrivenScrollActivity(
this,
from: pixels,
to: to,
duration: duration,
curve: curve,
vsync: 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.
///
/// Immediately after the jump, a ballistic activity is started, in case the
/// value was out of range.
void jumpTo(double value) {
beginIdleActivity();
if (_pixels != value) {
final double oldPixels = _pixels;
_pixels = value;
notifyListeners();
dispatchNotification(activity.createScrollStartNotification(state));
dispatchNotification(activity.createScrollUpdateNotification(state, _pixels - oldPixels));
dispatchNotification(activity.createScrollEndNotification(state));
}
beginBallisticActivity(0.0);
}
@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)}');
}
}
/// 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
/// those bounds.
///
/// This is the behavior typically seen on iOS.
///
/// See also:
///
/// * [ViewportScrollBehavior], which uses this to provide the iOS component of
/// its scroll behavior.
/// * [ClampingScrollPhysics], which is the analogous physics for Android's
/// clamping behavior.
class BouncingScrollPhysics extends ScrollPhysics {
const BouncingScrollPhysics();
/// The multiple applied to overscroll to make it appear that scrolling past
/// the edge of the scrollable contents is harder than scrolling the list.
/// 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.
///
/// By default this is 0.5, meaning that overscroll is twice as hard as normal
/// scroll.
double get frictionFactor => 0.5;
@override
double applyPhysicsToUserOffset(AbsoluteScrollPosition position, double offset) {
assert(offset != 0.0);
assert(position.minScrollExtent <= position.maxScrollExtent);
if (offset > 0.0)
return _applyFriction(position.pixels, position.minScrollExtent, position.maxScrollExtent, offset, frictionFactor);
return -_applyFriction(-position.pixels, -position.maxScrollExtent, -position.minScrollExtent, -offset, frictionFactor);
}
static double _applyFriction(double start, double lowLimit, double highLimit, double delta, double gamma) {
assert(lowLimit <= highLimit);
assert(delta > 0.0);
double total = 0.0;
if (start < lowLimit) {
double distanceToLimit = lowLimit - start;
double deltaToLimit = distanceToLimit / gamma;
if (delta < deltaToLimit)
return total + delta * gamma;
total += distanceToLimit;
delta -= deltaToLimit;
}
return total + delta;
/// For example, [BallisticScrollActivity]'s implementation calls
/// [ScrollPosition.beginBallisticActivity].
void resetActivity() { }
Notification createScrollStartNotification(Scrollable2State scrollable) {
return new ScrollStartNotification(scrollable: scrollable);
}
@override
Simulation createBallisticSimulation(AbsoluteScrollPosition position, double velocity) {
if (velocity.abs() >= position.scrollTolerances.velocity || position.outOfRange) {
return new BouncingScrollSimulation(
spring: scrollSpring,
position: position.pixels,
velocity: velocity,
leadingExtent: position.minScrollExtent,
trailingExtent: position.maxScrollExtent,
);
}
return null;
Notification createScrollUpdateNotification(Scrollable2State scrollable, double scrollDelta) {
return new ScrollUpdateNotification(scrollable: scrollable, scrollDelta: scrollDelta);
}
}
/// Scroll physics for environments that prevent the scroll offset from reaching
/// beyond the bounds of the content.
///
/// This is the behavior typically seen on Android.
///
/// See also:
///
/// * [ViewportScrollBehavior], which uses this to provide the Android component
/// of its scroll behavior.
/// * [BouncingScrollPhysics], which is the analogous physics for iOS' bouncing
/// behavior.
/// * [GlowingOverscrollIndicator], which is used by [ViewportScrollBehavior] to
/// provide the glowing effect that is usually found with this clamping effect
/// on Android.
class ClampingScrollPhysics extends ScrollPhysics {
const ClampingScrollPhysics();
@override
double applyBoundaryConditions(AbsoluteScrollPosition position, double value) {
assert(value != position.pixels);
if (value < position.pixels && position.pixels <= position.minScrollExtent) // underscroll
return value - position.pixels;
if (position.maxScrollExtent <= position.pixels && position.pixels < value) // overscroll
return value - position.pixels;
if (value < position.minScrollExtent && position.minScrollExtent < position.pixels) // hit top edge
return value - position.minScrollExtent;
if (position.pixels < position.maxScrollExtent && position.maxScrollExtent < value) // hit bottom edge
return value - position.maxScrollExtent;
return 0.0;
Notification createOverscrollNotification(Scrollable2State scrollable, double overscroll) {
return new OverscrollNotification(scrollable: scrollable, overscroll: overscroll);
}
Notification createScrollEndNotification(Scrollable2State scrollable) {
return new ScrollEndNotification(scrollable: scrollable);
}
void touched() { }
void applyNewDimensions() { }
bool get shouldIgnorePointer;
bool get isScrolling;
@mustCallSuper
void dispose() {
_position = null;
}
@override
Simulation createBallisticSimulation(AbsoluteScrollPosition position, double velocity) {
if (position.outOfRange) {
if (position.pixels > position.maxScrollExtent)
return new ScrollSpringSimulation(scrollSpring, position.pixels, position.maxScrollExtent, math.min(0.0, velocity));
if (position.pixels < position.minScrollExtent)
return new ScrollSpringSimulation(scrollSpring, position.pixels, position.minScrollExtent, math.max(0.0, velocity));
assert(false);
}
if (!position.atEdge && velocity.abs() >= position.scrollTolerances.velocity) {
return new ClampingScrollSimulation(
position: position.pixels,
velocity: velocity,
);
}
return null;
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 AbsoluteDragScrollActivity extends DragScrollActivity {
AbsoluteDragScrollActivity(
AbsoluteScrollPosition position,
class DragScrollActivity extends ScrollActivity {
DragScrollActivity(
ScrollPosition position,
DragStartDetails details,
this.scrollTolerances,
) : _lastDetails = details, super(position);
final Tolerance scrollTolerances;
@override
AbsoluteScrollPosition get position => super.position;
void touched() {
assert(false);
}
@override
void update(DragUpdateDetails details, { bool reverse }) {
assert(details.primaryDelta != null);
_lastDetails = details;
......@@ -554,7 +565,6 @@ class AbsoluteDragScrollActivity extends DragScrollActivity {
// because it gets reported via the reportOverscroll path.
}
@override
void end(DragEndDetails details, { bool reverse }) {
assert(details.primaryVelocity != null);
double velocity = details.primaryVelocity;
......@@ -570,6 +580,7 @@ class AbsoluteDragScrollActivity extends DragScrollActivity {
@override
void dispose() {
_lastDetails = null;
position.state.didEndDrag();
super.dispose();
}
......@@ -606,11 +617,11 @@ class AbsoluteDragScrollActivity extends DragScrollActivity {
bool get isScrolling => true;
}
class AbsoluteBallisticScrollActivity extends ScrollActivity {
class BallisticScrollActivity extends ScrollActivity {
///
/// The velocity should be in logical pixels per second.
AbsoluteBallisticScrollActivity(
AbsoluteScrollPosition position,
BallisticScrollActivity(
ScrollPosition position,
Simulation simulation,
TickerProvider vsync,
) : super(position) {
......@@ -625,7 +636,7 @@ class AbsoluteBallisticScrollActivity extends ScrollActivity {
}
@override
AbsoluteScrollPosition get position => super.position;
ScrollPosition get position => super.position;
double get velocity => _controller.velocity;
......@@ -678,8 +689,8 @@ class AbsoluteBallisticScrollActivity extends ScrollActivity {
}
}
class AbsoluteDrivenScrollActivity extends ScrollActivity {
AbsoluteDrivenScrollActivity(
class DrivenScrollActivity extends ScrollActivity {
DrivenScrollActivity(
ScrollPosition position, {
@required double from,
@required double to,
......@@ -704,7 +715,7 @@ class AbsoluteDrivenScrollActivity extends ScrollActivity {
}
@override
AbsoluteScrollPosition get position => super.position;
ScrollPosition get position => super.position;
Completer<Null> _completer;
AnimationController _controller;
......
......@@ -116,7 +116,8 @@ class ClampingScrollSimulation extends Simulation {
@required this.position,
@required this.velocity,
this.friction: 0.015,
}) {
Tolerance tolerance: Tolerance.defaultTolerance,
}) : super(tolerance: tolerance) {
_scaledFriction = friction * _decelerationForFriction(0.84); // See mPhysicalCoeff
_duration = _flingDuration(velocity);
_distance = _flingDistance(velocity);
......@@ -196,7 +197,9 @@ class ClampingScrollSimulation extends Simulation {
}
}
////////////////////////////////////////////////////////////////////////////////
// DELETE EVERYTHING BELOW THIS LINE WHEN REMOVING LEGACY SCROLLING CODE
////////////////////////////////////////////////////////////////////////////////
final SpringDescription _kScrollSpring = new SpringDescription.withDampingRatio(mass: 0.5, springConstant: 100.0, ratio: 1.1);
final double _kDrag = 0.025;
......
......@@ -7,8 +7,8 @@ import 'package:meta/meta.dart';
import 'framework.dart';
import 'basic.dart';
import 'page_scroll_physics.dart';
import 'scroll_absolute.dart';
import 'scroll_physics.dart';
import 'scroll_position.dart';
import 'scrollable.dart';
import 'sliver.dart';
import 'viewport.dart';
......
......@@ -23,383 +23,12 @@ import 'page_storage.dart';
import 'scroll_behavior.dart';
import 'scroll_configuration.dart';
import 'scroll_notification.dart';
import 'scroll_position.dart';
import 'ticker_provider.dart';
import 'viewport.dart';
// TODO(abarth): Merge AbsoluteScrollPosition and ScrollPosition.
import 'scroll_absolute.dart' show ViewportScrollBehavior, ScrollPhysics;
export 'package:flutter/physics.dart' show Tolerance;
// This file defines an unopinionated scrolling mechanism.
// See scroll_absolute.dart for variants that do things by pixels.
abstract class ScrollPosition extends ViewportOffset {
/// Create a new [ScrollPosition].
///
/// The first argument is the [Scrollable2State] object with which this scroll
/// position is associated. The second provides the tolerances for activities
/// that use simulations and need to decide when to end them. The final
/// argument is the previous instance of [ScrollPosition] that was being used
/// by the same [Scrollable2State], if any.
ScrollPosition(this.state, this.scrollTolerances, ScrollPosition oldPosition) {
assert(state is TickerProvider);
assert(scrollTolerances != null);
if (oldPosition != null)
absorb(oldPosition);
if (activity == null)
beginIdleActivity();
assert(activity != null);
assert(activity.position == this);
}
@protected
final Scrollable2State state;
final Tolerance scrollTolerances;
@protected
TickerProvider get vsync => state;
@protected
ScrollActivity get activity => _activity;
ScrollActivity _activity;
/// 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);
final bool oldIgnorePointer = shouldIgnorePointer;
_userScrollDirection = other._userScrollDirection;
other.activity._position = this;
_activity = other.activity;
other._activity = null;
if (oldIgnorePointer != shouldIgnorePointer)
state._updateIgnorePointer(shouldIgnorePointer);
if (other.runtimeType != runtimeType)
activity.resetActivity();
}
/// 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)
dispatchNotification(activity.createScrollEndNotification(state));
activity.dispose();
} else {
wasScrolling = false;
}
_activity = newActivity;
if (oldIgnorePointer != shouldIgnorePointer)
state._updateIgnorePointer(shouldIgnorePointer);
if (!activity.isScrolling)
updateUserScrollDirection(ScrollDirection.idle);
if (!wasScrolling && activity.isScrolling)
dispatchNotification(activity.createScrollStartNotification(state));
}
@protected
void dispatchNotification(Notification notification) {
assert(state.mounted);
notification.dispatch(state._gestureDetectorKey.currentContext);
}
@override
void dispose() {
activity?.dispose(); // it will be null if it got absorbed by another ScrollPosition
_activity = null;
super.dispose();
}
void touched() {
_activity.touched();
}
@override
@mustCallSuper
void applyViewportDimension(double viewportDimension) {
state._updateGestureDetectors(canDrag);
}
@override
@mustCallSuper
bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
state._updateGestureDetectors(canDrag);
return true;
}
/// 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;
dispatchNotification(new UserScrollNotification(scrollable: state, direction: value));
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('$activity');
description.add('$userScrollDirection');
}
bool get canDrag => false;
bool get shouldIgnorePointer => false;
@mustCallSuper
void postFrameCleanup() { }
void beginIdleActivity() {
beginActivity(new IdleScrollActivity(this));
}
DragScrollActivity beginDragActivity(DragStartDetails details) {
if (canDrag) {
throw new FlutterError(
'$runtimeType does not implement beginDragActivity but canDrag is true.\n'
'If a ScrollPosition class ever returns true from canDrag, then it must '
'implement the beginDragActivity method to handle drags.\n'
'The beginDragActivity method should call beginActivity, passing it a new '
'instance of a DragScrollActivity subclass that has been initialized with '
'this ScrollPosition object as its position.'
);
}
assert(false);
return null;
}
// ///
// /// The velocity should be in logical pixels per second.
void beginBallisticActivity(double velocity) {
beginIdleActivity();
}
// ABSTRACT METHODS
/// Update the scroll position ([pixels]) to a given pixel value.
///
/// This should only be called by the current [ScrollActivity], either during
/// the transient callback phase or in response to user input.
///
/// Returns the overscroll, if any. If the return value is 0.0, that means
/// that [pixels] now returns the given `value`. If the return value is
/// positive, then [pixels] is less than the requested `value` by the given
/// amount (overscroll past the max extent), and if it is negative, it is
/// 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);
/// 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).
ScrollableMetrics getMetrics();
// Subclasses must also implement the [pixels] getter and [correctBy].
}
/// Base class for scrolling activities like dragging, and flinging.
abstract class ScrollActivity {
ScrollActivity(ScrollPosition position) {
_position = 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(Scrollable2State scrollable) {
return new ScrollStartNotification(scrollable: scrollable);
}
Notification createScrollUpdateNotification(Scrollable2State scrollable, double scrollDelta) {
return new ScrollUpdateNotification(scrollable: scrollable, scrollDelta: scrollDelta);
}
Notification createOverscrollNotification(Scrollable2State scrollable, double overscroll) {
return new OverscrollNotification(scrollable: scrollable, overscroll: overscroll);
}
Notification createScrollEndNotification(Scrollable2State 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;
}
abstract class DragScrollActivity extends ScrollActivity {
DragScrollActivity(ScrollPosition position) : super(position);
void update(DragUpdateDetails details, { bool reverse });
void end(DragEndDetails details, { bool reverse });
@override
void touched() {
assert(false);
}
@override
void dispose() {
position.state._drag = null;
super.dispose();
}
}
/// Base class for delegates that instantiate [ScrollPosition] objects.
abstract class ScrollBehavior2 {
const ScrollBehavior2();
Widget wrap(BuildContext context, Widget child, AxisDirection axisDirection);
/// Returns a new instance of the ScrollPosition class that this
/// ScrollBehavior2 subclass creates.
///
/// A given ScrollBehavior2 object must always return the same kind of
/// ScrollPosition, with the same configuration.
///
/// The `oldPosition` argument should be passed to the `ScrollPosition`
/// constructor so that the new position can take over the old position's
/// offset and (if it's the same type) activity.
///
/// When calling [createScrollPosition] with a non-null `oldPosition`, that
/// object must be disposed (via [ScrollPosition.oldPosition]) in the same
/// call stack. Passing a non-null `oldPosition` is a destructive operation
/// for that [ScrollPosition].
ScrollPosition createScrollPosition(BuildContext context, Scrollable2State state, ScrollPosition oldPosition, ScrollPhysics physics);
/// Whether this delegate is different than the old delegate, or would now
/// return meaningfully different widgets from [wrap] or a meaningfully
/// different [ScrollPosition] from [createScrollPosition].
///
/// It is not necessary to return true if the return values for [wrap] and
/// [createScrollPosition] would only be different because of depending on the
/// [BuildContext] argument they are provided, as dependency checking is
/// handled separately.
bool shouldNotify(@checked ScrollBehavior2 oldDelegate);
@override
String toString() => '$runtimeType';
}
class ScrollConfiguration2 extends InheritedWidget {
const ScrollConfiguration2({
Key key,
@required this.delegate,
@required Widget child,
}) : super(key: key, child: child);
final ScrollBehavior2 delegate;
static ScrollBehavior2 of(BuildContext context) {
ScrollConfiguration2 configuration = context.inheritFromWidgetOfExactType(ScrollConfiguration2);
return configuration?.delegate;
}
@override
bool updateShouldNotify(ScrollConfiguration2 old) {
assert(delegate != null);
return delegate.runtimeType != old.delegate.runtimeType
|| delegate.shouldNotify(old.delegate);
}
}
typedef Widget ViewportBuilder(BuildContext context, ViewportOffset position);
class Scrollable2 extends StatefulWidget {
......@@ -407,7 +36,6 @@ class Scrollable2 extends StatefulWidget {
Key key,
this.axisDirection: AxisDirection.down,
this.physics,
this.scrollBehavior,
@required this.viewportBuilder,
}) : super (key: key) {
assert(axisDirection != null);
......@@ -418,14 +46,6 @@ class Scrollable2 extends StatefulWidget {
final ScrollPhysics physics;
/// The delegate that creates the [ScrollPosition] and wraps the viewport
/// in extra widgets (e.g. for overscroll effects).
///
/// If no scroll behavior delegate is explicitly supplied, the scroll behavior
/// from the nearest [ScrollConfiguration2] is used. If there is no
/// [ScrollConfiguration2] in scope, a new [ViewportScrollBehavior] is used.
final ScrollBehavior2 scrollBehavior;
final ViewportBuilder viewportBuilder;
Axis get axis => axisDirectionToAxis(axisDirection);
......@@ -433,29 +53,12 @@ class Scrollable2 extends StatefulWidget {
@override
Scrollable2State createState() => new Scrollable2State();
static ScrollBehavior2 getScrollBehavior(BuildContext context) {
return ScrollConfiguration2.of(context)
?? new ViewportScrollBehavior();
}
/// Whether, when this widget has been replaced by another, the scroll
/// behavior and scroll position need to be updated as well.
bool shouldUpdateScrollPosition(Scrollable2 oldWidget) {
return scrollBehavior.runtimeType != oldWidget.scrollBehavior.runtimeType
|| (scrollBehavior != null && scrollBehavior.shouldNotify(oldWidget.scrollBehavior));
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('$axisDirection');
if (physics != null)
description.add('physics: $physics');
if (scrollBehavior != null) {
description.add('scrollBehavior: $scrollBehavior');
} else {
description.add('scrollBehavior: use inherited ScrollBehavior2');
}
}
}
......@@ -468,24 +71,28 @@ class Scrollable2 extends StatefulWidget {
/// [NotificationListener] to listen for [ScrollNotification2] notifications.
///
/// This class is not intended to be subclassed. To specialize the behavior of a
/// [Scrollable2], provide it with a custom [ScrollBehavior2] delegate.
class Scrollable2State extends State<Scrollable2> with TickerProviderStateMixin {
/// [Scrollable2], provide it with a [ScrollPhysics].
class Scrollable2State extends State<Scrollable2> with TickerProviderStateMixin
implements AbstractScrollState {
/// The controller for this [Scrollable2] widget's viewport position.
///
/// To control what kind of [ScrollPosition] is created for a [Scrollable2],
/// provide it with a custom [ScrollBehavior2] delegate that creates the
/// appropriate [ScrollPosition] controller in its
/// [ScrollBehavior2.createScrollPosition] method.
/// provide it with custom [ScrollPhysics] that creates the appropriate
/// [ScrollPosition] controller in its [ScrollPhysics.createScrollPosition]
/// method.
ScrollPosition get position => _position;
ScrollPosition _position;
ScrollBehavior2 _scrollBehavior;
ScrollBehavior2 _configuration;
// only call this from places that will definitely trigger a rebuild
void _updatePosition() {
_scrollBehavior = config.scrollBehavior ?? Scrollable2.getScrollBehavior(context);
_configuration = ScrollConfiguration2.of(context);
ScrollPhysics physics = _configuration.getScrollPhysics(context);
if (config.physics != null)
physics = config.physics.applyTo(physics);
final ScrollPosition oldPosition = position;
_position = _scrollBehavior.createScrollPosition(context, this, oldPosition, config.physics);
_position = physics.createScrollPosition(physics, this, oldPosition);
assert(position != null);
if (oldPosition != null) {
// It's important that we not do this until after the viewport has had a
......@@ -501,10 +108,19 @@ class Scrollable2State extends State<Scrollable2> with TickerProviderStateMixin
_updatePosition();
}
bool _shouldUpdatePosition(Scrollable2 oldConfig) {
if (config.physics == oldConfig.physics)
return false;
if ((config.physics == null) != (oldConfig.physics == null))
return true;
return config.physics.runtimeType != oldConfig.physics.runtimeType
|| config.physics.shouldUpdateScrollPosition(oldConfig.physics);
}
@override
void didUpdateConfig(Scrollable2 oldConfig) {
super.didUpdateConfig(oldConfig);
if (config.shouldUpdateScrollPosition(oldConfig))
if (_shouldUpdatePosition(oldConfig))
_updatePosition();
}
......@@ -527,7 +143,9 @@ class Scrollable2State extends State<Scrollable2> with TickerProviderStateMixin
bool _lastCanDrag;
Axis _lastAxisDirection;
void _updateGestureDetectors(bool canDrag) {
@override
@protected
void setCanDrag(bool canDrag) {
if (canDrag == _lastCanDrag && (!canDrag || config.axis == _lastAxisDirection))
return;
if (!canDrag) {
......@@ -564,7 +182,12 @@ class Scrollable2State extends State<Scrollable2> with TickerProviderStateMixin
_gestureDetectorKey.currentState.replaceGestureRecognizers(_gestureRecognizers);
}
void _updateIgnorePointer(bool value) {
@override
TickerProvider get vsync => this;
@override
@protected
void setIgnorePointer(bool value) {
if (_shouldIgnorePointer == value)
return;
_shouldIgnorePointer = value;
......@@ -574,6 +197,18 @@ class Scrollable2State extends State<Scrollable2> with TickerProviderStateMixin
}
}
@override
@protected
void didEndDrag() {
_drag = null;
}
@override
@protected
void dispatchNotification(Notification notification) {
assert(mounted);
notification.dispatch(_gestureDetectorKey.currentContext);
}
// TOUCH HANDLERS
......@@ -630,7 +265,7 @@ class Scrollable2State extends State<Scrollable2> with TickerProviderStateMixin
child: config.viewportBuilder(context, position),
),
);
return _scrollBehavior.wrap(context, result, config.axisDirection);
return _configuration.buildViewportChrome(context, result, config.axisDirection);
}
@override
......@@ -640,8 +275,9 @@ class Scrollable2State extends State<Scrollable2> with TickerProviderStateMixin
}
}
////////////////////////////////////////////////////////////////////////////////
// DELETE EVERYTHING BELOW THIS LINE WHEN REMOVING LEGACY SCROLLING CODE
////////////////////////////////////////////////////////////////////////////////
/// Identifies one or both limits of a [Scrollable] in terms of its scrollDirection.
enum ScrollableEdge {
......
......@@ -46,10 +46,11 @@ export 'src/widgets/performance_overlay.dart';
export 'src/widgets/placeholder.dart';
export 'src/widgets/raw_keyboard_listener.dart';
export 'src/widgets/routes.dart';
export 'src/widgets/scroll_absolute.dart';
export 'src/widgets/scroll_behavior.dart';
export 'src/widgets/scroll_configuration.dart';
export 'src/widgets/scroll_notification.dart';
export 'src/widgets/scroll_physics.dart';
export 'src/widgets/scroll_position.dart';
export 'src/widgets/scroll_simulation.dart';
export 'src/widgets/scroll_view.dart';
export 'src/widgets/scrollable.dart';
......
......@@ -72,7 +72,7 @@ void main() {
log.clear();
Scrollable2State state = tester.state(find.byType(Scrollable2));
AbsoluteScrollPosition position = state.position;
ScrollPosition position = state.position;
position.jumpTo(2025.0);
expect(log, isEmpty);
......
......@@ -204,12 +204,14 @@ void main() {
RenderObject painter;
await tester.pumpWidget(
new TestScrollable(
axisDirection: AxisDirection.left,
scrollBehavior: new TestScrollBehavior1(),
slivers: <Widget>[
new SliverToBoxAdapter(child: new SizedBox(height: 20.0)),
],
new ScrollConfiguration2(
behavior: new TestScrollBehavior1(),
child: new TestScrollable(
axisDirection: AxisDirection.left,
slivers: <Widget>[
new SliverToBoxAdapter(child: new SizedBox(height: 20.0)),
],
),
),
);
painter = tester.renderObject(find.byType(CustomPaint));
......@@ -219,12 +221,14 @@ void main() {
await tester.pumpUntilNoTransientCallbacks(const Duration(seconds: 1));
await tester.pumpWidget(
new TestScrollable(
axisDirection: AxisDirection.right,
scrollBehavior: new TestScrollBehavior2(),
slivers: <Widget>[
new SliverToBoxAdapter(child: new SizedBox(height: 20.0)),
],
new ScrollConfiguration2(
behavior: new TestScrollBehavior2(),
child: new TestScrollable(
axisDirection: AxisDirection.right,
slivers: <Widget>[
new SliverToBoxAdapter(child: new SizedBox(height: 20.0)),
],
),
),
);
painter = tester.renderObject(find.byType(CustomPaint));
......@@ -234,14 +238,14 @@ void main() {
});
}
class TestScrollBehavior1 extends ViewportScrollBehavior {
class TestScrollBehavior1 extends ScrollBehavior2 {
@override
Color getGlowColor(BuildContext context) {
return const Color(0xFF00FF00);
}
}
class TestScrollBehavior2 extends ViewportScrollBehavior {
class TestScrollBehavior2 extends ScrollBehavior2 {
@override
Color getGlowColor(BuildContext context) {
return const Color(0xFF0000FF);
......
......@@ -158,7 +158,7 @@ void main() {
Scrollable2State state = tester.state(find.byType(Scrollable2));
AbsoluteScrollPosition position = state.position;
ScrollPosition position = state.position;
position.jumpTo(3025.0);
expect(log, isEmpty);
......
......@@ -10,13 +10,13 @@ import 'test_widgets.dart';
class TestScrollPosition extends ScrollPosition {
TestScrollPosition(
this.extentMultiplier,
Scrollable2State state,
Tolerance scrollTolerances,
TestScrollPhysics physics,
AbstractScrollState state,
ScrollPosition oldPosition,
) : _pixels = 100.0, super(state, scrollTolerances, oldPosition);
) : _pixels = 100.0, super(physics, state, oldPosition);
final double extentMultiplier;
@override
TestScrollPhysics get physics => super.physics;
double _min, _viewport, _max, _pixels;
......@@ -27,7 +27,7 @@ class TestScrollPosition extends ScrollPosition {
double setPixels(double value) {
double oldPixels = _pixels;
_pixels = value;
dispatchNotification(activity.createScrollUpdateNotification(state, _pixels - oldPixels));
state.dispatchNotification(activity.createScrollUpdateNotification(state, _pixels - oldPixels));
return 0.0;
}
......@@ -56,9 +56,9 @@ class TestScrollPosition extends ScrollPosition {
double afterExtent = _max - _pixels;
if (insideExtent > 0.0) {
return new ScrollableMetrics(
extentBefore: extentMultiplier * beforeExtent / insideExtent,
extentInside: extentMultiplier,
extentAfter: extentMultiplier * afterExtent / insideExtent,
extentBefore: physics.extentMultiplier * beforeExtent / insideExtent,
extentInside: physics.extentMultiplier,
extentAfter: physics.extentMultiplier * afterExtent / insideExtent,
);
} else {
return new ScrollableMetrics(
......@@ -70,17 +70,36 @@ class TestScrollPosition extends ScrollPosition {
}
}
class TestScrollPhysics extends ScrollPhysics {
const TestScrollPhysics({ ScrollPhysics parent, this.extentMultiplier }) : super(parent);
final double extentMultiplier;
@override
TestScrollPhysics applyTo(ScrollPhysics parent) {
return new TestScrollPhysics(parent: parent, extentMultiplier: extentMultiplier);
}
@override
ScrollPosition createScrollPosition(ScrollPhysics physics, AbstractScrollState state, ScrollPosition oldPosition) {
return new TestScrollPosition(physics, state, oldPosition);
}
}
class TestScrollBehavior extends ScrollBehavior2 {
TestScrollBehavior(this.extentMultiplier);
final double extentMultiplier;
@override
Widget wrap(BuildContext context, Widget child, AxisDirection axisDirection) => child;
ScrollPhysics getScrollPhysics(BuildContext context) {
return new TestScrollPhysics(
extentMultiplier: extentMultiplier
).applyTo(super.getScrollPhysics(context));
}
@override
ScrollPosition createScrollPosition(BuildContext context, Scrollable2State state, ScrollPosition oldPosition, ScrollPhysics physics) {
return new TestScrollPosition(extentMultiplier, state, ViewportScrollBehavior.defaultScrollTolerances, oldPosition);
}
Widget buildViewportChrome(BuildContext context, Widget child, AxisDirection axisDirection) => child;
@override
bool shouldNotify(TestScrollBehavior oldDelegate) {
......@@ -90,20 +109,24 @@ class TestScrollBehavior extends ScrollBehavior2 {
void main() {
testWidgets('Changing the scroll behavior dynamically', (WidgetTester tester) async {
await tester.pumpWidget(new TestScrollable(
scrollBehavior: new TestScrollBehavior(1.0),
slivers: <Widget>[
new SliverToBoxAdapter(child: new SizedBox(height: 2000.0)),
],
await tester.pumpWidget(new ScrollConfiguration2(
behavior: new TestScrollBehavior(1.0),
child: new TestScrollable(
slivers: <Widget>[
new SliverToBoxAdapter(child: new SizedBox(height: 2000.0)),
],
),
));
Scrollable2State state = tester.state(find.byType(Scrollable2));
expect(state.position.getMetrics().extentInside, 1.0);
await tester.pumpWidget(new TestScrollable(
scrollBehavior: new TestScrollBehavior(2.0),
slivers: <Widget>[
new SliverToBoxAdapter(child: new SizedBox(height: 2000.0)),
],
await tester.pumpWidget(new ScrollConfiguration2(
behavior: new TestScrollBehavior(2.0),
child: new TestScrollable(
slivers: <Widget>[
new SliverToBoxAdapter(child: new SizedBox(height: 2000.0)),
],
),
));
expect(state.position.getMetrics().extentInside, 2.0);
});
......
......@@ -32,7 +32,7 @@ double getScrollOffset(WidgetTester tester) {
void resetScrollOffset(WidgetTester tester) {
RenderViewport2 viewport = tester.renderObject(find.byType(Viewport2));
AbsoluteScrollPosition position = viewport.offset;
ScrollPosition position = viewport.offset;
position.jumpTo(0.0);
}
......
......@@ -37,7 +37,7 @@ void main() {
],
),
);
AbsoluteScrollPosition position = tester.state<Scrollable2State>(find.byType(Scrollable2)).position;
ScrollPosition position = tester.state<Scrollable2State>(find.byType(Scrollable2)).position;
final double max = bigHeight * 2.0 + new TestDelegate().maxExtent - 600.0; // 600 is the height of the test viewport
assert(max < 10000.0);
expect(max, 1600.0);
......@@ -65,7 +65,7 @@ void main() {
],
),
);
AbsoluteScrollPosition position = tester.state<Scrollable2State>(find.byType(Scrollable2)).position;
ScrollPosition position = tester.state<Scrollable2State>(find.byType(Scrollable2)).position;
verifyPaintPosition(key1, new Offset(0.0, 0.0), true);
verifyPaintPosition(key2, new Offset(0.0, 600.0), false);
......@@ -135,7 +135,7 @@ void main() {
],
),
);
AbsoluteScrollPosition position = tester.state<Scrollable2State>(find.byType(Scrollable2)).position;
ScrollPosition position = tester.state<Scrollable2State>(find.byType(Scrollable2)).position;
verifyPaintPosition(key1, new Offset(0.0, 0.0), true);
verifyPaintPosition(key2, new Offset(0.0, 600.0), false);
......@@ -168,7 +168,7 @@ void main() {
],
),
);
AbsoluteScrollPosition position = tester.state<Scrollable2State>(find.byType(Scrollable2)).position;
ScrollPosition position = tester.state<Scrollable2State>(find.byType(Scrollable2)).position;
verifyPaintPosition(key1, new Offset(0.0, 0.0), true);
verifyPaintPosition(key2, new Offset(0.0, 600.0), false);
......
......@@ -40,7 +40,7 @@ void main() {
],
),
);
AbsoluteScrollPosition position = tester.state<Scrollable2State>(find.byType(Scrollable2)).position;
ScrollPosition position = tester.state<Scrollable2State>(find.byType(Scrollable2)).position;
final double max = bigHeight * 3.0 + new TestDelegate().maxExtent * 2.0 - 600.0; // 600 is the height of the test viewport
assert(max < 10000.0);
expect(max, 1450.0);
......@@ -74,7 +74,7 @@ void main() {
],
),
);
AbsoluteScrollPosition position = tester.state<Scrollable2State>(find.byType(Scrollable2)).position;
ScrollPosition position = tester.state<Scrollable2State>(find.byType(Scrollable2)).position;
verifyPaintPosition(key1, new Offset(0.0, 0.0), true);
verifyPaintPosition(key2, new Offset(0.0, 550.0), true);
verifyPaintPosition(key3, new Offset(0.0, 600.0), false);
......@@ -164,7 +164,7 @@ void main() {
],
),
);
AbsoluteScrollPosition position = tester.state<Scrollable2State>(find.byType(Scrollable2)).position;
ScrollPosition position = tester.state<Scrollable2State>(find.byType(Scrollable2)).position;
final double max = bigHeight * 3.0 + new TestDelegate().maxExtent * 2.0 - 600.0; // 600 is the height of the test viewport
assert(max < 10000.0);
expect(max, 1750.0);
......
......@@ -31,7 +31,7 @@ void main() {
],
),
);
AbsoluteScrollPosition position = tester.state<Scrollable2State>(find.byType(Scrollable2)).position;
ScrollPosition position = tester.state<Scrollable2State>(find.byType(Scrollable2)).position;
final double max = RenderBigSliver.height * 3.0 + new TestDelegate().maxExtent * 2.0 - 600.0; // 600 is the height of the test viewport
assert(max < 10000.0);
expect(max, 1450.0);
......@@ -64,7 +64,7 @@ void main() {
],
),
);
AbsoluteScrollPosition position = tester.state<Scrollable2State>(find.byType(Scrollable2)).position;
ScrollPosition position = tester.state<Scrollable2State>(find.byType(Scrollable2)).position;
position.animate(to: RenderBigSliver.height + delegate.maxExtent - 5.0, curve: Curves.linear, duration: const Duration(minutes: 1));
await tester.pumpUntilNoTransientCallbacks(const Duration(milliseconds: 1000));
RenderBox box = tester.renderObject<RenderBox>(find.byType(Container));
......
......@@ -33,35 +33,31 @@ class TestSliverAppBarDelegate extends SliverAppBarDelegate {
class TestBehavior extends ScrollBehavior2 {
@override
Widget wrap(BuildContext context, Widget child, AxisDirection axisDirection) {
Widget buildViewportChrome(BuildContext context, Widget child, AxisDirection axisDirection) {
return new GlowingOverscrollIndicator(
child: child,
axisDirection: axisDirection,
color: const Color(0xFFFFFFFF),
);
}
}
class TestScrollPhysics extends ClampingScrollPhysics {
const TestScrollPhysics({ ScrollPhysics parent }) : super(parent: parent);
@override
ScrollPosition createScrollPosition(BuildContext context, Scrollable2State state, ScrollPosition oldPosition, ScrollPhysics physics) {
return new TestViewportScrollPosition(
state,
new Tolerance(velocity: 20.0, distance: 1.0),
oldPosition,
physics,
);
}
TestScrollPhysics applyTo(ScrollPhysics parent) => new TestScrollPhysics(parent: parent);
@override
bool shouldNotify(TestBehavior oldDelegate) => false;
Tolerance get tolerance => new Tolerance(velocity: 20.0, distance: 1.0);
}
class TestViewportScrollPosition extends AbsoluteScrollPosition {
class TestViewportScrollPosition extends ScrollPosition {
TestViewportScrollPosition(
Scrollable2State state,
Tolerance scrollTolerances,
ScrollPosition oldPosition,
ScrollPhysics physics,
) : super(state, scrollTolerances, oldPosition, physics);
AbstractScrollState state,
ScrollPosition oldPosition,
) : super(physics, state, oldPosition);
@override
bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
......@@ -75,85 +71,87 @@ void main() {
testWidgets('Evil test of sliver features - 1', (WidgetTester tester) async {
final GlobalKey centerKey = new GlobalKey();
await tester.pumpWidget(
new Scrollbar2(
child: new TestScrollable(
axisDirection: AxisDirection.down,
center: centerKey,
anchor: 0.25,
physics: const ClampingScrollPhysics(),
scrollBehavior: new TestBehavior(),
slivers: <Widget>[
new SliverToBoxAdapter(child: new Container(height: 5.0)),
new SliverToBoxAdapter(child: new Container(height: 520.0)),
new SliverToBoxAdapter(child: new Container(height: 520.0)),
new SliverToBoxAdapter(child: new Container(height: 520.0)),
new SliverAppBar(delegate: new TestSliverAppBarDelegate(150.0), pinned: true),
new SliverToBoxAdapter(child: new Container(height: 520.0)),
new SliverPadding(
padding: new EdgeInsets.all(50.0),
child: new SliverToBoxAdapter(child: new Container(height: 520.0)),
),
new SliverToBoxAdapter(child: new Container(height: 520.0)),
new SliverAppBar(delegate: new TestSliverAppBarDelegate(150.0), floating: true),
new SliverToBoxAdapter(child: new Container(height: 520.0)),
new SliverToBoxAdapter(key: centerKey, child: new Container(height: 520.0)), // ------------------------ CENTER ------------------------
new SliverAppBar(delegate: new TestSliverAppBarDelegate(150.0), pinned: true),
new SliverToBoxAdapter(child: new Container(height: 520.0)),
new SliverToBoxAdapter(child: new Container(height: 520.0)),
new SliverToBoxAdapter(child: new Container(height: 520.0)),
new SliverPadding(
padding: new EdgeInsets.all(50.0),
child: new SliverAppBar(delegate: new TestSliverAppBarDelegate(250.0), pinned: true),
),
new SliverToBoxAdapter(child: new Container(height: 520.0)),
new SliverAppBar(delegate: new TestSliverAppBarDelegate(250.0), pinned: true),
new SliverToBoxAdapter(child: new Container(height: 5.0)),
new SliverAppBar(delegate: new TestSliverAppBarDelegate(250.0), pinned: true),
new SliverToBoxAdapter(child: new Container(height: 5.0)),
new SliverAppBar(delegate: new TestSliverAppBarDelegate(250.0), pinned: true),
new SliverAppBar(delegate: new TestSliverAppBarDelegate(250.0), pinned: true),
new SliverToBoxAdapter(child: new Container(height: 5.0)),
new SliverAppBar(delegate: new TestSliverAppBarDelegate(250.0), pinned: true),
new SliverAppBar(delegate: new TestSliverAppBarDelegate(250.0)),
new SliverToBoxAdapter(child: new Container(height: 520.0)),
new SliverAppBar(delegate: new TestSliverAppBarDelegate(150.0), floating: true),
new SliverToBoxAdapter(child: new Container(height: 520.0)),
new SliverAppBar(delegate: new TestSliverAppBarDelegate(150.0), floating: true),
new SliverToBoxAdapter(child: new Container(height: 5.0)),
new SliverBlock(
delegate: new SliverChildListDelegate(<Widget>[
new Container(height: 50.0),
new Container(height: 50.0),
new Container(height: 50.0),
new Container(height: 50.0),
new Container(height: 50.0),
new Container(height: 50.0),
new Container(height: 50.0),
new Container(height: 50.0),
new Container(height: 50.0),
new Container(height: 50.0),
new Container(height: 50.0),
new Container(height: 50.0),
new Container(height: 50.0),
new Container(height: 50.0),
new Container(height: 50.0),
]),
),
new SliverAppBar(delegate: new TestSliverAppBarDelegate(250.0)),
new SliverAppBar(delegate: new TestSliverAppBarDelegate(250.0)),
new SliverAppBar(delegate: new TestSliverAppBarDelegate(250.0)),
new SliverPadding(
padding: new EdgeInsets.symmetric(horizontal: 50.0),
child: new SliverToBoxAdapter(child: new Container(height: 520.0)),
),
new SliverToBoxAdapter(child: new Container(height: 520.0)),
new SliverToBoxAdapter(child: new Container(height: 520.0)),
new SliverToBoxAdapter(child: new Container(height: 5.0)),
],
new ScrollConfiguration2(
behavior: new TestBehavior(),
child: new Scrollbar2(
child: new TestScrollable(
axisDirection: AxisDirection.down,
center: centerKey,
anchor: 0.25,
physics: const TestScrollPhysics(),
slivers: <Widget>[
new SliverToBoxAdapter(child: new Container(height: 5.0)),
new SliverToBoxAdapter(child: new Container(height: 520.0)),
new SliverToBoxAdapter(child: new Container(height: 520.0)),
new SliverToBoxAdapter(child: new Container(height: 520.0)),
new SliverAppBar(delegate: new TestSliverAppBarDelegate(150.0), pinned: true),
new SliverToBoxAdapter(child: new Container(height: 520.0)),
new SliverPadding(
padding: new EdgeInsets.all(50.0),
child: new SliverToBoxAdapter(child: new Container(height: 520.0)),
),
new SliverToBoxAdapter(child: new Container(height: 520.0)),
new SliverAppBar(delegate: new TestSliverAppBarDelegate(150.0), floating: true),
new SliverToBoxAdapter(child: new Container(height: 520.0)),
new SliverToBoxAdapter(key: centerKey, child: new Container(height: 520.0)), // ------------------------ CENTER ------------------------
new SliverAppBar(delegate: new TestSliverAppBarDelegate(150.0), pinned: true),
new SliverToBoxAdapter(child: new Container(height: 520.0)),
new SliverToBoxAdapter(child: new Container(height: 520.0)),
new SliverToBoxAdapter(child: new Container(height: 520.0)),
new SliverPadding(
padding: new EdgeInsets.all(50.0),
child: new SliverAppBar(delegate: new TestSliverAppBarDelegate(250.0), pinned: true),
),
new SliverToBoxAdapter(child: new Container(height: 520.0)),
new SliverAppBar(delegate: new TestSliverAppBarDelegate(250.0), pinned: true),
new SliverToBoxAdapter(child: new Container(height: 5.0)),
new SliverAppBar(delegate: new TestSliverAppBarDelegate(250.0), pinned: true),
new SliverToBoxAdapter(child: new Container(height: 5.0)),
new SliverAppBar(delegate: new TestSliverAppBarDelegate(250.0), pinned: true),
new SliverAppBar(delegate: new TestSliverAppBarDelegate(250.0), pinned: true),
new SliverToBoxAdapter(child: new Container(height: 5.0)),
new SliverAppBar(delegate: new TestSliverAppBarDelegate(250.0), pinned: true),
new SliverAppBar(delegate: new TestSliverAppBarDelegate(250.0)),
new SliverToBoxAdapter(child: new Container(height: 520.0)),
new SliverAppBar(delegate: new TestSliverAppBarDelegate(150.0), floating: true),
new SliverToBoxAdapter(child: new Container(height: 520.0)),
new SliverAppBar(delegate: new TestSliverAppBarDelegate(150.0), floating: true),
new SliverToBoxAdapter(child: new Container(height: 5.0)),
new SliverBlock(
delegate: new SliverChildListDelegate(<Widget>[
new Container(height: 50.0),
new Container(height: 50.0),
new Container(height: 50.0),
new Container(height: 50.0),
new Container(height: 50.0),
new Container(height: 50.0),
new Container(height: 50.0),
new Container(height: 50.0),
new Container(height: 50.0),
new Container(height: 50.0),
new Container(height: 50.0),
new Container(height: 50.0),
new Container(height: 50.0),
new Container(height: 50.0),
new Container(height: 50.0),
]),
),
new SliverAppBar(delegate: new TestSliverAppBarDelegate(250.0)),
new SliverAppBar(delegate: new TestSliverAppBarDelegate(250.0)),
new SliverAppBar(delegate: new TestSliverAppBarDelegate(250.0)),
new SliverPadding(
padding: new EdgeInsets.symmetric(horizontal: 50.0),
child: new SliverToBoxAdapter(child: new Container(height: 520.0)),
),
new SliverToBoxAdapter(child: new Container(height: 520.0)),
new SliverToBoxAdapter(child: new Container(height: 520.0)),
new SliverToBoxAdapter(child: new Container(height: 5.0)),
],
),
),
),
);
AbsoluteScrollPosition position = tester.state<Scrollable2State>(find.byType(Scrollable2)).position;
ScrollPosition position = tester.state<Scrollable2State>(find.byType(Scrollable2)).position;
position.animate(to: 10000.0, curve: Curves.linear, duration: const Duration(minutes: 1));
await tester.pump(const Duration(milliseconds: 10));
......
......@@ -33,7 +33,7 @@ void main() {
],
),
);
AbsoluteScrollPosition position = tester.state<Scrollable2State>(find.byType(Scrollable2)).position;
ScrollPosition position = tester.state<Scrollable2State>(find.byType(Scrollable2)).position;
final double max = RenderBigSliver.height * 3.0 + (RenderOverlappingSliver.totalHeight) * 2.0 - 600.0; // 600 is the height of the test viewport
assert(max < 10000.0);
expect(max, 1450.0);
......
......@@ -65,7 +65,6 @@ class TestScrollable extends StatelessWidget {
this.physics,
this.anchor: 0.0,
this.center,
this.scrollBehavior,
this.slivers: const <Widget>[],
}) {
assert(slivers != null);
......@@ -79,8 +78,6 @@ class TestScrollable extends StatelessWidget {
final Key center;
final ScrollBehavior2 scrollBehavior;
final List<Widget> slivers;
Axis get axis => axisDirectionToAxis(axisDirection);
......@@ -90,7 +87,6 @@ class TestScrollable extends StatelessWidget {
return new Scrollable2(
axisDirection: axisDirection,
physics: physics,
scrollBehavior: scrollBehavior,
viewportBuilder: (BuildContext context, ViewportOffset offset) {
return new Viewport2(
axisDirection: axisDirection,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment