mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
442 lines
13 KiB
Dart
442 lines
13 KiB
Dart
// 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.
|
|
|
|
import 'dart:math' as math;
|
|
import 'dart:ui' show lerpDouble;
|
|
|
|
import 'package:flutter/animation.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
|
|
// How close the begin and end points must be to an axis to be considered
|
|
// vertical or horizontal.
|
|
const double _kOnAxisDelta = 2.0;
|
|
|
|
/// A [Tween] that interpolates an [Offset] along a circular arc.
|
|
///
|
|
/// This class specializes the interpolation of [Tween<Offset>] so that instead
|
|
/// of a straight line, the intermediate points follow the arc of a circle in a
|
|
/// manner consistent with Material Design principles.
|
|
///
|
|
/// The arc's radius is related to the bounding box that contains the [begin]
|
|
/// and [end] points. If the bounding box is taller than it is wide, then the
|
|
/// center of the circle will be horizontally aligned with the end point.
|
|
/// Otherwise the center of the circle will be aligned with the begin point.
|
|
/// The arc's sweep is always less than or equal to 90 degrees.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [Tween], for a discussion on how to use interpolation objects.
|
|
/// * [MaterialRectArcTween], which extends this concept to interpolating [Rect]s.
|
|
class MaterialPointArcTween extends Tween<Offset> {
|
|
/// Creates a [Tween] for animating [Offset]s along a circular arc.
|
|
///
|
|
/// The [begin] and [end] properties must be non-null before the tween is
|
|
/// first used, but the arguments can be null if the values are going to be
|
|
/// filled in later.
|
|
MaterialPointArcTween({
|
|
super.begin,
|
|
super.end,
|
|
});
|
|
|
|
bool _dirty = true;
|
|
|
|
void _initialize() {
|
|
assert(this.begin != null);
|
|
assert(this.end != null);
|
|
|
|
final Offset begin = this.begin!;
|
|
final Offset end = this.end!;
|
|
|
|
// An explanation with a diagram can be found at https://goo.gl/vMSdRg
|
|
final Offset delta = end - begin;
|
|
final double deltaX = delta.dx.abs();
|
|
final double deltaY = delta.dy.abs();
|
|
final double distanceFromAtoB = delta.distance;
|
|
final Offset c = Offset(end.dx, begin.dy);
|
|
|
|
double sweepAngle() => 2.0 * math.asin(distanceFromAtoB / (2.0 * _radius!));
|
|
|
|
if (deltaX > _kOnAxisDelta && deltaY > _kOnAxisDelta) {
|
|
if (deltaX < deltaY) {
|
|
_radius = distanceFromAtoB * distanceFromAtoB / (c - begin).distance / 2.0;
|
|
_center = Offset(end.dx + _radius! * (begin.dx - end.dx).sign, end.dy);
|
|
if (begin.dx < end.dx) {
|
|
_beginAngle = sweepAngle() * (begin.dy - end.dy).sign;
|
|
_endAngle = 0.0;
|
|
} else {
|
|
_beginAngle = math.pi + sweepAngle() * (end.dy - begin.dy).sign;
|
|
_endAngle = math.pi;
|
|
}
|
|
} else {
|
|
_radius = distanceFromAtoB * distanceFromAtoB / (c - end).distance / 2.0;
|
|
_center = Offset(begin.dx, begin.dy + (end.dy - begin.dy).sign * _radius!);
|
|
if (begin.dy < end.dy) {
|
|
_beginAngle = -math.pi / 2.0;
|
|
_endAngle = _beginAngle! + sweepAngle() * (end.dx - begin.dx).sign;
|
|
} else {
|
|
_beginAngle = math.pi / 2.0;
|
|
_endAngle = _beginAngle! + sweepAngle() * (begin.dx - end.dx).sign;
|
|
}
|
|
}
|
|
assert(_beginAngle != null);
|
|
assert(_endAngle != null);
|
|
} else {
|
|
_beginAngle = null;
|
|
_endAngle = null;
|
|
}
|
|
_dirty = false;
|
|
}
|
|
|
|
/// The center of the circular arc, null if [begin] and [end] are horizontally or
|
|
/// vertically aligned, or if either is null.
|
|
Offset? get center {
|
|
if (begin == null || end == null) {
|
|
return null;
|
|
}
|
|
if (_dirty) {
|
|
_initialize();
|
|
}
|
|
return _center;
|
|
}
|
|
Offset? _center;
|
|
|
|
/// The radius of the circular arc, null if [begin] and [end] are horizontally or
|
|
/// vertically aligned, or if either is null.
|
|
double? get radius {
|
|
if (begin == null || end == null) {
|
|
return null;
|
|
}
|
|
if (_dirty) {
|
|
_initialize();
|
|
}
|
|
return _radius;
|
|
}
|
|
double? _radius;
|
|
|
|
/// The beginning of the arc's sweep in radians, measured from the positive x
|
|
/// axis. Positive angles turn clockwise.
|
|
///
|
|
/// This will be null if [begin] and [end] are horizontally or vertically
|
|
/// aligned, or if either is null.
|
|
double? get beginAngle {
|
|
if (begin == null || end == null) {
|
|
return null;
|
|
}
|
|
if (_dirty) {
|
|
_initialize();
|
|
}
|
|
return _beginAngle;
|
|
}
|
|
double? _beginAngle;
|
|
|
|
/// The end of the arc's sweep in radians, measured from the positive x axis.
|
|
/// Positive angles turn clockwise.
|
|
///
|
|
/// This will be null if [begin] and [end] are horizontally or vertically
|
|
/// aligned, or if either is null.
|
|
double? get endAngle {
|
|
if (begin == null || end == null) {
|
|
return null;
|
|
}
|
|
if (_dirty) {
|
|
_initialize();
|
|
}
|
|
return _beginAngle;
|
|
}
|
|
double? _endAngle;
|
|
|
|
@override
|
|
set begin(Offset? value) {
|
|
if (value != begin) {
|
|
super.begin = value;
|
|
_dirty = true;
|
|
}
|
|
}
|
|
|
|
@override
|
|
set end(Offset? value) {
|
|
if (value != end) {
|
|
super.end = value;
|
|
_dirty = true;
|
|
}
|
|
}
|
|
|
|
@override
|
|
Offset lerp(double t) {
|
|
if (_dirty) {
|
|
_initialize();
|
|
}
|
|
if (t == 0.0) {
|
|
return begin!;
|
|
}
|
|
if (t == 1.0) {
|
|
return end!;
|
|
}
|
|
if (_beginAngle == null || _endAngle == null) {
|
|
return Offset.lerp(begin, end, t)!;
|
|
}
|
|
final double angle = lerpDouble(_beginAngle, _endAngle, t)!;
|
|
final double x = math.cos(angle) * _radius!;
|
|
final double y = math.sin(angle) * _radius!;
|
|
return _center! + Offset(x, y);
|
|
}
|
|
|
|
@override
|
|
String toString() {
|
|
return '${objectRuntimeType(this, 'MaterialPointArcTween')}($begin \u2192 $end; center=$center, radius=$radius, beginAngle=$beginAngle, endAngle=$endAngle)';
|
|
}
|
|
}
|
|
|
|
enum _CornerId {
|
|
topLeft,
|
|
topRight,
|
|
bottomLeft,
|
|
bottomRight
|
|
}
|
|
|
|
class _Diagonal {
|
|
const _Diagonal(this.beginId, this.endId);
|
|
final _CornerId beginId;
|
|
final _CornerId endId;
|
|
}
|
|
|
|
const List<_Diagonal> _allDiagonals = <_Diagonal>[
|
|
_Diagonal(_CornerId.topLeft, _CornerId.bottomRight),
|
|
_Diagonal(_CornerId.bottomRight, _CornerId.topLeft),
|
|
_Diagonal(_CornerId.topRight, _CornerId.bottomLeft),
|
|
_Diagonal(_CornerId.bottomLeft, _CornerId.topRight),
|
|
];
|
|
|
|
typedef _KeyFunc<T> = double Function(T input);
|
|
|
|
// Select the element for which the key function returns the maximum value.
|
|
T _maxBy<T>(Iterable<T> input, _KeyFunc<T> keyFunc) {
|
|
late T maxValue;
|
|
double? maxKey;
|
|
for (final T value in input) {
|
|
final double key = keyFunc(value);
|
|
if (maxKey == null || key > maxKey) {
|
|
maxValue = value;
|
|
maxKey = key;
|
|
}
|
|
}
|
|
return maxValue;
|
|
}
|
|
|
|
/// A [Tween] that interpolates a [Rect] by having its opposite corners follow
|
|
/// circular arcs.
|
|
///
|
|
/// This class specializes the interpolation of [Tween<Rect>] so that instead of
|
|
/// growing or shrinking linearly, opposite corners of the rectangle follow arcs
|
|
/// in a manner consistent with Material Design principles.
|
|
///
|
|
/// Specifically, the rectangle corners whose diagonals are closest to the overall
|
|
/// direction of the animation follow arcs defined with [MaterialPointArcTween].
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [MaterialRectCenterArcTween], which interpolates a rect along a circular
|
|
/// arc between the begin and end [Rect]'s centers.
|
|
/// * [Tween], for a discussion on how to use interpolation objects.
|
|
/// * [MaterialPointArcTween], the analog for [Offset] interpolation.
|
|
/// * [RectTween], which does a linear rectangle interpolation.
|
|
/// * [Hero.createRectTween], which can be used to specify the tween that defines
|
|
/// a hero's path.
|
|
class MaterialRectArcTween extends RectTween {
|
|
/// Creates a [Tween] for animating [Rect]s along a circular arc.
|
|
///
|
|
/// The [begin] and [end] properties must be non-null before the tween is
|
|
/// first used, but the arguments can be null if the values are going to be
|
|
/// filled in later.
|
|
MaterialRectArcTween({
|
|
super.begin,
|
|
super.end,
|
|
});
|
|
|
|
bool _dirty = true;
|
|
|
|
void _initialize() {
|
|
assert(begin != null);
|
|
assert(end != null);
|
|
final Offset centersVector = end!.center - begin!.center;
|
|
final _Diagonal diagonal = _maxBy<_Diagonal>(_allDiagonals, (_Diagonal d) => _diagonalSupport(centersVector, d));
|
|
_beginArc = MaterialPointArcTween(
|
|
begin: _cornerFor(begin!, diagonal.beginId),
|
|
end: _cornerFor(end!, diagonal.beginId),
|
|
);
|
|
_endArc = MaterialPointArcTween(
|
|
begin: _cornerFor(begin!, diagonal.endId),
|
|
end: _cornerFor(end!, diagonal.endId),
|
|
);
|
|
_dirty = false;
|
|
}
|
|
|
|
double _diagonalSupport(Offset centersVector, _Diagonal diagonal) {
|
|
final Offset delta = _cornerFor(begin!, diagonal.endId) - _cornerFor(begin!, diagonal.beginId);
|
|
final double length = delta.distance;
|
|
return centersVector.dx * delta.dx / length + centersVector.dy * delta.dy / length;
|
|
}
|
|
|
|
Offset _cornerFor(Rect rect, _CornerId id) {
|
|
switch (id) {
|
|
case _CornerId.topLeft: return rect.topLeft;
|
|
case _CornerId.topRight: return rect.topRight;
|
|
case _CornerId.bottomLeft: return rect.bottomLeft;
|
|
case _CornerId.bottomRight: return rect.bottomRight;
|
|
}
|
|
}
|
|
|
|
/// The path of the corresponding [begin], [end] rectangle corners that lead
|
|
/// the animation.
|
|
MaterialPointArcTween? get beginArc {
|
|
if (begin == null) {
|
|
return null;
|
|
}
|
|
if (_dirty) {
|
|
_initialize();
|
|
}
|
|
return _beginArc;
|
|
}
|
|
late MaterialPointArcTween _beginArc;
|
|
|
|
/// The path of the corresponding [begin], [end] rectangle corners that trail
|
|
/// the animation.
|
|
MaterialPointArcTween? get endArc {
|
|
if (end == null) {
|
|
return null;
|
|
}
|
|
if (_dirty) {
|
|
_initialize();
|
|
}
|
|
return _endArc;
|
|
}
|
|
late MaterialPointArcTween _endArc;
|
|
|
|
@override
|
|
set begin(Rect? value) {
|
|
if (value != begin) {
|
|
super.begin = value;
|
|
_dirty = true;
|
|
}
|
|
}
|
|
|
|
@override
|
|
set end(Rect? value) {
|
|
if (value != end) {
|
|
super.end = value;
|
|
_dirty = true;
|
|
}
|
|
}
|
|
|
|
@override
|
|
Rect lerp(double t) {
|
|
if (_dirty) {
|
|
_initialize();
|
|
}
|
|
if (t == 0.0) {
|
|
return begin!;
|
|
}
|
|
if (t == 1.0) {
|
|
return end!;
|
|
}
|
|
return Rect.fromPoints(_beginArc.lerp(t), _endArc.lerp(t));
|
|
}
|
|
|
|
@override
|
|
String toString() {
|
|
return '${objectRuntimeType(this, 'MaterialRectArcTween')}($begin \u2192 $end; beginArc=$beginArc, endArc=$endArc)';
|
|
}
|
|
}
|
|
|
|
/// A [Tween] that interpolates a [Rect] by moving it along a circular arc from
|
|
/// [begin]'s [Rect.center] to [end]'s [Rect.center] while interpolating the
|
|
/// rectangle's width and height.
|
|
///
|
|
/// The arc that defines that center of the interpolated rectangle as it morphs
|
|
/// from [begin] to [end] is a [MaterialPointArcTween].
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [MaterialRectArcTween], A [Tween] that interpolates a [Rect] by having
|
|
/// its opposite corners follow circular arcs.
|
|
/// * [Tween], for a discussion on how to use interpolation objects.
|
|
/// * [MaterialPointArcTween], the analog for [Offset] interpolation.
|
|
/// * [RectTween], which does a linear rectangle interpolation.
|
|
/// * [Hero.createRectTween], which can be used to specify the tween that defines
|
|
/// a hero's path.
|
|
class MaterialRectCenterArcTween extends RectTween {
|
|
/// Creates a [Tween] for animating [Rect]s along a circular arc.
|
|
///
|
|
/// The [begin] and [end] properties must be non-null before the tween is
|
|
/// first used, but the arguments can be null if the values are going to be
|
|
/// filled in later.
|
|
MaterialRectCenterArcTween({
|
|
super.begin,
|
|
super.end,
|
|
});
|
|
|
|
bool _dirty = true;
|
|
|
|
void _initialize() {
|
|
assert(begin != null);
|
|
assert(end != null);
|
|
_centerArc = MaterialPointArcTween(
|
|
begin: begin!.center,
|
|
end: end!.center,
|
|
);
|
|
_dirty = false;
|
|
}
|
|
|
|
/// If [begin] and [end] are non-null, returns a tween that interpolates along
|
|
/// a circular arc between [begin]'s [Rect.center] and [end]'s [Rect.center].
|
|
MaterialPointArcTween? get centerArc {
|
|
if (begin == null || end == null) {
|
|
return null;
|
|
}
|
|
if (_dirty) {
|
|
_initialize();
|
|
}
|
|
return _centerArc;
|
|
}
|
|
late MaterialPointArcTween _centerArc;
|
|
|
|
@override
|
|
set begin(Rect? value) {
|
|
if (value != begin) {
|
|
super.begin = value;
|
|
_dirty = true;
|
|
}
|
|
}
|
|
|
|
@override
|
|
set end(Rect? value) {
|
|
if (value != end) {
|
|
super.end = value;
|
|
_dirty = true;
|
|
}
|
|
}
|
|
|
|
@override
|
|
Rect lerp(double t) {
|
|
if (_dirty) {
|
|
_initialize();
|
|
}
|
|
if (t == 0.0) {
|
|
return begin!;
|
|
}
|
|
if (t == 1.0) {
|
|
return end!;
|
|
}
|
|
final Offset center = _centerArc.lerp(t);
|
|
final double width = lerpDouble(begin!.width, end!.width, t)!;
|
|
final double height = lerpDouble(begin!.height, end!.height, t)!;
|
|
return Rect.fromLTWH(center.dx - width / 2.0, center.dy - height / 2.0, width, height);
|
|
}
|
|
|
|
@override
|
|
String toString() {
|
|
return '${objectRuntimeType(this, 'MaterialRectCenterArcTween')}($begin \u2192 $end; centerArc=$centerArc)';
|
|
}
|
|
}
|