| // 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'; |
| import 'package:flutter/painting.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({ |
| Offset begin, |
| Offset end, |
| }) : super(begin: begin, end: end); |
| |
| bool _dirty = true; |
| |
| void _initialize() { |
| assert(begin != null); |
| assert(end != null); |
| |
| // 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) { |
| 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({ |
| Rect begin, |
| Rect end, |
| }) : super(begin: begin, end: 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; |
| } |
| return Offset.zero; |
| } |
| |
| /// 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; |
| } |
| 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; |
| } |
| 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({ |
| Rect begin, |
| Rect end, |
| }) : super(begin: begin, end: 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; |
| } |
| 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)'; |
| } |
| } |