blob: 89b728ba60547f01b7bf9ea05448ac045ea8c419 [file] [log] [blame]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// @dart = 2.8
import 'dart:ui' show Offset;
import 'package:flutter/foundation.dart';
import 'lsq_solver.dart';
export 'dart:ui' show Offset;
/// A velocity in two dimensions.
@immutable
class Velocity {
/// Creates a velocity.
///
/// The [pixelsPerSecond] argument must not be null.
const Velocity({
@required this.pixelsPerSecond,
}) : assert(pixelsPerSecond != null);
/// A velocity that isn't moving at all.
static const Velocity zero = Velocity(pixelsPerSecond: Offset.zero);
/// The number of pixels per second of velocity in the x and y directions.
final Offset pixelsPerSecond;
/// Return the negation of a velocity.
Velocity operator -() => Velocity(pixelsPerSecond: -pixelsPerSecond);
/// Return the difference of two velocities.
Velocity operator -(Velocity other) {
return Velocity(
pixelsPerSecond: pixelsPerSecond - other.pixelsPerSecond);
}
/// Return the sum of two velocities.
Velocity operator +(Velocity other) {
return 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 Velocity(pixelsPerSecond: (pixelsPerSecond / pixelsPerSecond.distance) * maxValue);
if (valueSquared < minValue * minValue)
return Velocity(pixelsPerSecond: (pixelsPerSecond / pixelsPerSecond.distance) * minValue);
return this;
}
@override
bool operator ==(Object other) {
return other is Velocity
&& other.pixelsPerSecond == pixelsPerSecond;
}
@override
int get hashCode => pixelsPerSecond.hashCode;
@override
String toString() => 'Velocity(${pixelsPerSecond.dx.toStringAsFixed(1)}, ${pixelsPerSecond.dy.toStringAsFixed(1)})';
}
/// A two dimensional velocity estimate.
///
/// VelocityEstimates are computed by [VelocityTracker.getVelocityEstimate]. An
/// estimate's [confidence] measures how well 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.
///
/// [pixelsPerSecond], [confidence], [duration], and [offset] must not be null.
const VelocityEstimate({
@required this.pixelsPerSecond,
@required this.confidence,
@required this.duration,
@required this.offset,
}) : assert(pixelsPerSecond != null),
assert(confidence != null),
assert(duration != null),
assert(offset != null);
/// 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)}; offset: $offset, duration: $duration, confidence: ${confidence.toStringAsFixed(1)})';
}
class _PointAtTime {
const _PointAtTime(this.point, this.time)
: assert(point != null),
assert(time != null);
final Duration time;
final Offset point;
@override
String toString() => '_PointAtTime($point at $time)';
}
/// Computes a pointer's velocity based on data from [PointerMoveEvent]s.
///
/// The input data is provided by calling [addPosition]. Adding data is cheap.
///
/// To obtain a velocity, call [getVelocity] or [getVelocityEstimate]. This will
/// compute the velocity based on the data added so far. Only call these when
/// you need to use the velocity, as they are comparatively expensive.
///
/// The quality of the velocity estimation will be better if more data points
/// have been received.
class VelocityTracker {
static const int _assumePointerMoveStoppedMilliseconds = 40;
static const int _historySize = 20;
static const int _horizonMilliseconds = 100;
static const int _minSampleSize = 3;
// Circular buffer; current sample at _index.
final List<_PointAtTime> _samples = List<_PointAtTime>(_historySize);
int _index = 0;
/// Adds a position as the given time to the tracker.
void addPosition(Duration time, Offset position) {
_index += 1;
if (_index == _historySize)
_index = 0;
_samples[_index] = _PointAtTime(position, time);
}
/// Returns an estimate of the velocity of the object being tracked by the
/// tracker given the current information available to the tracker.
///
/// Information is added using [addPosition].
///
/// Returns null if there is no data on which to base an estimate.
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;
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 > _horizonMilliseconds || delta > _assumePointerMoveStoppedMilliseconds)
break;
oldestSample = sample;
final Offset position = sample.point;
x.add(position.dx);
y.add(position.dy);
w.add(1.0);
time.add(-age);
index = (index == 0 ? _historySize : index) - 1;
sampleCount += 1;
} while (sampleCount < _historySize);
if (sampleCount >= _minSampleSize) {
final LeastSquaresSolver xSolver = LeastSquaresSolver(time, x, w);
final PolynomialFit xFit = xSolver.solve(2);
if (xFit != null) {
final LeastSquaresSolver ySolver = LeastSquaresSolver(time, y, w);
final PolynomialFit yFit = ySolver.solve(2);
if (yFit != null) {
return VelocityEstimate( // convert from pixels/ms to pixels/s
pixelsPerSecond: 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 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
/// provided data point.
///
/// This can be expensive. Only call this when you need the velocity.
///
/// Returns [Velocity.zero] if there is no data from which to compute an
/// estimate or if the estimated velocity is zero.
Velocity getVelocity() {
final VelocityEstimate estimate = getVelocityEstimate();
if (estimate == null || estimate.pixelsPerSecond == Offset.zero)
return Velocity.zero;
return Velocity(pixelsPerSecond: estimate.pixelsPerSecond);
}
}