mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Make min/max fling velocity and min fling distance ScrollPhysics properties (#8928)
This commit is contained in:
parent
14933de986
commit
dac80aac89
@ -20,10 +20,10 @@ enum _DragState {
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [DragGestureRecognizer.onDown], which uses [GestureDragDownCallback].
|
||||
/// * [DragStartDetails], the details for [GestureDragStartCallback].
|
||||
/// * [DragUpdateDetails], the details for [GestureDragUpdateCallback].
|
||||
/// * [DragEndDetails], the details for [GestureDragEndCallback].
|
||||
/// * [DragGestureRecognizer.onDown], which uses [GestureDragDownCallback].
|
||||
/// * [DragStartDetails], the details for [GestureDragStartCallback].
|
||||
/// * [DragUpdateDetails], the details for [GestureDragUpdateCallback].
|
||||
/// * [DragEndDetails], the details for [GestureDragEndCallback].
|
||||
class DragDownDetails {
|
||||
/// Creates details for a [GestureDragDownCallback].
|
||||
///
|
||||
@ -53,10 +53,10 @@ typedef void GestureDragDownCallback(DragDownDetails details);
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [DragGestureRecognizer.onStart], which uses [GestureDragStartCallback].
|
||||
/// * [DragDownDetails], the details for [GestureDragDownCallback].
|
||||
/// * [DragUpdateDetails], the details for [GestureDragUpdateCallback].
|
||||
/// * [DragEndDetails], the details for [GestureDragEndCallback].
|
||||
/// * [DragGestureRecognizer.onStart], which uses [GestureDragStartCallback].
|
||||
/// * [DragDownDetails], the details for [GestureDragDownCallback].
|
||||
/// * [DragUpdateDetails], the details for [GestureDragUpdateCallback].
|
||||
/// * [DragEndDetails], the details for [GestureDragEndCallback].
|
||||
class DragStartDetails {
|
||||
/// Creates details for a [GestureDragStartCallback].
|
||||
///
|
||||
@ -86,10 +86,10 @@ typedef void GestureDragStartCallback(DragStartDetails details);
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [DragGestureRecognizer.onUpdate], which uses [GestureDragUpdateCallback].
|
||||
/// * [DragDownDetails], the details for [GestureDragDownCallback].
|
||||
/// * [DragStartDetails], the details for [GestureDragStartCallback].
|
||||
/// * [DragEndDetails], the details for [GestureDragEndCallback].
|
||||
/// * [DragGestureRecognizer.onUpdate], which uses [GestureDragUpdateCallback].
|
||||
/// * [DragDownDetails], the details for [GestureDragDownCallback].
|
||||
/// * [DragStartDetails], the details for [GestureDragStartCallback].
|
||||
/// * [DragEndDetails], the details for [GestureDragEndCallback].
|
||||
class DragUpdateDetails {
|
||||
/// Creates details for a [DragUpdateDetails].
|
||||
///
|
||||
@ -150,10 +150,10 @@ typedef void GestureDragUpdateCallback(DragUpdateDetails details);
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [DragGestureRecognizer.onEnd], which uses [GestureDragEndCallback].
|
||||
/// * [DragDownDetails], the details for [GestureDragDownCallback].
|
||||
/// * [DragStartDetails], the details for [GestureDragStartCallback].
|
||||
/// * [DragUpdateDetails], the details for [GestureDragUpdateCallback].
|
||||
/// * [DragGestureRecognizer.onEnd], which uses [GestureDragEndCallback].
|
||||
/// * [DragDownDetails], the details for [GestureDragDownCallback].
|
||||
/// * [DragStartDetails], the details for [GestureDragStartCallback].
|
||||
/// * [DragUpdateDetails], the details for [GestureDragUpdateCallback].
|
||||
class DragEndDetails {
|
||||
/// Creates details for a [GestureDragEndCallback].
|
||||
///
|
||||
@ -204,12 +204,6 @@ typedef void GestureDragEndCallback(DragEndDetails details);
|
||||
/// See [DragGestureRecognizer.onCancel].
|
||||
typedef void GestureDragCancelCallback();
|
||||
|
||||
bool _isFlingGesture(Velocity velocity) {
|
||||
assert(velocity != null);
|
||||
final double speedSquared = velocity.pixelsPerSecond.distanceSquared;
|
||||
return speedSquared > kMinFlingVelocity * kMinFlingVelocity;
|
||||
}
|
||||
|
||||
/// Recognizes movement.
|
||||
///
|
||||
/// In contrast to [MultiDragGestureRecognizer], [DragGestureRecognizer]
|
||||
@ -256,10 +250,30 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
/// The pointer that previously triggered [onDown] did not complete.
|
||||
GestureDragCancelCallback onCancel;
|
||||
|
||||
/// The minimum distance an input pointer drag must have moved to
|
||||
/// to be considered a fling gesture.
|
||||
///
|
||||
/// This value is typically compared with the distance traveled along the
|
||||
/// scrolling axis. If null then [kTouchSlop] is used.
|
||||
double minFlingDistance;
|
||||
|
||||
/// The minimum velocity for an input pointer drag to be considered fling.
|
||||
///
|
||||
/// This value is typically compared with the magnitude of fling gesture's
|
||||
/// velocity along the scrolling axis. If null then [kMinFlingVelocity]
|
||||
/// is used.
|
||||
double minFlingVelocity;
|
||||
|
||||
/// Fling velocity magnitudes will be clamped to this value.
|
||||
///
|
||||
/// If null then [kMaxFlingVelocity] is used.
|
||||
double maxFlingVelocity;
|
||||
|
||||
_DragState _state = _DragState.ready;
|
||||
Point _initialPosition;
|
||||
Offset _pendingDragOffset;
|
||||
|
||||
bool _isFlingGesture(VelocityEstimate estimate);
|
||||
Offset _getDeltaForDetails(Offset delta);
|
||||
double _getPrimaryValueFromOffset(Offset value);
|
||||
bool get _hasSufficientPendingDragDeltaToAccept;
|
||||
@ -345,11 +359,10 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
final VelocityTracker tracker = _velocityTrackers[pointer];
|
||||
assert(tracker != null);
|
||||
|
||||
Velocity velocity = tracker.getVelocity();
|
||||
if (velocity != null && _isFlingGesture(velocity)) {
|
||||
final Offset pixelsPerSecond = velocity.pixelsPerSecond;
|
||||
if (pixelsPerSecond.distanceSquared > kMaxFlingVelocity * kMaxFlingVelocity)
|
||||
velocity = new Velocity(pixelsPerSecond: (pixelsPerSecond / pixelsPerSecond.distance) * kMaxFlingVelocity);
|
||||
final VelocityEstimate estimate = tracker.getVelocityEstimate();
|
||||
if (estimate != null && _isFlingGesture(estimate)) {
|
||||
final Velocity velocity = new Velocity(pixelsPerSecond: estimate.pixelsPerSecond)
|
||||
.clampMagnitude(minFlingVelocity ?? kMinFlingVelocity, maxFlingVelocity ?? kMaxFlingVelocity);
|
||||
invokeCallback<Null>('onEnd', () => onEnd(new DragEndDetails( // ignore: STRONG_MODE_INVALID_CAST_FUNCTION_EXPR, https://github.com/dart-lang/sdk/issues/27504
|
||||
velocity: velocity,
|
||||
primaryVelocity: _getPrimaryValueFromOffset(velocity.pixelsPerSecond),
|
||||
@ -379,6 +392,13 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
///
|
||||
/// * [VerticalMultiDragGestureRecognizer]
|
||||
class VerticalDragGestureRecognizer extends DragGestureRecognizer {
|
||||
@override
|
||||
bool _isFlingGesture(VelocityEstimate estimate) {
|
||||
final double minVelocity = minFlingVelocity ?? kMinFlingVelocity;
|
||||
final double minDistance = minFlingDistance ?? kTouchSlop;
|
||||
return estimate.pixelsPerSecond.dy.abs() > minVelocity && estimate.offset.dy.abs() > minDistance;
|
||||
}
|
||||
|
||||
@override
|
||||
bool get _hasSufficientPendingDragDeltaToAccept => _pendingDragOffset.dy.abs() > kTouchSlop;
|
||||
|
||||
@ -400,6 +420,13 @@ class VerticalDragGestureRecognizer extends DragGestureRecognizer {
|
||||
///
|
||||
/// * [HorizontalMultiDragGestureRecognizer]
|
||||
class HorizontalDragGestureRecognizer extends DragGestureRecognizer {
|
||||
@override
|
||||
bool _isFlingGesture(VelocityEstimate estimate) {
|
||||
final double minVelocity = minFlingVelocity ?? kMinFlingVelocity;
|
||||
final double minDistance = minFlingDistance ?? kTouchSlop;
|
||||
return estimate.pixelsPerSecond.dx.abs() > minVelocity && estimate.offset.dx.abs() > minDistance;
|
||||
}
|
||||
|
||||
@override
|
||||
bool get _hasSufficientPendingDragDeltaToAccept => _pendingDragOffset.dx.abs() > kTouchSlop;
|
||||
|
||||
@ -420,6 +447,14 @@ class HorizontalDragGestureRecognizer extends DragGestureRecognizer {
|
||||
/// * [ImmediateMultiDragGestureRecognizer]
|
||||
/// * [DelayedMultiDragGestureRecognizer]
|
||||
class PanGestureRecognizer extends DragGestureRecognizer {
|
||||
@override
|
||||
bool _isFlingGesture(VelocityEstimate estimate) {
|
||||
final double minVelocity = minFlingVelocity ?? kMinFlingVelocity;
|
||||
final double minDistance = minFlingDistance ?? kTouchSlop;
|
||||
return estimate.pixelsPerSecond.distanceSquared > minVelocity * minVelocity &&
|
||||
estimate.offset.distanceSquared > minDistance * minDistance;
|
||||
}
|
||||
|
||||
@override
|
||||
bool get _hasSufficientPendingDragDeltaToAccept {
|
||||
return _pendingDragOffset.distance > kPanSlop;
|
||||
|
||||
@ -8,135 +8,6 @@ import 'lsq_solver.dart';
|
||||
|
||||
export 'dart:ui' show Point, Offset;
|
||||
|
||||
class _Estimate {
|
||||
const _Estimate({ this.xCoefficients, this.yCoefficients, this.time, this.degree, this.confidence });
|
||||
|
||||
final List<double> xCoefficients;
|
||||
final List<double> yCoefficients;
|
||||
final Duration time;
|
||||
final int degree;
|
||||
final double confidence;
|
||||
}
|
||||
|
||||
abstract class _VelocityTrackerStrategy {
|
||||
void addMovement(Duration timeStamp, Point position);
|
||||
_Estimate getEstimate();
|
||||
void clear();
|
||||
}
|
||||
|
||||
class _Movement {
|
||||
const _Movement(this.eventTime, this.position);
|
||||
|
||||
final Duration eventTime;
|
||||
final Point position;
|
||||
|
||||
@override
|
||||
String toString() => 'Movement($position at $eventTime)';
|
||||
}
|
||||
|
||||
// TODO: On iOS we're not necccessarily seeing all of the motion events. See:
|
||||
// https://github.com/flutter/flutter/issues/4737#issuecomment-241076994
|
||||
|
||||
class _LeastSquaresVelocityTrackerStrategy extends _VelocityTrackerStrategy {
|
||||
_LeastSquaresVelocityTrackerStrategy(this.degree);
|
||||
|
||||
final int degree;
|
||||
|
||||
final List<_Movement> _movements = new List<_Movement>(kHistorySize);
|
||||
int _index = 0;
|
||||
|
||||
static const int kHistorySize = 20;
|
||||
static const int kHorizonMilliseconds = 100;
|
||||
|
||||
// The maximum length of time between two move events to allow before
|
||||
// assuming the pointer stopped.
|
||||
static const int kAssumePointerMoveStoppedMilliseconds = 40;
|
||||
|
||||
@override
|
||||
void addMovement(Duration timeStamp, Point position) {
|
||||
_index += 1;
|
||||
if (_index == kHistorySize)
|
||||
_index = 0;
|
||||
_movements[_index] = new _Movement(timeStamp, position);
|
||||
}
|
||||
|
||||
@override
|
||||
_Estimate getEstimate() {
|
||||
// Iterate over movement samples in reverse time order and collect samples.
|
||||
final List<double> x = <double>[];
|
||||
final List<double> y = <double>[];
|
||||
final List<double> w = <double>[];
|
||||
final List<double> time = <double>[];
|
||||
int m = 0;
|
||||
int index = _index;
|
||||
|
||||
final _Movement newestMovement = _movements[index];
|
||||
_Movement previousMovement = newestMovement;
|
||||
if (newestMovement == null)
|
||||
return null;
|
||||
|
||||
do {
|
||||
final _Movement movement = _movements[index];
|
||||
if (movement == null)
|
||||
break;
|
||||
|
||||
final double age = (newestMovement.eventTime - movement.eventTime).inMilliseconds.toDouble();
|
||||
final double delta = (movement.eventTime - previousMovement.eventTime).inMilliseconds.abs().toDouble();
|
||||
previousMovement = movement;
|
||||
if (age > kHorizonMilliseconds || delta > kAssumePointerMoveStoppedMilliseconds)
|
||||
break;
|
||||
|
||||
final Point position = movement.position;
|
||||
x.add(position.x);
|
||||
y.add(position.y);
|
||||
w.add(1.0);
|
||||
time.add(-age);
|
||||
index = (index == 0 ? kHistorySize : index) - 1;
|
||||
|
||||
m += 1;
|
||||
} while (m < kHistorySize);
|
||||
|
||||
// Calculate a least squares polynomial fit.
|
||||
int n = degree;
|
||||
if (n > m - 1)
|
||||
n = m - 1;
|
||||
|
||||
if (n >= 1) {
|
||||
final LeastSquaresSolver xSolver = new LeastSquaresSolver(time, x, w);
|
||||
final PolynomialFit xFit = xSolver.solve(n);
|
||||
if (xFit != null) {
|
||||
final LeastSquaresSolver ySolver = new LeastSquaresSolver(time, y, w);
|
||||
final PolynomialFit yFit = ySolver.solve(n);
|
||||
if (yFit != null) {
|
||||
return new _Estimate(
|
||||
xCoefficients: xFit.coefficients,
|
||||
yCoefficients: yFit.coefficients,
|
||||
time: newestMovement.eventTime,
|
||||
degree: n,
|
||||
confidence: xFit.confidence * yFit.confidence
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No velocity data available for this pointer, but we do have its current
|
||||
// position.
|
||||
return new _Estimate(
|
||||
xCoefficients: <double>[ x[0] ],
|
||||
yCoefficients: <double>[ y[0] ],
|
||||
time: newestMovement.eventTime,
|
||||
degree: 0,
|
||||
confidence: 1.0
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void clear() {
|
||||
_index = -1;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// A velocity in two dimensions.
|
||||
class Velocity {
|
||||
/// Creates a velocity.
|
||||
@ -165,6 +36,27 @@ class Velocity {
|
||||
pixelsPerSecond: pixelsPerSecond + other.pixelsPerSecond);
|
||||
}
|
||||
|
||||
/// Return a velocity whose magnitude has been clamped to [minValue]
|
||||
/// and [maxValue].
|
||||
///
|
||||
/// If the magnitude of this Velocity is less than minValue then return a new
|
||||
/// Velocity with the same direction and with magnitude [minValue]. Similarly,
|
||||
/// if the magnitude of this Velocity is greater than maxValue then return a
|
||||
/// new Velocity with the same direction and magnitude [maxValue].
|
||||
///
|
||||
/// If the magnitude of this Velocity is within the specified bounds then
|
||||
/// just return this.
|
||||
Velocity clampMagnitude(double minValue, double maxValue) {
|
||||
assert(minValue != null && minValue >= 0.0);
|
||||
assert(maxValue != null && maxValue >= 0.0 && maxValue >= minValue);
|
||||
final double valueSquared = pixelsPerSecond.distanceSquared;
|
||||
if (valueSquared > maxValue * maxValue)
|
||||
return new Velocity(pixelsPerSecond: (pixelsPerSecond / pixelsPerSecond.distance) * maxValue);
|
||||
if (valueSquared < minValue * minValue)
|
||||
return new Velocity(pixelsPerSecond: (pixelsPerSecond / pixelsPerSecond.distance) * minValue);
|
||||
return this;
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
if (other is! Velocity)
|
||||
@ -180,39 +72,152 @@ class Velocity {
|
||||
String toString() => 'Velocity(${pixelsPerSecond.dx.toStringAsFixed(1)}, ${pixelsPerSecond.dy.toStringAsFixed(1)})';
|
||||
}
|
||||
|
||||
/// Computes a pointer velocity based on data from PointerMove events.
|
||||
/// A two dimensional velocity estimate.
|
||||
///
|
||||
/// VelocityEstimates are computed by [VelocityTracker.getVelocityEstimate]. An
|
||||
/// estimate's [confidence] measures how well the the velocity tracker's position
|
||||
/// data fit a straight line, [duration] is the time that elapsed between the
|
||||
/// first and last position sample used to compute the velocity, and [offset]
|
||||
/// is similarly the difference between the first and last positions.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * VelocityTracker, which computes [VelocityEstimate]s.
|
||||
/// * Velocity, which encapsulates (just) a velocity vector and provides some
|
||||
/// useful velocity operations.
|
||||
class VelocityEstimate {
|
||||
/// Creates a dimensional velocity estimate.
|
||||
const VelocityEstimate({
|
||||
this.pixelsPerSecond,
|
||||
this.confidence,
|
||||
this.duration,
|
||||
this.offset,
|
||||
});
|
||||
|
||||
/// The number of pixels per second of velocity in the x and y directions.
|
||||
final Offset pixelsPerSecond;
|
||||
|
||||
/// A value between 0.0 and 1.0 that indicates how well [VelocityTracker]
|
||||
/// was able to fit a straight line to its position data.
|
||||
///
|
||||
/// The value of this property is 1.0 for a perfect fit, 0.0 for a poor fit.
|
||||
final double confidence;
|
||||
|
||||
/// The time that elapsed between the first and last position sample used
|
||||
/// to compute [pixelsPerSecond].
|
||||
final Duration duration;
|
||||
|
||||
/// The difference between the first and last position sample used
|
||||
/// to compute [pixelsPerSecond].
|
||||
final Offset offset;
|
||||
|
||||
@override
|
||||
String toString() => 'VelocityEstimate(${pixelsPerSecond.dx.toStringAsFixed(1)}, ${pixelsPerSecond.dy.toStringAsFixed(1)})';
|
||||
}
|
||||
|
||||
class _PointAtTime {
|
||||
const _PointAtTime(this.point, this.time);
|
||||
|
||||
final Duration time;
|
||||
final Point point;
|
||||
|
||||
@override
|
||||
String toString() => '_PointAtTime($point at $time)';
|
||||
}
|
||||
|
||||
/// Computes a pointer's velocity based on data from PointerMove events.
|
||||
///
|
||||
/// The input data is provided by calling addPosition(). Adding data
|
||||
/// is cheap.
|
||||
///
|
||||
/// To obtain a velocity, call getVelocity(). This will compute the
|
||||
/// velocity based on the data added so far. Only call this when you
|
||||
/// need to use the velocity, as it is comparatively expensive.
|
||||
/// To obtain a velocity, call [getVelocity] or [getVelocityEstimate].
|
||||
/// This will compute the velocity based on the data added so far. Only
|
||||
/// call this when you need to use the velocity, as it is comparatively
|
||||
/// expensive.
|
||||
///
|
||||
/// The quality of the velocity estimation will be better if more data
|
||||
/// points have been received.
|
||||
class VelocityTracker {
|
||||
/// Creates a velocity tracker.
|
||||
VelocityTracker() : _strategy = _createStrategy();
|
||||
static const int _kAssumePointerMoveStoppedMilliseconds = 40;
|
||||
static const int _kHistorySize = 20;
|
||||
static const int _kHorizonMilliseconds = 100;
|
||||
static const int _kMinSampleSize = 3;
|
||||
|
||||
// VelocityTracker is designed to easily be adapted to using different
|
||||
// algorithms in the future, potentially picking algorithms on the fly based
|
||||
// on hardware or other environment factors.
|
||||
//
|
||||
// For now, though, we just use the _LeastSquaresVelocityTrackerStrategy
|
||||
// defined above.
|
||||
// Circular buffer; current sample at _index.
|
||||
final List<_PointAtTime> _samples = new List<_PointAtTime>(_kHistorySize);
|
||||
int _index = 0;
|
||||
|
||||
// TODO(ianh): Simplify this. We don't see to need multiple stategies.
|
||||
|
||||
static _VelocityTrackerStrategy _createStrategy() {
|
||||
return new _LeastSquaresVelocityTrackerStrategy(2);
|
||||
void addPosition(Duration time, Point position) {
|
||||
_index += 1;
|
||||
if (_index == _kHistorySize)
|
||||
_index = 0;
|
||||
_samples[_index] = new _PointAtTime(position, time);
|
||||
}
|
||||
|
||||
_VelocityTrackerStrategy _strategy;
|
||||
VelocityEstimate getVelocityEstimate() {
|
||||
final List<double> x = <double>[];
|
||||
final List<double> y = <double>[];
|
||||
final List<double> w = <double>[];
|
||||
final List<double> time = <double>[];
|
||||
int sampleCount = 0;
|
||||
int index = _index;
|
||||
|
||||
/// Add a given position corresponding to a specific time.
|
||||
void addPosition(Duration timeStamp, Point position) {
|
||||
_strategy.addMovement(timeStamp, position);
|
||||
final _PointAtTime newestSample = _samples[index];
|
||||
if (newestSample == null)
|
||||
return null;
|
||||
|
||||
_PointAtTime previousSample = newestSample;
|
||||
_PointAtTime oldestSample = newestSample;
|
||||
|
||||
// Starting with the most recent PointAtTime sample, iterate backwards while
|
||||
// the samples represent continuous motion.
|
||||
do {
|
||||
final _PointAtTime sample = _samples[index];
|
||||
if (sample == null)
|
||||
break;
|
||||
|
||||
final double age = (newestSample.time - sample.time).inMilliseconds.toDouble();
|
||||
final double delta = (sample.time - previousSample.time).inMilliseconds.abs().toDouble();
|
||||
previousSample = sample;
|
||||
if (age > _kHorizonMilliseconds || delta > _kAssumePointerMoveStoppedMilliseconds)
|
||||
break;
|
||||
|
||||
oldestSample = sample;
|
||||
final Point position = sample.point;
|
||||
x.add(position.x);
|
||||
y.add(position.y);
|
||||
w.add(1.0);
|
||||
time.add(-age);
|
||||
index = (index == 0 ? _kHistorySize : index) - 1;
|
||||
|
||||
sampleCount += 1;
|
||||
} while (sampleCount < _kHistorySize);
|
||||
|
||||
if (sampleCount >= _kMinSampleSize) {
|
||||
final LeastSquaresSolver xSolver = new LeastSquaresSolver(time, x, w);
|
||||
final PolynomialFit xFit = xSolver.solve(2);
|
||||
if (xFit != null) {
|
||||
final LeastSquaresSolver ySolver = new LeastSquaresSolver(time, y, w);
|
||||
final PolynomialFit yFit = ySolver.solve(2);
|
||||
if (yFit != null) {
|
||||
return new VelocityEstimate( // convert from pixels/ms to pixels/s
|
||||
pixelsPerSecond: new Offset(xFit.coefficients[1] * 1000, yFit.coefficients[1] * 1000),
|
||||
confidence: xFit.confidence * yFit.confidence,
|
||||
duration: newestSample.time - oldestSample.time,
|
||||
offset: newestSample.point - oldestSample.point,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We're unable to make a velocity estimate but we did have at least one
|
||||
// valid pointer position.
|
||||
return new VelocityEstimate(
|
||||
pixelsPerSecond: Offset.zero,
|
||||
confidence: 1.0,
|
||||
duration: newestSample.time - oldestSample.time,
|
||||
offset: newestSample.point - oldestSample.point,
|
||||
);
|
||||
}
|
||||
|
||||
/// Computes the velocity of the pointer at the time of the last
|
||||
@ -223,15 +228,9 @@ class VelocityTracker {
|
||||
/// getVelocity() will return null if no estimate is available or if
|
||||
/// the velocity is zero.
|
||||
Velocity getVelocity() {
|
||||
final _Estimate estimate = _strategy.getEstimate();
|
||||
if (estimate != null && estimate.degree >= 1) {
|
||||
return new Velocity(
|
||||
pixelsPerSecond: new Offset( // convert from pixels/ms to pixels/s
|
||||
estimate.xCoefficients[1] * 1000,
|
||||
estimate.yCoefficients[1] * 1000
|
||||
)
|
||||
);
|
||||
}
|
||||
return null;
|
||||
final VelocityEstimate estimate = getVelocityEstimate();
|
||||
if (estimate == null || estimate.pixelsPerSecond == Offset.zero)
|
||||
return null;
|
||||
return new Velocity(pixelsPerSecond: estimate.pixelsPerSecond);
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/gestures.dart' show kMinFlingVelocity;
|
||||
import 'package:flutter/physics.dart';
|
||||
|
||||
import 'overscroll_indicator.dart';
|
||||
@ -22,10 +23,10 @@ export 'scroll_position.dart' show ScrollPhysics;
|
||||
///
|
||||
/// 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.
|
||||
/// * [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 {
|
||||
/// Creates scroll physics that bounce back from the edge.
|
||||
const BouncingScrollPhysics({ ScrollPhysics parent }) : super(parent);
|
||||
@ -81,6 +82,12 @@ class BouncingScrollPhysics extends ScrollPhysics {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// The ballistic simulation here decelerates more slowly than the one for
|
||||
// ClampingScrollPhysics so we require a more deliberate input gesture
|
||||
// to trigger a fling.
|
||||
@override
|
||||
double get minFlingVelocity => kMinFlingVelocity * 2.0;
|
||||
}
|
||||
|
||||
/// Scroll physics for environments that prevent the scroll offset from reaching
|
||||
@ -90,13 +97,13 @@ class BouncingScrollPhysics extends ScrollPhysics {
|
||||
///
|
||||
/// 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.
|
||||
/// * [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 {
|
||||
/// Creates scroll physics that prevent the scroll offset from exceeding the
|
||||
/// bounds of the content..
|
||||
@ -156,10 +163,10 @@ class ClampingScrollPhysics extends ScrollPhysics {
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [BouncingScrollPhysics], which provides the bouncing overscroll behavior
|
||||
/// found on iOS.
|
||||
/// * [ClampingScrollPhysics], which provides the clamping overscroll behavior
|
||||
/// found on Android.
|
||||
/// * [BouncingScrollPhysics], which provides the bouncing overscroll behavior
|
||||
/// found on iOS.
|
||||
/// * [ClampingScrollPhysics], which provides the clamping overscroll behavior
|
||||
/// found on Android.
|
||||
class AlwaysScrollableScrollPhysics extends ScrollPhysics {
|
||||
/// Creates scroll physics that always lets the user scroll.
|
||||
const AlwaysScrollableScrollPhysics({ ScrollPhysics parent }) : super(parent);
|
||||
|
||||
@ -106,6 +106,33 @@ abstract class ScrollPhysics {
|
||||
|
||||
Tolerance get tolerance => parent?.tolerance ?? _kDefaultTolerance;
|
||||
|
||||
/// The minimum distance an input pointer drag must have moved to
|
||||
/// to be considered a scroll fling gesture.
|
||||
///
|
||||
/// This value is typically compared with the distance traveled along the
|
||||
/// scrolling axis.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [VelocityTracker.getVelocityEstimate], which computes the velocity
|
||||
/// of a press-drag-release gesture.
|
||||
double get minFlingDistance => parent?.minFlingDistance ?? kTouchSlop;
|
||||
|
||||
/// The minimum velocity for an input pointer drag to be considered a
|
||||
/// scroll fling.
|
||||
///
|
||||
/// This value is typically compared with the magnitude of fling gesture's
|
||||
/// velocity along the scrolling axis.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [VelocityTracker.getVelocityEstimate], which computes the velocity
|
||||
/// of a press-drag-release gesture.
|
||||
double get minFlingVelocity => parent?.minFlingVelocity ?? kMinFlingVelocity;
|
||||
|
||||
/// Scroll fling velocity magnitudes will be clamped to this value.
|
||||
double get maxFlingVelocity => parent?.maxFlingVelocity ?? kMaxFlingVelocity;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
if (parent == null)
|
||||
|
||||
@ -118,13 +118,14 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin
|
||||
ScrollPosition _position;
|
||||
|
||||
ScrollBehavior _configuration;
|
||||
ScrollPhysics _physics;
|
||||
|
||||
// only call this from places that will definitely trigger a rebuild
|
||||
// Only call this from places that will definitely trigger a rebuild.
|
||||
void _updatePosition() {
|
||||
_configuration = ScrollConfiguration.of(context);
|
||||
ScrollPhysics physics = _configuration.getScrollPhysics(context);
|
||||
_physics = _configuration.getScrollPhysics(context);
|
||||
if (config.physics != null)
|
||||
physics = config.physics.applyTo(physics);
|
||||
_physics = config.physics.applyTo(_physics);
|
||||
final ScrollController controller = config.controller;
|
||||
final ScrollPosition oldPosition = position;
|
||||
if (oldPosition != null) {
|
||||
@ -135,8 +136,8 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin
|
||||
scheduleMicrotask(oldPosition.dispose);
|
||||
}
|
||||
|
||||
_position = controller?.createScrollPosition(physics, this, oldPosition)
|
||||
?? ScrollController.createDefaultScrollPosition(physics, this, oldPosition);
|
||||
_position = controller?.createScrollPosition(_physics, this, oldPosition)
|
||||
?? ScrollController.createDefaultScrollPosition(_physics, this, oldPosition);
|
||||
assert(position != null);
|
||||
controller?.attach(position);
|
||||
}
|
||||
@ -201,7 +202,10 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin
|
||||
..onDown = _handleDragDown
|
||||
..onStart = _handleDragStart
|
||||
..onUpdate = _handleDragUpdate
|
||||
..onEnd = _handleDragEnd;
|
||||
..onEnd = _handleDragEnd
|
||||
..minFlingDistance = _physics?.minFlingDistance
|
||||
..minFlingVelocity = _physics?.minFlingVelocity
|
||||
..maxFlingVelocity = _physics?.maxFlingVelocity;
|
||||
}
|
||||
};
|
||||
break;
|
||||
@ -212,7 +216,10 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin
|
||||
..onDown = _handleDragDown
|
||||
..onStart = _handleDragStart
|
||||
..onUpdate = _handleDragUpdate
|
||||
..onEnd = _handleDragEnd;
|
||||
..onEnd = _handleDragEnd
|
||||
..minFlingDistance = _physics?.minFlingDistance
|
||||
..minFlingVelocity = _physics?.minFlingVelocity
|
||||
..maxFlingVelocity = _physics?.maxFlingVelocity;
|
||||
}
|
||||
};
|
||||
break;
|
||||
|
||||
@ -102,7 +102,7 @@ void main() {
|
||||
expect(showBottomSheetThenCalled, isFalse);
|
||||
expect(find.text('BottomSheet'), findsOneWidget);
|
||||
|
||||
await tester.fling(find.text('BottomSheet'), const Offset(0.0, 20.0), 1000.0);
|
||||
await tester.fling(find.text('BottomSheet'), const Offset(0.0, 30.0), 1000.0);
|
||||
await tester.pump(); // drain the microtask queue (Future completion callback)
|
||||
|
||||
expect(showBottomSheetThenCalled, isTrue);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user