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 { ...@@ -214,7 +214,7 @@ class _ScrollLikeMountainViewDelegate extends ScrollConfigurationDelegate {
bool updateShouldNotify(ScrollConfigurationDelegate old) => false; bool updateShouldNotify(ScrollConfigurationDelegate old) => false;
} }
class _MaterialScrollBehavior extends ViewportScrollBehavior { class _MaterialScrollBehavior extends ScrollBehavior2 {
@override @override
TargetPlatform getPlatform(BuildContext context) { TargetPlatform getPlatform(BuildContext context) {
return Theme.of(context).platform; return Theme.of(context).platform;
...@@ -308,7 +308,7 @@ class _MaterialAppState extends State<MaterialApp> { ...@@ -308,7 +308,7 @@ class _MaterialAppState extends State<MaterialApp> {
); );
return new ScrollConfiguration2( return new ScrollConfiguration2(
delegate: new _MaterialScrollBehavior(), behavior: new _MaterialScrollBehavior(),
child: result child: result
); );
} }
......
...@@ -2,7 +2,9 @@ ...@@ -2,7 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
////////////////////////////////////////////////////////////////////////////////
// DELETE THIS FILE WHEN REMOVING LEGACY SCROLLING CODE // DELETE THIS FILE WHEN REMOVING LEGACY SCROLLING CODE
////////////////////////////////////////////////////////////////////////////////
import 'dart:async' show Timer; import 'dart:async' show Timer;
import 'dart:math' as math; import 'dart:math' as math;
......
...@@ -180,8 +180,9 @@ class _ScrollbarPainter extends CustomPainter { ...@@ -180,8 +180,9 @@ class _ScrollbarPainter extends CustomPainter {
} }
} }
////////////////////////////////////////////////////////////////////////////////
// DELETE EVERYTHING BELOW THIS LINE WHEN REMOVING LEGACY SCROLLING CODE // DELETE EVERYTHING BELOW THIS LINE WHEN REMOVING LEGACY SCROLLING CODE
////////////////////////////////////////////////////////////////////////////////
const double _kMinScrollbarThumbExtent = 18.0; const double _kMinScrollbarThumbExtent = 18.0;
const double _kScrollbarThumbGirth = 6.0; const double _kScrollbarThumbGirth = 6.0;
......
...@@ -30,6 +30,8 @@ import 'tolerance.dart'; ...@@ -30,6 +30,8 @@ import 'tolerance.dart';
/// should establish a convention and use that convention consistently with all /// should establish a convention and use that convention consistently with all
/// related objects. /// related objects.
abstract class Simulation { abstract class Simulation {
Simulation({ this.tolerance: Tolerance.defaultTolerance });
/// The position of the object in the simulation at the given time. /// The position of the object in the simulation at the given time.
double x(double time); double x(double time);
...@@ -46,7 +48,7 @@ abstract class Simulation { ...@@ -46,7 +48,7 @@ abstract class Simulation {
/// but once the difference from the value at a particular time and the /// 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 /// 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 defines how to determine if the difference could not be seen.
Tolerance tolerance = Tolerance.defaultTolerance; Tolerance tolerance;
@override @override
String toString() => '$runtimeType'; String toString() => '$runtimeType';
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'simulation.dart'; import 'simulation.dart';
import 'tolerance.dart';
import 'utils.dart'; import 'utils.dart';
/// Structure that describes a spring's constants. /// Structure that describes a spring's constants.
...@@ -86,12 +87,14 @@ class SpringSimulation extends Simulation { ...@@ -86,12 +87,14 @@ class SpringSimulation extends Simulation {
/// arbitrary unit of length, and T is the time unit used for driving the /// arbitrary unit of length, and T is the time unit used for driving the
/// [SpringSimulation]. /// [SpringSimulation].
SpringSimulation( SpringSimulation(
SpringDescription desc, SpringDescription spring,
double start, double start,
double end, double end,
double velocity double velocity, {
) : _endPosition = end, Tolerance tolerance: Tolerance.defaultTolerance,
_solution = new _SpringSolution(desc, start - end, velocity); }) : _endPosition = end,
_solution = new _SpringSolution(spring, start - end, velocity),
super(tolerance: tolerance);
final double _endPosition; final double _endPosition;
final _SpringSolution _solution; final _SpringSolution _solution;
...@@ -124,11 +127,12 @@ class ScrollSpringSimulation extends SpringSimulation { ...@@ -124,11 +127,12 @@ class ScrollSpringSimulation extends SpringSimulation {
/// See the [new SpringSimulation] constructor on the superclass for a /// See the [new SpringSimulation] constructor on the superclass for a
/// discussion of the arguments' units. /// discussion of the arguments' units.
ScrollSpringSimulation( ScrollSpringSimulation(
SpringDescription desc, SpringDescription spring,
double start, double start,
double end, double end,
double velocity double velocity, {
) : super(desc, start, end, velocity); Tolerance tolerance: Tolerance.defaultTolerance,
}) : super(spring, start, end, velocity, tolerance: tolerance);
@override @override
double x(double time) => isDone(time) ? _endPosition : super.x(time); double x(double time) => isDone(time) ? _endPosition : super.x(time);
...@@ -139,22 +143,22 @@ class ScrollSpringSimulation extends SpringSimulation { ...@@ -139,22 +143,22 @@ class ScrollSpringSimulation extends SpringSimulation {
abstract class _SpringSolution { abstract class _SpringSolution {
factory _SpringSolution( factory _SpringSolution(
SpringDescription desc, SpringDescription spring,
double initialPosition, double initialPosition,
double initialVelocity double initialVelocity
) { ) {
assert(desc != null); assert(spring != null);
assert(desc.mass != null); assert(spring.mass != null);
assert(desc.springConstant != null); assert(spring.springConstant != null);
assert(desc.damping != null); assert(spring.damping != null);
assert(initialPosition != null); assert(initialPosition != null);
assert(initialVelocity != 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) if (cmk == 0.0)
return new _CriticalSolution(desc, initialPosition, initialVelocity); return new _CriticalSolution(spring, initialPosition, initialVelocity);
if (cmk > 0.0) if (cmk > 0.0)
return new _OverdampedSolution(desc, initialPosition, initialVelocity); return new _OverdampedSolution(spring, initialPosition, initialVelocity);
return new _UnderdampedSolution(desc, initialPosition, initialVelocity); return new _UnderdampedSolution(spring, initialPosition, initialVelocity);
} }
double x(double time); double x(double time);
...@@ -164,11 +168,11 @@ abstract class _SpringSolution { ...@@ -164,11 +168,11 @@ abstract class _SpringSolution {
class _CriticalSolution implements _SpringSolution { class _CriticalSolution implements _SpringSolution {
factory _CriticalSolution( factory _CriticalSolution(
SpringDescription desc, SpringDescription spring,
double distance, double distance,
double velocity 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 c1 = distance;
final double c2 = velocity / (r * distance); final double c2 = velocity / (r * distance);
return new _CriticalSolution.withArgs(r, c1, c2); return new _CriticalSolution.withArgs(r, c1, c2);
...@@ -198,13 +202,13 @@ class _CriticalSolution implements _SpringSolution { ...@@ -198,13 +202,13 @@ class _CriticalSolution implements _SpringSolution {
class _OverdampedSolution implements _SpringSolution { class _OverdampedSolution implements _SpringSolution {
factory _OverdampedSolution( factory _OverdampedSolution(
SpringDescription desc, SpringDescription spring,
double distance, double distance,
double velocity double velocity
) { ) {
final double cmk = desc.damping * desc.damping - 4 * desc.mass * desc.springConstant; final double cmk = spring.damping * spring.damping - 4 * spring.mass * spring.springConstant;
final double r1 = (-desc.damping - math.sqrt(cmk)) / (2.0 * desc.mass); final double r1 = (-spring.damping - math.sqrt(cmk)) / (2.0 * spring.mass);
final double r2 = (-desc.damping + math.sqrt(cmk)) / (2.0 * desc.mass); final double r2 = (-spring.damping + math.sqrt(cmk)) / (2.0 * spring.mass);
final double c2 = (velocity - r1 * distance) / (r2 - r1); final double c2 = (velocity - r1 * distance) / (r2 - r1);
final double c1 = distance - c2; final double c1 = distance - c2;
return new _OverdampedSolution.withArgs(r1, r2, c1, c2); return new _OverdampedSolution.withArgs(r1, r2, c1, c2);
...@@ -236,13 +240,13 @@ class _OverdampedSolution implements _SpringSolution { ...@@ -236,13 +240,13 @@ class _OverdampedSolution implements _SpringSolution {
class _UnderdampedSolution implements _SpringSolution { class _UnderdampedSolution implements _SpringSolution {
factory _UnderdampedSolution( factory _UnderdampedSolution(
SpringDescription desc, SpringDescription spring,
double distance, double distance,
double velocity double velocity
) { ) {
final double w = math.sqrt(4.0 * desc.mass * desc.springConstant - final double w = math.sqrt(4.0 * spring.mass * spring.springConstant -
desc.damping * desc.damping) / (2.0 * desc.mass); spring.damping * spring.damping) / (2.0 * spring.mass);
final double r = -(desc.damping / 2.0 * desc.mass); final double r = -(spring.damping / 2.0 * spring.mass);
final double c1 = distance; final double c1 = distance;
final double c2 = (velocity - r * distance) / w; final double c2 = (velocity - r * distance) / w;
return new _UnderdampedSolution.withArgs(w, r, c1, c2); return new _UnderdampedSolution.withArgs(w, r, c1, c2);
......
...@@ -62,8 +62,8 @@ class GlowingOverscrollIndicator extends StatefulWidget { ...@@ -62,8 +62,8 @@ class GlowingOverscrollIndicator extends StatefulWidget {
/// widget. /// widget.
/// ///
/// Typically a [GlowingOverscrollIndicator] is created by a /// Typically a [GlowingOverscrollIndicator] is created by a
/// [ScrollBehavior2.wrap] method, in which case the child is usually the one /// [ScrollBehavior2.buildViewportChrome] method, in which case
/// provided as an argument to that method. /// the child is usually the one provided as an argument to that method.
final Widget child; final Widget child;
@override @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 @@ ...@@ -2,7 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
////////////////////////////////////////////////////////////////////////////////
// DELETE THIS FILE WHEN REMOVING LEGACY SCROLLING CODE // DELETE THIS FILE WHEN REMOVING LEGACY SCROLLING CODE
////////////////////////////////////////////////////////////////////////////////
import 'dart:math' as math; import 'dart:math' as math;
......
...@@ -2,12 +2,86 @@ ...@@ -2,12 +2,86 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
// DELETE THIS FILE WHEN REMOVING LEGACY SCROLLING CODE
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'framework.dart'; import 'framework.dart';
import 'scroll_behavior.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. /// 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'; ...@@ -15,202 +15,238 @@ import 'package:flutter/scheduler.dart';
import 'basic.dart'; import 'basic.dart';
import 'framework.dart'; import 'framework.dart';
import 'gesture_detector.dart'; import 'gesture_detector.dart';
import 'overscroll_indicator.dart';
import 'scroll_simulation.dart';
import 'notification_listener.dart'; import 'notification_listener.dart';
import 'scroll_notification.dart'; import 'scroll_notification.dart';
import 'scrollable.dart'; import 'scrollable.dart';
import 'viewport.dart'; import 'ticker_provider.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;
/// The accuracy to which scrolling is computed by default. export 'package:flutter/physics.dart' show Tolerance;
///
/// 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
);
/// The platform whose scroll physics should be implemented. abstract class AbstractScrollState {
/// BuildContext get context;
/// Defaults to the current platform. TickerProvider get vsync;
TargetPlatform getPlatform(BuildContext context) => defaultTargetPlatform;
/// The color to use for the glow effect when [platform] indicates a platform void setIgnorePointer(bool value);
/// that uses a [GlowingOverscrollIndicator]. void setCanDrag(bool value);
/// void didEndDrag();
/// Defaults to white. void dispatchNotification(Notification notification);
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;
}
/// The scroll physics to use for the given platform. abstract class ScrollPhysics {
/// const ScrollPhysics(this.parent);
/// 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;
}
ScrollPhysics _getEffectiveScrollPhysics(BuildContext context, ScrollPhysics physics) { final ScrollPhysics parent;
final ScrollPhysics defaultPhysics = getScrollPhysics(getPlatform(context));
if (physics != null)
return physics.applyTo(defaultPhysics);
return defaultPhysics;
}
@override ScrollPhysics applyTo(ScrollPhysics parent);
ScrollPosition createScrollPosition(BuildContext context, Scrollable2State state, ScrollPosition oldPosition, ScrollPhysics physics) {
return new AbsoluteScrollPosition(state, scrollTolerances, oldPosition, _getEffectiveScrollPhysics(context, physics));
}
@override ScrollPosition createScrollPosition(ScrollPhysics physics, AbstractScrollState state, ScrollPosition oldPosition) {
bool shouldNotify(ViewportScrollBehavior oldDelegate) { if (parent == null)
return scrollTolerances != oldDelegate.scrollTolerances; return new ScrollPosition(physics, state, oldPosition);
return parent.createScrollPosition(physics, state, oldPosition);
} }
}
abstract class ScrollPhysics { /// Used by [DragScrollActivity] and other user-driven activities to
const ScrollPhysics();
ScrollPhysicsProxy applyTo(ScrollPhysics parent) => this;
/// Used by [AbsoluteDragScrollActivity] and other user-driven activities to
/// convert an offset in logical pixels as provided by the [DragUpdateDetails] /// convert an offset in logical pixels as provided by the [DragUpdateDetails]
/// into a delta to apply using [setPixels]. /// into a delta to apply using [setPixels].
/// ///
/// This is used by some [ScrollPosition] subclasses to apply friction during /// This is used by some [ScrollPosition] subclasses to apply friction during
/// overscroll situations. /// 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. /// Determines the overscroll by applying the boundary conditions.
/// ///
/// Called by [AbsoluteScrollPosition.setPixels] just before the [pixels] value is updated, to /// Called by [ScrollPosition.setPixels] just before the [pixels] value is
/// determine how much of the offset is to be clamped off and sent to /// updated, to determine how much of the offset is to be clamped off and sent
/// [AbsoluteScrollPosition.reportOverscroll]. /// to [ScrollPosition.reportOverscroll].
/// ///
/// The `value` argument is guaranteed to not equal [pixels] when this is /// The `value` argument is guaranteed to not equal [pixels] when this is
/// called. /// 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 /// Returns a simulation for ballisitic scrolling starting from the given
/// position with the given velocity. /// position with the given velocity.
/// ///
/// If the result is non-null, the [ScrollPosition] will begin an /// 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. /// [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, mass: 0.5,
springConstant: 100.0, springConstant: 100.0,
ratio: 1.1, ratio: 1.1,
); );
SpringDescription get scrollSpring => _kDefaultScrollSpring; SpringDescription get spring => parent?.spring ?? _kDefaultSpring;
}
abstract class ScrollPhysicsProxy extends ScrollPhysics {
const ScrollPhysicsProxy(this.parent);
final ScrollPhysics parent; /// The default accuracy to which scrolling is computed.
static final Tolerance _kDefaultTolerance = new Tolerance(
@override // TODO(ianh): Handle the case of the device pixel ratio changing.
ScrollPhysicsProxy applyTo(ScrollPhysics parent) { // TODO(ianh): Get this from the local MediaQuery not dart:ui's window object.
throw new FlutterError( velocity: 1.0 / (0.050 * ui.window.devicePixelRatio), // logical pixels per second
'$runtimeType must override applyTo.\n' distance: 1.0 / ui.window.devicePixelRatio // logical pixels
'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);
}
@override Tolerance get tolerance => parent?.tolerance ?? _kDefaultTolerance;
double applyBoundaryConditions(AbsoluteScrollPosition position, double value) {
if (parent == null)
return super.applyBoundaryConditions(position, value);
return parent.applyBoundaryConditions(position, value);
}
@override @mustCallSuper
Simulation createBallisticSimulation(AbsoluteScrollPosition position, double velocity) { bool shouldUpdateScrollPosition(@checked ScrollPhysics other) {
if (parent == null) if ((parent == null) != (other.parent == null))
return super.createBallisticSimulation(position, velocity); return true;
return parent.createBallisticSimulation(position, velocity); if (parent == null) {
assert(other.parent == null);
return false;
}
return parent.runtimeType != other.parent.runtimeType
|| parent.shouldUpdateScrollPosition(other.parent);
} }
@override @override
SpringDescription get scrollSpring { String toString() {
if (parent == null) if (parent == null)
return super.scrollSpring; return runtimeType.toString();
return parent.scrollSpring; return '$runtimeType -> $parent';
} }
} }
class AbsoluteScrollPosition extends ScrollPosition { class ScrollPosition extends ViewportOffset {
AbsoluteScrollPosition( ScrollPosition(this.physics, this.state, ScrollPosition oldPosition) {
Scrollable2State state, assert(physics != null);
Tolerance scrollTolerances, assert(state != null);
ScrollPosition oldPosition, assert(state.vsync != null);
this.physics, if (oldPosition != null)
) : super(state, scrollTolerances, oldPosition); absorb(oldPosition);
if (activity == null)
beginIdleActivity();
assert(activity != null);
assert(activity.position == this);
}
final ScrollPhysics physics; final ScrollPhysics physics;
final AbstractScrollState state;
@override @override
double get pixels => _pixels; double get pixels => _pixels;
double _pixels = 0.0; 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) { double setPixels(double value) {
assert(SchedulerBinding.instance.schedulerPhase.index <= SchedulerPhase.transientCallbacks.index); assert(SchedulerBinding.instance.schedulerPhase.index <= SchedulerPhase.transientCallbacks.index);
assert(activity.isScrolling); assert(activity.isScrolling);
...@@ -234,7 +270,7 @@ class AbsoluteScrollPosition extends ScrollPosition { ...@@ -234,7 +270,7 @@ class AbsoluteScrollPosition extends ScrollPosition {
_pixels = value - overScroll; _pixels = value - overScroll;
if (_pixels != oldPixels) { if (_pixels != oldPixels) {
notifyListeners(); notifyListeners();
dispatchNotification(activity.createScrollUpdateNotification(state, _pixels - oldPixels)); state.dispatchNotification(activity.createScrollUpdateNotification(state, _pixels - oldPixels));
} }
if (overScroll != 0.0) { if (overScroll != 0.0) {
reportOverscroll(overScroll); reportOverscroll(overScroll);
...@@ -244,10 +280,15 @@ class AbsoluteScrollPosition extends ScrollPosition { ...@@ -244,10 +280,15 @@ class AbsoluteScrollPosition extends ScrollPosition {
return 0.0; return 0.0;
} }
@override
void correctBy(double correction) {
_pixels += correction;
}
@protected @protected
void reportOverscroll(double value) { void reportOverscroll(double value) {
assert(activity.isScrolling); assert(activity.isScrolling);
dispatchNotification(activity.createOverscrollNotification(state, value)); state.dispatchNotification(activity.createOverscrollNotification(state, value));
} }
double get viewportDimension => _viewportDimension; double get viewportDimension => _viewportDimension;
...@@ -274,7 +315,7 @@ class AbsoluteScrollPosition extends ScrollPosition { ...@@ -274,7 +315,7 @@ class AbsoluteScrollPosition extends ScrollPosition {
// soon afterwards in the same layout phase. So we put all the logic that // soon afterwards in the same layout phase. So we put all the logic that
// relies on both values being computed into applyContentDimensions. // relies on both values being computed into applyContentDimensions.
} }
super.applyViewportDimension(viewportDimension); state.setCanDrag(canDrag);
} }
@override @override
...@@ -287,259 +328,229 @@ class AbsoluteScrollPosition extends ScrollPosition { ...@@ -287,259 +328,229 @@ class AbsoluteScrollPosition extends ScrollPosition {
activity.applyNewDimensions(); activity.applyNewDimensions();
_didChangeViewportDimension = false; _didChangeViewportDimension = false;
} }
return super.applyContentDimensions(minScrollExtent, maxScrollExtent); state.setCanDrag(canDrag);
return true;
} }
@override /// Take any current applicable state from the given [ScrollPosition].
ScrollableMetrics getMetrics() { ///
return new ScrollableMetrics( /// This method is called by the constructor, instead of calling
extentBefore: math.max(pixels - minScrollExtent, 0.0), /// [beginIdleActivity], if it is given an `oldPosition`. It adopts the old
extentInside: math.min(pixels, maxScrollExtent) - math.max(pixels, minScrollExtent) + math.min(viewportDimension, maxScrollExtent - minScrollExtent), /// position's current [activity] as its own.
extentAfter: math.max(maxScrollExtent - pixels, 0.0), ///
); /// 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; bool get canDrag => true;
@override
bool get shouldIgnorePointer => activity?.shouldIgnorePointer; bool get shouldIgnorePointer => activity?.shouldIgnorePointer;
void touched() {
_activity.touched();
}
/// The direction that the user most recently began scrolling in.
@override @override
void correctBy(double correction) { ScrollDirection get userScrollDirection => _userScrollDirection;
_pixels += correction; 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 @override
void absorb(ScrollPosition other) { void dispose() {
if (other is AbsoluteScrollPosition) { activity?.dispose(); // it will be null if it got absorbed by another ScrollPosition
final AbsoluteScrollPosition typedOther = other; _activity = null;
_pixels = typedOther._pixels; super.dispose();
_viewportDimension = typedOther.viewportDimension; }
_minScrollExtent = typedOther.minScrollExtent;
_maxScrollExtent = typedOther.maxScrollExtent; // 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) { DragScrollActivity beginDragActivity(DragStartDetails details) {
beginActivity(new AbsoluteDragScrollActivity(this, details, scrollTolerances)); beginActivity(new DragScrollActivity(this, details));
return activity; return activity;
} }
@override // ///
// /// The velocity should be in logical pixels per second.
void beginBallisticActivity(double velocity) { void beginBallisticActivity(double velocity) {
final Simulation simulation = physics.createBallisticSimulation(this, velocity); final Simulation simulation = physics.createBallisticSimulation(this, velocity);
if (simulation != null) { if (simulation != null) {
simulation.tolerance = scrollTolerances; beginActivity(new BallisticScrollActivity(this, simulation, state.vsync));
beginActivity(new AbsoluteBallisticScrollActivity(this, simulation, vsync));
} else { } else {
beginIdleActivity(); 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 @override
void debugFillDescription(List<String> description) { void debugFillDescription(List<String> description) {
super.debugFillDescription(description); super.debugFillDescription(description);
description.add('$activity');
description.add('$userScrollDirection');
description.add('range: ${minScrollExtent?.toStringAsFixed(1)}..${maxScrollExtent?.toStringAsFixed(1)}'); description.add('range: ${minScrollExtent?.toStringAsFixed(1)}..${maxScrollExtent?.toStringAsFixed(1)}');
description.add('viewport: ${viewportDimension?.toStringAsFixed(1)}'); description.add('viewport: ${viewportDimension?.toStringAsFixed(1)}');
} }
} }
/// Scroll physics for environments that allow the scroll offset to go beyond /// Base class for scrolling activities like dragging, and flinging.
/// the bounds of the content, but then bounce the content back to the edge of abstract class ScrollActivity {
/// those bounds. ScrollActivity(this._position);
///
/// This is the behavior typically seen on iOS. @protected
/// ScrollPosition get position => _position;
/// See also: ScrollPosition _position;
///
/// * [ViewportScrollBehavior], which uses this to provide the iOS component of /// Called by the [ScrollPosition] when it has changed type (for example, when
/// its scroll behavior. /// changing from an Android-style scroll position to an iOS-style scroll
/// * [ClampingScrollPhysics], which is the analogous physics for Android's /// position). If this activity can differ between the two modes, then it
/// clamping behavior. /// should tell the position to restart that activity appropriately.
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.
/// ///
/// By default this is 0.5, meaning that overscroll is twice as hard as normal /// For example, [BallisticScrollActivity]'s implementation calls
/// scroll. /// [ScrollPosition.beginBallisticActivity].
double get frictionFactor => 0.5; void resetActivity() { }
@override Notification createScrollStartNotification(Scrollable2State scrollable) {
double applyPhysicsToUserOffset(AbsoluteScrollPosition position, double offset) { return new ScrollStartNotification(scrollable: scrollable);
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 Notification createScrollUpdateNotification(Scrollable2State scrollable, double scrollDelta) {
Simulation createBallisticSimulation(AbsoluteScrollPosition position, double velocity) { return new ScrollUpdateNotification(scrollable: scrollable, scrollDelta: scrollDelta);
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;
} }
}
/// Scroll physics for environments that prevent the scroll offset from reaching Notification createOverscrollNotification(Scrollable2State scrollable, double overscroll) {
/// beyond the bounds of the content. return new OverscrollNotification(scrollable: scrollable, overscroll: overscroll);
/// }
/// This is the behavior typically seen on Android.
/// Notification createScrollEndNotification(Scrollable2State scrollable) {
/// See also: return new ScrollEndNotification(scrollable: scrollable);
/// }
/// * [ViewportScrollBehavior], which uses this to provide the Android component
/// of its scroll behavior. void touched() { }
/// * [BouncingScrollPhysics], which is the analogous physics for iOS' bouncing
/// behavior. void applyNewDimensions() { }
/// * [GlowingOverscrollIndicator], which is used by [ViewportScrollBehavior] to
/// provide the glowing effect that is usually found with this clamping effect bool get shouldIgnorePointer;
/// on Android.
class ClampingScrollPhysics extends ScrollPhysics { bool get isScrolling;
const ClampingScrollPhysics();
@mustCallSuper
@override void dispose() {
double applyBoundaryConditions(AbsoluteScrollPosition position, double value) { _position = null;
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 @override
Simulation createBallisticSimulation(AbsoluteScrollPosition position, double velocity) { String toString() => '$runtimeType';
if (position.outOfRange) { }
if (position.pixels > position.maxScrollExtent)
return new ScrollSpringSimulation(scrollSpring, position.pixels, position.maxScrollExtent, math.min(0.0, velocity)); class IdleScrollActivity extends ScrollActivity {
if (position.pixels < position.minScrollExtent) IdleScrollActivity(ScrollPosition position) : super(position);
return new ScrollSpringSimulation(scrollSpring, position.pixels, position.minScrollExtent, math.max(0.0, velocity));
assert(false); @override
} void applyNewDimensions() {
if (!position.atEdge && velocity.abs() >= position.scrollTolerances.velocity) { position.beginBallisticActivity(0.0);
return new ClampingScrollSimulation(
position: position.pixels,
velocity: velocity,
);
}
return null;
} }
@override
bool get shouldIgnorePointer => false;
@override
bool get isScrolling => false;
} }
class AbsoluteDragScrollActivity extends DragScrollActivity { class DragScrollActivity extends ScrollActivity {
AbsoluteDragScrollActivity( DragScrollActivity(
AbsoluteScrollPosition position, ScrollPosition position,
DragStartDetails details, DragStartDetails details,
this.scrollTolerances,
) : _lastDetails = details, super(position); ) : _lastDetails = details, super(position);
final Tolerance scrollTolerances;
@override @override
AbsoluteScrollPosition get position => super.position; void touched() {
assert(false);
}
@override
void update(DragUpdateDetails details, { bool reverse }) { void update(DragUpdateDetails details, { bool reverse }) {
assert(details.primaryDelta != null); assert(details.primaryDelta != null);
_lastDetails = details; _lastDetails = details;
...@@ -554,7 +565,6 @@ class AbsoluteDragScrollActivity extends DragScrollActivity { ...@@ -554,7 +565,6 @@ class AbsoluteDragScrollActivity extends DragScrollActivity {
// because it gets reported via the reportOverscroll path. // because it gets reported via the reportOverscroll path.
} }
@override
void end(DragEndDetails details, { bool reverse }) { void end(DragEndDetails details, { bool reverse }) {
assert(details.primaryVelocity != null); assert(details.primaryVelocity != null);
double velocity = details.primaryVelocity; double velocity = details.primaryVelocity;
...@@ -570,6 +580,7 @@ class AbsoluteDragScrollActivity extends DragScrollActivity { ...@@ -570,6 +580,7 @@ class AbsoluteDragScrollActivity extends DragScrollActivity {
@override @override
void dispose() { void dispose() {
_lastDetails = null; _lastDetails = null;
position.state.didEndDrag();
super.dispose(); super.dispose();
} }
...@@ -606,11 +617,11 @@ class AbsoluteDragScrollActivity extends DragScrollActivity { ...@@ -606,11 +617,11 @@ class AbsoluteDragScrollActivity extends DragScrollActivity {
bool get isScrolling => true; bool get isScrolling => true;
} }
class AbsoluteBallisticScrollActivity extends ScrollActivity { class BallisticScrollActivity extends ScrollActivity {
/// ///
/// The velocity should be in logical pixels per second. /// The velocity should be in logical pixels per second.
AbsoluteBallisticScrollActivity( BallisticScrollActivity(
AbsoluteScrollPosition position, ScrollPosition position,
Simulation simulation, Simulation simulation,
TickerProvider vsync, TickerProvider vsync,
) : super(position) { ) : super(position) {
...@@ -625,7 +636,7 @@ class AbsoluteBallisticScrollActivity extends ScrollActivity { ...@@ -625,7 +636,7 @@ class AbsoluteBallisticScrollActivity extends ScrollActivity {
} }
@override @override
AbsoluteScrollPosition get position => super.position; ScrollPosition get position => super.position;
double get velocity => _controller.velocity; double get velocity => _controller.velocity;
...@@ -678,8 +689,8 @@ class AbsoluteBallisticScrollActivity extends ScrollActivity { ...@@ -678,8 +689,8 @@ class AbsoluteBallisticScrollActivity extends ScrollActivity {
} }
} }
class AbsoluteDrivenScrollActivity extends ScrollActivity { class DrivenScrollActivity extends ScrollActivity {
AbsoluteDrivenScrollActivity( DrivenScrollActivity(
ScrollPosition position, { ScrollPosition position, {
@required double from, @required double from,
@required double to, @required double to,
...@@ -704,7 +715,7 @@ class AbsoluteDrivenScrollActivity extends ScrollActivity { ...@@ -704,7 +715,7 @@ class AbsoluteDrivenScrollActivity extends ScrollActivity {
} }
@override @override
AbsoluteScrollPosition get position => super.position; ScrollPosition get position => super.position;
Completer<Null> _completer; Completer<Null> _completer;
AnimationController _controller; AnimationController _controller;
......
...@@ -116,7 +116,8 @@ class ClampingScrollSimulation extends Simulation { ...@@ -116,7 +116,8 @@ class ClampingScrollSimulation extends Simulation {
@required this.position, @required this.position,
@required this.velocity, @required this.velocity,
this.friction: 0.015, this.friction: 0.015,
}) { Tolerance tolerance: Tolerance.defaultTolerance,
}) : super(tolerance: tolerance) {
_scaledFriction = friction * _decelerationForFriction(0.84); // See mPhysicalCoeff _scaledFriction = friction * _decelerationForFriction(0.84); // See mPhysicalCoeff
_duration = _flingDuration(velocity); _duration = _flingDuration(velocity);
_distance = _flingDistance(velocity); _distance = _flingDistance(velocity);
...@@ -196,7 +197,9 @@ class ClampingScrollSimulation extends Simulation { ...@@ -196,7 +197,9 @@ class ClampingScrollSimulation extends Simulation {
} }
} }
////////////////////////////////////////////////////////////////////////////////
// DELETE EVERYTHING BELOW THIS LINE WHEN REMOVING LEGACY SCROLLING CODE // 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 SpringDescription _kScrollSpring = new SpringDescription.withDampingRatio(mass: 0.5, springConstant: 100.0, ratio: 1.1);
final double _kDrag = 0.025; final double _kDrag = 0.025;
......
...@@ -7,8 +7,8 @@ import 'package:meta/meta.dart'; ...@@ -7,8 +7,8 @@ import 'package:meta/meta.dart';
import 'framework.dart'; import 'framework.dart';
import 'basic.dart'; import 'basic.dart';
import 'page_scroll_physics.dart'; import 'scroll_physics.dart';
import 'scroll_absolute.dart'; import 'scroll_position.dart';
import 'scrollable.dart'; import 'scrollable.dart';
import 'sliver.dart'; import 'sliver.dart';
import 'viewport.dart'; import 'viewport.dart';
......
...@@ -23,383 +23,12 @@ import 'page_storage.dart'; ...@@ -23,383 +23,12 @@ import 'page_storage.dart';
import 'scroll_behavior.dart'; import 'scroll_behavior.dart';
import 'scroll_configuration.dart'; import 'scroll_configuration.dart';
import 'scroll_notification.dart'; import 'scroll_notification.dart';
import 'scroll_position.dart';
import 'ticker_provider.dart'; import 'ticker_provider.dart';
import 'viewport.dart'; import 'viewport.dart';
// TODO(abarth): Merge AbsoluteScrollPosition and ScrollPosition.
import 'scroll_absolute.dart' show ViewportScrollBehavior, ScrollPhysics;
export 'package:flutter/physics.dart' show Tolerance; 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); typedef Widget ViewportBuilder(BuildContext context, ViewportOffset position);
class Scrollable2 extends StatefulWidget { class Scrollable2 extends StatefulWidget {
...@@ -407,7 +36,6 @@ class Scrollable2 extends StatefulWidget { ...@@ -407,7 +36,6 @@ class Scrollable2 extends StatefulWidget {
Key key, Key key,
this.axisDirection: AxisDirection.down, this.axisDirection: AxisDirection.down,
this.physics, this.physics,
this.scrollBehavior,
@required this.viewportBuilder, @required this.viewportBuilder,
}) : super (key: key) { }) : super (key: key) {
assert(axisDirection != null); assert(axisDirection != null);
...@@ -418,14 +46,6 @@ class Scrollable2 extends StatefulWidget { ...@@ -418,14 +46,6 @@ class Scrollable2 extends StatefulWidget {
final ScrollPhysics physics; 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; final ViewportBuilder viewportBuilder;
Axis get axis => axisDirectionToAxis(axisDirection); Axis get axis => axisDirectionToAxis(axisDirection);
...@@ -433,29 +53,12 @@ class Scrollable2 extends StatefulWidget { ...@@ -433,29 +53,12 @@ class Scrollable2 extends StatefulWidget {
@override @override
Scrollable2State createState() => new Scrollable2State(); 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 @override
void debugFillDescription(List<String> description) { void debugFillDescription(List<String> description) {
super.debugFillDescription(description); super.debugFillDescription(description);
description.add('$axisDirection'); description.add('$axisDirection');
if (physics != null) if (physics != null)
description.add('physics: $physics'); 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 { ...@@ -468,24 +71,28 @@ class Scrollable2 extends StatefulWidget {
/// [NotificationListener] to listen for [ScrollNotification2] notifications. /// [NotificationListener] to listen for [ScrollNotification2] notifications.
/// ///
/// This class is not intended to be subclassed. To specialize the behavior of a /// This class is not intended to be subclassed. To specialize the behavior of a
/// [Scrollable2], provide it with a custom [ScrollBehavior2] delegate. /// [Scrollable2], provide it with a [ScrollPhysics].
class Scrollable2State extends State<Scrollable2> with TickerProviderStateMixin { class Scrollable2State extends State<Scrollable2> with TickerProviderStateMixin
implements AbstractScrollState {
/// The controller for this [Scrollable2] widget's viewport position. /// The controller for this [Scrollable2] widget's viewport position.
/// ///
/// To control what kind of [ScrollPosition] is created for a [Scrollable2], /// To control what kind of [ScrollPosition] is created for a [Scrollable2],
/// provide it with a custom [ScrollBehavior2] delegate that creates the /// provide it with custom [ScrollPhysics] that creates the appropriate
/// appropriate [ScrollPosition] controller in its /// [ScrollPosition] controller in its [ScrollPhysics.createScrollPosition]
/// [ScrollBehavior2.createScrollPosition] method. /// method.
ScrollPosition get position => _position; ScrollPosition get position => _position;
ScrollPosition _position; ScrollPosition _position;
ScrollBehavior2 _scrollBehavior; ScrollBehavior2 _configuration;
// only call this from places that will definitely trigger a rebuild // only call this from places that will definitely trigger a rebuild
void _updatePosition() { 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; final ScrollPosition oldPosition = position;
_position = _scrollBehavior.createScrollPosition(context, this, oldPosition, config.physics); _position = physics.createScrollPosition(physics, this, oldPosition);
assert(position != null); assert(position != null);
if (oldPosition != null) { if (oldPosition != null) {
// It's important that we not do this until after the viewport has had a // 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 ...@@ -501,10 +108,19 @@ class Scrollable2State extends State<Scrollable2> with TickerProviderStateMixin
_updatePosition(); _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 @override
void didUpdateConfig(Scrollable2 oldConfig) { void didUpdateConfig(Scrollable2 oldConfig) {
super.didUpdateConfig(oldConfig); super.didUpdateConfig(oldConfig);
if (config.shouldUpdateScrollPosition(oldConfig)) if (_shouldUpdatePosition(oldConfig))
_updatePosition(); _updatePosition();
} }
...@@ -527,7 +143,9 @@ class Scrollable2State extends State<Scrollable2> with TickerProviderStateMixin ...@@ -527,7 +143,9 @@ class Scrollable2State extends State<Scrollable2> with TickerProviderStateMixin
bool _lastCanDrag; bool _lastCanDrag;
Axis _lastAxisDirection; Axis _lastAxisDirection;
void _updateGestureDetectors(bool canDrag) { @override
@protected
void setCanDrag(bool canDrag) {
if (canDrag == _lastCanDrag && (!canDrag || config.axis == _lastAxisDirection)) if (canDrag == _lastCanDrag && (!canDrag || config.axis == _lastAxisDirection))
return; return;
if (!canDrag) { if (!canDrag) {
...@@ -564,7 +182,12 @@ class Scrollable2State extends State<Scrollable2> with TickerProviderStateMixin ...@@ -564,7 +182,12 @@ class Scrollable2State extends State<Scrollable2> with TickerProviderStateMixin
_gestureDetectorKey.currentState.replaceGestureRecognizers(_gestureRecognizers); _gestureDetectorKey.currentState.replaceGestureRecognizers(_gestureRecognizers);
} }
void _updateIgnorePointer(bool value) { @override
TickerProvider get vsync => this;
@override
@protected
void setIgnorePointer(bool value) {
if (_shouldIgnorePointer == value) if (_shouldIgnorePointer == value)
return; return;
_shouldIgnorePointer = value; _shouldIgnorePointer = value;
...@@ -574,6 +197,18 @@ class Scrollable2State extends State<Scrollable2> with TickerProviderStateMixin ...@@ -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 // TOUCH HANDLERS
...@@ -630,7 +265,7 @@ class Scrollable2State extends State<Scrollable2> with TickerProviderStateMixin ...@@ -630,7 +265,7 @@ class Scrollable2State extends State<Scrollable2> with TickerProviderStateMixin
child: config.viewportBuilder(context, position), child: config.viewportBuilder(context, position),
), ),
); );
return _scrollBehavior.wrap(context, result, config.axisDirection); return _configuration.buildViewportChrome(context, result, config.axisDirection);
} }
@override @override
...@@ -640,8 +275,9 @@ class Scrollable2State extends State<Scrollable2> with TickerProviderStateMixin ...@@ -640,8 +275,9 @@ class Scrollable2State extends State<Scrollable2> with TickerProviderStateMixin
} }
} }
////////////////////////////////////////////////////////////////////////////////
// DELETE EVERYTHING BELOW THIS LINE WHEN REMOVING LEGACY SCROLLING CODE // DELETE EVERYTHING BELOW THIS LINE WHEN REMOVING LEGACY SCROLLING CODE
////////////////////////////////////////////////////////////////////////////////
/// Identifies one or both limits of a [Scrollable] in terms of its scrollDirection. /// Identifies one or both limits of a [Scrollable] in terms of its scrollDirection.
enum ScrollableEdge { enum ScrollableEdge {
......
...@@ -46,10 +46,11 @@ export 'src/widgets/performance_overlay.dart'; ...@@ -46,10 +46,11 @@ export 'src/widgets/performance_overlay.dart';
export 'src/widgets/placeholder.dart'; export 'src/widgets/placeholder.dart';
export 'src/widgets/raw_keyboard_listener.dart'; export 'src/widgets/raw_keyboard_listener.dart';
export 'src/widgets/routes.dart'; export 'src/widgets/routes.dart';
export 'src/widgets/scroll_absolute.dart';
export 'src/widgets/scroll_behavior.dart'; export 'src/widgets/scroll_behavior.dart';
export 'src/widgets/scroll_configuration.dart'; export 'src/widgets/scroll_configuration.dart';
export 'src/widgets/scroll_notification.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_simulation.dart';
export 'src/widgets/scroll_view.dart'; export 'src/widgets/scroll_view.dart';
export 'src/widgets/scrollable.dart'; export 'src/widgets/scrollable.dart';
......
...@@ -72,7 +72,7 @@ void main() { ...@@ -72,7 +72,7 @@ void main() {
log.clear(); log.clear();
Scrollable2State state = tester.state(find.byType(Scrollable2)); Scrollable2State state = tester.state(find.byType(Scrollable2));
AbsoluteScrollPosition position = state.position; ScrollPosition position = state.position;
position.jumpTo(2025.0); position.jumpTo(2025.0);
expect(log, isEmpty); expect(log, isEmpty);
......
...@@ -204,12 +204,14 @@ void main() { ...@@ -204,12 +204,14 @@ void main() {
RenderObject painter; RenderObject painter;
await tester.pumpWidget( await tester.pumpWidget(
new TestScrollable( new ScrollConfiguration2(
axisDirection: AxisDirection.left, behavior: new TestScrollBehavior1(),
scrollBehavior: new TestScrollBehavior1(), child: new TestScrollable(
slivers: <Widget>[ axisDirection: AxisDirection.left,
new SliverToBoxAdapter(child: new SizedBox(height: 20.0)), slivers: <Widget>[
], new SliverToBoxAdapter(child: new SizedBox(height: 20.0)),
],
),
), ),
); );
painter = tester.renderObject(find.byType(CustomPaint)); painter = tester.renderObject(find.byType(CustomPaint));
...@@ -219,12 +221,14 @@ void main() { ...@@ -219,12 +221,14 @@ void main() {
await tester.pumpUntilNoTransientCallbacks(const Duration(seconds: 1)); await tester.pumpUntilNoTransientCallbacks(const Duration(seconds: 1));
await tester.pumpWidget( await tester.pumpWidget(
new TestScrollable( new ScrollConfiguration2(
axisDirection: AxisDirection.right, behavior: new TestScrollBehavior2(),
scrollBehavior: new TestScrollBehavior2(), child: new TestScrollable(
slivers: <Widget>[ axisDirection: AxisDirection.right,
new SliverToBoxAdapter(child: new SizedBox(height: 20.0)), slivers: <Widget>[
], new SliverToBoxAdapter(child: new SizedBox(height: 20.0)),
],
),
), ),
); );
painter = tester.renderObject(find.byType(CustomPaint)); painter = tester.renderObject(find.byType(CustomPaint));
...@@ -234,14 +238,14 @@ void main() { ...@@ -234,14 +238,14 @@ void main() {
}); });
} }
class TestScrollBehavior1 extends ViewportScrollBehavior { class TestScrollBehavior1 extends ScrollBehavior2 {
@override @override
Color getGlowColor(BuildContext context) { Color getGlowColor(BuildContext context) {
return const Color(0xFF00FF00); return const Color(0xFF00FF00);
} }
} }
class TestScrollBehavior2 extends ViewportScrollBehavior { class TestScrollBehavior2 extends ScrollBehavior2 {
@override @override
Color getGlowColor(BuildContext context) { Color getGlowColor(BuildContext context) {
return const Color(0xFF0000FF); return const Color(0xFF0000FF);
......
...@@ -158,7 +158,7 @@ void main() { ...@@ -158,7 +158,7 @@ void main() {
Scrollable2State state = tester.state(find.byType(Scrollable2)); Scrollable2State state = tester.state(find.byType(Scrollable2));
AbsoluteScrollPosition position = state.position; ScrollPosition position = state.position;
position.jumpTo(3025.0); position.jumpTo(3025.0);
expect(log, isEmpty); expect(log, isEmpty);
......
...@@ -10,13 +10,13 @@ import 'test_widgets.dart'; ...@@ -10,13 +10,13 @@ import 'test_widgets.dart';
class TestScrollPosition extends ScrollPosition { class TestScrollPosition extends ScrollPosition {
TestScrollPosition( TestScrollPosition(
this.extentMultiplier, TestScrollPhysics physics,
Scrollable2State state, AbstractScrollState state,
Tolerance scrollTolerances,
ScrollPosition oldPosition, 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; double _min, _viewport, _max, _pixels;
...@@ -27,7 +27,7 @@ class TestScrollPosition extends ScrollPosition { ...@@ -27,7 +27,7 @@ class TestScrollPosition extends ScrollPosition {
double setPixels(double value) { double setPixels(double value) {
double oldPixels = _pixels; double oldPixels = _pixels;
_pixels = value; _pixels = value;
dispatchNotification(activity.createScrollUpdateNotification(state, _pixels - oldPixels)); state.dispatchNotification(activity.createScrollUpdateNotification(state, _pixels - oldPixels));
return 0.0; return 0.0;
} }
...@@ -56,9 +56,9 @@ class TestScrollPosition extends ScrollPosition { ...@@ -56,9 +56,9 @@ class TestScrollPosition extends ScrollPosition {
double afterExtent = _max - _pixels; double afterExtent = _max - _pixels;
if (insideExtent > 0.0) { if (insideExtent > 0.0) {
return new ScrollableMetrics( return new ScrollableMetrics(
extentBefore: extentMultiplier * beforeExtent / insideExtent, extentBefore: physics.extentMultiplier * beforeExtent / insideExtent,
extentInside: extentMultiplier, extentInside: physics.extentMultiplier,
extentAfter: extentMultiplier * afterExtent / insideExtent, extentAfter: physics.extentMultiplier * afterExtent / insideExtent,
); );
} else { } else {
return new ScrollableMetrics( return new ScrollableMetrics(
...@@ -70,17 +70,36 @@ class TestScrollPosition extends ScrollPosition { ...@@ -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 { class TestScrollBehavior extends ScrollBehavior2 {
TestScrollBehavior(this.extentMultiplier); TestScrollBehavior(this.extentMultiplier);
final double extentMultiplier; final double extentMultiplier;
@override @override
Widget wrap(BuildContext context, Widget child, AxisDirection axisDirection) => child; ScrollPhysics getScrollPhysics(BuildContext context) {
return new TestScrollPhysics(
extentMultiplier: extentMultiplier
).applyTo(super.getScrollPhysics(context));
}
@override @override
ScrollPosition createScrollPosition(BuildContext context, Scrollable2State state, ScrollPosition oldPosition, ScrollPhysics physics) { Widget buildViewportChrome(BuildContext context, Widget child, AxisDirection axisDirection) => child;
return new TestScrollPosition(extentMultiplier, state, ViewportScrollBehavior.defaultScrollTolerances, oldPosition);
}
@override @override
bool shouldNotify(TestScrollBehavior oldDelegate) { bool shouldNotify(TestScrollBehavior oldDelegate) {
...@@ -90,20 +109,24 @@ class TestScrollBehavior extends ScrollBehavior2 { ...@@ -90,20 +109,24 @@ class TestScrollBehavior extends ScrollBehavior2 {
void main() { void main() {
testWidgets('Changing the scroll behavior dynamically', (WidgetTester tester) async { testWidgets('Changing the scroll behavior dynamically', (WidgetTester tester) async {
await tester.pumpWidget(new TestScrollable( await tester.pumpWidget(new ScrollConfiguration2(
scrollBehavior: new TestScrollBehavior(1.0), behavior: new TestScrollBehavior(1.0),
slivers: <Widget>[ child: new TestScrollable(
new SliverToBoxAdapter(child: new SizedBox(height: 2000.0)), slivers: <Widget>[
], new SliverToBoxAdapter(child: new SizedBox(height: 2000.0)),
],
),
)); ));
Scrollable2State state = tester.state(find.byType(Scrollable2)); Scrollable2State state = tester.state(find.byType(Scrollable2));
expect(state.position.getMetrics().extentInside, 1.0); expect(state.position.getMetrics().extentInside, 1.0);
await tester.pumpWidget(new TestScrollable( await tester.pumpWidget(new ScrollConfiguration2(
scrollBehavior: new TestScrollBehavior(2.0), behavior: new TestScrollBehavior(2.0),
slivers: <Widget>[ child: new TestScrollable(
new SliverToBoxAdapter(child: new SizedBox(height: 2000.0)), slivers: <Widget>[
], new SliverToBoxAdapter(child: new SizedBox(height: 2000.0)),
],
),
)); ));
expect(state.position.getMetrics().extentInside, 2.0); expect(state.position.getMetrics().extentInside, 2.0);
}); });
......
...@@ -32,7 +32,7 @@ double getScrollOffset(WidgetTester tester) { ...@@ -32,7 +32,7 @@ double getScrollOffset(WidgetTester tester) {
void resetScrollOffset(WidgetTester tester) { void resetScrollOffset(WidgetTester tester) {
RenderViewport2 viewport = tester.renderObject(find.byType(Viewport2)); RenderViewport2 viewport = tester.renderObject(find.byType(Viewport2));
AbsoluteScrollPosition position = viewport.offset; ScrollPosition position = viewport.offset;
position.jumpTo(0.0); position.jumpTo(0.0);
} }
......
...@@ -37,7 +37,7 @@ void main() { ...@@ -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 final double max = bigHeight * 2.0 + new TestDelegate().maxExtent - 600.0; // 600 is the height of the test viewport
assert(max < 10000.0); assert(max < 10000.0);
expect(max, 1600.0); expect(max, 1600.0);
...@@ -65,7 +65,7 @@ void main() { ...@@ -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(key1, new Offset(0.0, 0.0), true);
verifyPaintPosition(key2, new Offset(0.0, 600.0), false); verifyPaintPosition(key2, new Offset(0.0, 600.0), false);
...@@ -135,7 +135,7 @@ void main() { ...@@ -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(key1, new Offset(0.0, 0.0), true);
verifyPaintPosition(key2, new Offset(0.0, 600.0), false); verifyPaintPosition(key2, new Offset(0.0, 600.0), false);
...@@ -168,7 +168,7 @@ void main() { ...@@ -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(key1, new Offset(0.0, 0.0), true);
verifyPaintPosition(key2, new Offset(0.0, 600.0), false); verifyPaintPosition(key2, new Offset(0.0, 600.0), false);
......
...@@ -40,7 +40,7 @@ void main() { ...@@ -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 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); assert(max < 10000.0);
expect(max, 1450.0); expect(max, 1450.0);
...@@ -74,7 +74,7 @@ void main() { ...@@ -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(key1, new Offset(0.0, 0.0), true);
verifyPaintPosition(key2, new Offset(0.0, 550.0), true); verifyPaintPosition(key2, new Offset(0.0, 550.0), true);
verifyPaintPosition(key3, new Offset(0.0, 600.0), false); verifyPaintPosition(key3, new Offset(0.0, 600.0), false);
...@@ -164,7 +164,7 @@ void main() { ...@@ -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 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); assert(max < 10000.0);
expect(max, 1750.0); expect(max, 1750.0);
......
...@@ -31,7 +31,7 @@ void main() { ...@@ -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 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); assert(max < 10000.0);
expect(max, 1450.0); expect(max, 1450.0);
...@@ -64,7 +64,7 @@ void main() { ...@@ -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)); position.animate(to: RenderBigSliver.height + delegate.maxExtent - 5.0, curve: Curves.linear, duration: const Duration(minutes: 1));
await tester.pumpUntilNoTransientCallbacks(const Duration(milliseconds: 1000)); await tester.pumpUntilNoTransientCallbacks(const Duration(milliseconds: 1000));
RenderBox box = tester.renderObject<RenderBox>(find.byType(Container)); RenderBox box = tester.renderObject<RenderBox>(find.byType(Container));
......
...@@ -33,35 +33,31 @@ class TestSliverAppBarDelegate extends SliverAppBarDelegate { ...@@ -33,35 +33,31 @@ class TestSliverAppBarDelegate extends SliverAppBarDelegate {
class TestBehavior extends ScrollBehavior2 { class TestBehavior extends ScrollBehavior2 {
@override @override
Widget wrap(BuildContext context, Widget child, AxisDirection axisDirection) { Widget buildViewportChrome(BuildContext context, Widget child, AxisDirection axisDirection) {
return new GlowingOverscrollIndicator( return new GlowingOverscrollIndicator(
child: child, child: child,
axisDirection: axisDirection, axisDirection: axisDirection,
color: const Color(0xFFFFFFFF), color: const Color(0xFFFFFFFF),
); );
} }
}
class TestScrollPhysics extends ClampingScrollPhysics {
const TestScrollPhysics({ ScrollPhysics parent }) : super(parent: parent);
@override @override
ScrollPosition createScrollPosition(BuildContext context, Scrollable2State state, ScrollPosition oldPosition, ScrollPhysics physics) { TestScrollPhysics applyTo(ScrollPhysics parent) => new TestScrollPhysics(parent: parent);
return new TestViewportScrollPosition(
state,
new Tolerance(velocity: 20.0, distance: 1.0),
oldPosition,
physics,
);
}
@override @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( TestViewportScrollPosition(
Scrollable2State state,
Tolerance scrollTolerances,
ScrollPosition oldPosition,
ScrollPhysics physics, ScrollPhysics physics,
) : super(state, scrollTolerances, oldPosition, physics); AbstractScrollState state,
ScrollPosition oldPosition,
) : super(physics, state, oldPosition);
@override @override
bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) { bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
...@@ -75,85 +71,87 @@ void main() { ...@@ -75,85 +71,87 @@ void main() {
testWidgets('Evil test of sliver features - 1', (WidgetTester tester) async { testWidgets('Evil test of sliver features - 1', (WidgetTester tester) async {
final GlobalKey centerKey = new GlobalKey(); final GlobalKey centerKey = new GlobalKey();
await tester.pumpWidget( await tester.pumpWidget(
new Scrollbar2( new ScrollConfiguration2(
child: new TestScrollable( behavior: new TestBehavior(),
axisDirection: AxisDirection.down, child: new Scrollbar2(
center: centerKey, child: new TestScrollable(
anchor: 0.25, axisDirection: AxisDirection.down,
physics: const ClampingScrollPhysics(), center: centerKey,
scrollBehavior: new TestBehavior(), anchor: 0.25,
slivers: <Widget>[ physics: const TestScrollPhysics(),
new SliverToBoxAdapter(child: new Container(height: 5.0)), slivers: <Widget>[
new SliverToBoxAdapter(child: new Container(height: 520.0)), 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 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 SliverToBoxAdapter(child: new Container(height: 520.0)), new SliverAppBar(delegate: new TestSliverAppBarDelegate(150.0), pinned: true),
new SliverPadding( new SliverToBoxAdapter(child: new Container(height: 520.0)),
padding: new EdgeInsets.all(50.0), new SliverPadding(
child: new SliverToBoxAdapter(child: new Container(height: 520.0)), 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(child: new Container(height: 520.0)), new SliverAppBar(delegate: new TestSliverAppBarDelegate(150.0), floating: true),
new SliverToBoxAdapter(key: centerKey, child: new Container(height: 520.0)), // ------------------------ CENTER ------------------------ new SliverToBoxAdapter(child: new Container(height: 520.0)),
new SliverAppBar(delegate: new TestSliverAppBarDelegate(150.0), pinned: true), new SliverToBoxAdapter(key: centerKey, child: new Container(height: 520.0)), // ------------------------ CENTER ------------------------
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 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( new SliverToBoxAdapter(child: new Container(height: 520.0)),
padding: new EdgeInsets.all(50.0), new SliverPadding(
child: new SliverAppBar(delegate: new TestSliverAppBarDelegate(250.0), pinned: true), 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: 520.0)),
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 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), 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)), new SliverAppBar(delegate: new TestSliverAppBarDelegate(250.0), pinned: true),
new SliverToBoxAdapter(child: new Container(height: 520.0)), new SliverAppBar(delegate: new TestSliverAppBarDelegate(250.0)),
new SliverAppBar(delegate: new TestSliverAppBarDelegate(150.0), floating: true), 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 SliverAppBar(delegate: new TestSliverAppBarDelegate(150.0), floating: true), new SliverToBoxAdapter(child: new Container(height: 520.0)),
new SliverToBoxAdapter(child: new Container(height: 5.0)), new SliverAppBar(delegate: new TestSliverAppBarDelegate(150.0), floating: true),
new SliverBlock( new SliverToBoxAdapter(child: new Container(height: 5.0)),
delegate: new SliverChildListDelegate(<Widget>[ new SliverBlock(
new Container(height: 50.0), 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 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 SliverAppBar(delegate: new TestSliverAppBarDelegate(250.0)), new SliverAppBar(delegate: new TestSliverAppBarDelegate(250.0)),
new SliverPadding( new SliverAppBar(delegate: new TestSliverAppBarDelegate(250.0)),
padding: new EdgeInsets.symmetric(horizontal: 50.0), new SliverPadding(
child: new SliverToBoxAdapter(child: new Container(height: 520.0)), 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: 520.0)),
new SliverToBoxAdapter(child: new Container(height: 5.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)); position.animate(to: 10000.0, curve: Curves.linear, duration: const Duration(minutes: 1));
await tester.pump(const Duration(milliseconds: 10)); await tester.pump(const Duration(milliseconds: 10));
......
...@@ -33,7 +33,7 @@ void main() { ...@@ -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 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); assert(max < 10000.0);
expect(max, 1450.0); expect(max, 1450.0);
......
...@@ -65,7 +65,6 @@ class TestScrollable extends StatelessWidget { ...@@ -65,7 +65,6 @@ class TestScrollable extends StatelessWidget {
this.physics, this.physics,
this.anchor: 0.0, this.anchor: 0.0,
this.center, this.center,
this.scrollBehavior,
this.slivers: const <Widget>[], this.slivers: const <Widget>[],
}) { }) {
assert(slivers != null); assert(slivers != null);
...@@ -79,8 +78,6 @@ class TestScrollable extends StatelessWidget { ...@@ -79,8 +78,6 @@ class TestScrollable extends StatelessWidget {
final Key center; final Key center;
final ScrollBehavior2 scrollBehavior;
final List<Widget> slivers; final List<Widget> slivers;
Axis get axis => axisDirectionToAxis(axisDirection); Axis get axis => axisDirectionToAxis(axisDirection);
...@@ -90,7 +87,6 @@ class TestScrollable extends StatelessWidget { ...@@ -90,7 +87,6 @@ class TestScrollable extends StatelessWidget {
return new Scrollable2( return new Scrollable2(
axisDirection: axisDirection, axisDirection: axisDirection,
physics: physics, physics: physics,
scrollBehavior: scrollBehavior,
viewportBuilder: (BuildContext context, ViewportOffset offset) { viewportBuilder: (BuildContext context, ViewportOffset offset) {
return new Viewport2( return new Viewport2(
axisDirection: axisDirection, 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