Commit 36eb4a06 authored by Hans Muller's avatar Hans Muller Committed by GitHub

Support for Material arc point and rect transitions (#4938)

parent 4abaf64c
......@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
export 'animation_demo.dart';
export 'buttons_demo.dart';
export 'contacts_demo.dart';
export 'cards_demo.dart';
......
This diff is collapsed.
......@@ -66,6 +66,12 @@ final List<GalleryItem> kAllGalleryItems = <GalleryItem>[
buildRoute: (BuildContext context) => new ContactsDemo()
),
// Components
new GalleryItem(
title: 'Animation',
subtitle: 'Material motion for points and rectangles',
routeName: AnimationDemo.routeName,
buildRoute: (BuildContext context) => new AnimationDemo()
),
new GalleryItem(
title: 'Buttons',
subtitle: 'All kinds: flat, raised, dropdown, icon, etc',
......
......@@ -14,6 +14,7 @@ library material;
export 'src/material/about.dart';
export 'src/material/app.dart';
export 'src/material/app_bar.dart';
export 'src/material/arc.dart';
export 'src/material/bottom_sheet.dart';
export 'src/material/button.dart';
export 'src/material/button_bar.dart';
......
......@@ -7,6 +7,7 @@ import 'dart:io' show Platform;
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'arc.dart';
import 'colors.dart';
import 'overscroll_indicator.dart';
import 'page.dart';
......@@ -152,7 +153,17 @@ final ScrollConfigurationDelegate _indicatorScroll = new _IndicatorScrollConfigu
final ScrollConfigurationDelegate _bounceScroll = new ScrollConfigurationDelegate();
class _MaterialAppState extends State<MaterialApp> {
final HeroController _heroController = new HeroController();
HeroController _heroController;
@override
void initState() {
super.initState();
_heroController = new HeroController(createRectTween: _createRectTween);
}
RectTween _createRectTween(Rect begin, Rect end) {
return new MaterialRectArcTween(begin: begin, end: end);
}
Route<dynamic> _onGenerateRoute(RouteSettings settings) {
WidgetBuilder builder = config.routes[settings.name];
......
// Copyright 2016 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 'dart:ui' show hashValues, lerpDouble;
import 'package:flutter/material.dart';
import 'package:meta/meta.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 animates a point along a circular arc.
///
/// 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:
///
/// * [MaterialRectArcTween]
class MaterialPointArcTween extends Tween<Point> {
MaterialPointArcTween({
@required Point begin,
@required Point end
}) : super(begin: begin, end: 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 Point c = new Point(end.x, begin.y);
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 = new Point(end.x + _radius * (begin.x - end.x).sign, end.y);
if (begin.x < end.x) {
_beginAngle = sweepAngle() * (begin.y - end.y).sign;
_endAngle = 0.0;
} else {
_beginAngle = math.PI + sweepAngle() * (end.y - begin.y).sign;
_endAngle = math.PI;
}
} else {
_radius = distanceFromAtoB * distanceFromAtoB / (c - end).distance / 2.0;
_center = new Point(begin.x, begin.y + (end.y - begin.y).sign * _radius);
if (begin.y < end.y) {
_beginAngle = -math.PI / 2.0;
_endAngle = _beginAngle + sweepAngle() * (end.x - begin.x).sign;
} else {
_beginAngle = math.PI / 2.0;
_endAngle = _beginAngle + sweepAngle() * (begin.x - end.x).sign;
}
}
}
}
Point _center;
double _radius;
double _beginAngle;
double _endAngle;
/// The center of the circular arc, null if [begin] and [end] are horiztonally or
/// vertically aligned.
Point get center => _center;
/// The radius of the circular arc, null if begin and end are horiztonally or
/// vertically aligned.
double get radius => _radius;
/// The beginning of the arc's sweep in radians, measured from the positive X axis.
/// Positive angles turn clockwise. Null if begin and end are horiztonally or
/// vertically aligned.
double get beginAngle => _beginAngle;
/// The end of the arc's sweep in radians, measured from the positive X axis.
/// Positive angles turn clockwise.
double get endAngle => _beginAngle;
/// Setting the arc's [begin] parameter is not supported. Construct a new arc instead.
@override
set begin(Point value) {
assert(false); // not supported
}
/// Setting the arc's [end] parameter is not supported. Construct a new arc instead.
@override
set end(Point value) {
assert(false); // not supported
}
@override
Point lerp(double t) {
if (t == 0.0)
return begin;
if (t == 1.0)
return end;
if (_beginAngle == null || _endAngle == null)
return Point.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 + new Offset(x, y);
}
@override
bool operator ==(dynamic other) {
if (identical(this, other))
return true;
if (other is! MaterialPointArcTween)
return false;
final MaterialPointArcTween typedOther = other;
return begin == typedOther.begin
&& end == typedOther.end;
}
@override
int get hashCode => hashValues(begin, end);
@override
String toString() {
return '$runtimeType($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 = const <_Diagonal>[
const _Diagonal(_CornerId.topLeft, _CornerId.bottomRight),
const _Diagonal(_CornerId.bottomRight, _CornerId.topLeft),
const _Diagonal(_CornerId.topRight, _CornerId.bottomLeft),
const _Diagonal(_CornerId.bottomLeft, _CornerId.topRight),
];
/// A Tween that animates a rectangle from [begin] to [end].
///
/// The rectangle corners whose diagonal is closest to the overall direction of
/// the animation follow arcs defined with [MaterialPointArcTween].
///
/// See also:
///
/// * [RectTween] (linear rectangle interpolation)
/// * [MaterialPointArcTween]
class MaterialRectArcTween extends RectTween {
MaterialRectArcTween({
@required Rect begin,
@required Rect end
}) : super(begin: begin, end: end) {
final Offset centersVector = end.center - begin.center;
double maxSupport = 0.0;
for (_Diagonal diagonal in _allDiagonals) {
final double support = _diagonalSupport(centersVector, diagonal);
if (support > maxSupport) {
_diagonal = diagonal;
maxSupport = support;
}
}
_beginArc = new MaterialPointArcTween(
begin: _cornerFor(begin, _diagonal.beginId),
end: _cornerFor(end, _diagonal.beginId)
);
_endArc = new MaterialPointArcTween(
begin: _cornerFor(begin, _diagonal.endId),
end: _cornerFor(end, _diagonal.endId)
);
}
_Diagonal _diagonal;
MaterialPointArcTween _beginArc;
MaterialPointArcTween _endArc;
Point _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 Point.origin;
}
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;
}
/// The path of the corresponding [begin], [end] rectangle corners that lead
/// the animation.
MaterialPointArcTween get beginArc => _beginArc;
/// The path of the corresponding [begin], [end] rectangle corners that trail
/// the animation.
MaterialPointArcTween get endArc => _endArc;
/// Setting the arc's [begin] parameter is not supported. Construct a new arc instead.
@override
set begin(Rect value) {
assert(false); // not supported
}
/// Setting the arc's [end] parameter is not supported. Construct a new arc instead.
@override
set end(Rect value) {
assert(false); // not supported
}
@override
Rect lerp(double t) {
if (t == 0.0)
return begin;
if (t == 1.0)
return end;
return new Rect.fromPoints(_beginArc.lerp(t), _endArc.lerp(t));
}
@override
bool operator ==(dynamic other) {
if (identical(this, other))
return true;
if (other is! MaterialRectArcTween)
return false;
final MaterialRectArcTween typedOther = other;
return begin == typedOther.begin
&& end == typedOther.end;
}
@override
int get hashCode => hashValues(begin, end);
@override
String toString() {
return '$runtimeType($begin \u2192 $end beginArc=$beginArc, endArc=$endArc)';
}
}
......@@ -61,7 +61,7 @@ class MaterialPageRoute<T> extends PageRoute<T> {
final WidgetBuilder builder;
@override
Duration get transitionDuration => const Duration(milliseconds: 150);
Duration get transitionDuration => const Duration(milliseconds: 300);
@override
Color get barrierColor => null;
......
......@@ -66,13 +66,13 @@ class _HeroManifest {
final GlobalKey key;
final Widget config;
final Set<HeroState> sourceStates;
final RelativeRect currentRect;
final Rect currentRect;
final double currentTurns;
}
abstract class HeroHandle {
bool get alwaysAnimate;
_HeroManifest _takeChild(Rect animationArea, Animation<double> currentAnimation);
_HeroManifest _takeChild(Animation<double> currentAnimation);
}
class Hero extends StatefulWidget {
......@@ -161,7 +161,7 @@ class HeroState extends State<Hero> implements HeroHandle {
bool get alwaysAnimate => config.alwaysAnimate;
@override
_HeroManifest _takeChild(Rect animationArea, Animation<double> currentAnimation) {
_HeroManifest _takeChild(Animation<double> currentAnimation) {
assert(mounted);
final RenderBox renderObject = context.findRenderObject();
assert(renderObject != null);
......@@ -175,12 +175,11 @@ class HeroState extends State<Hero> implements HeroHandle {
final Point heroTopLeft = renderObject.localToGlobal(Point.origin);
final Point heroBottomRight = renderObject.localToGlobal(renderObject.size.bottomRight(Point.origin));
final Rect heroArea = new Rect.fromLTRB(heroTopLeft.x, heroTopLeft.y, heroBottomRight.x, heroBottomRight.y);
final RelativeRect startRect = new RelativeRect.fromRect(heroArea, animationArea);
_HeroManifest result = new _HeroManifest(
key: _key, // might be null, e.g. if the hero is returning to us
config: config,
sourceStates: new HashSet<HeroState>.from(<HeroState>[this]),
currentRect: startRect,
currentRect: heroArea,
currentTurns: config.turns.toDouble()
);
if (_key != null)
......@@ -224,6 +223,7 @@ class _HeroQuestState implements HeroHandle {
this.key,
this.child,
this.sourceStates,
this.animationArea,
this.targetRect,
this.targetTurns,
this.targetState,
......@@ -237,10 +237,11 @@ class _HeroQuestState implements HeroHandle {
final GlobalKey key;
final Widget child;
final Set<HeroState> sourceStates;
final RelativeRect targetRect;
final Rect animationArea;
final Rect targetRect;
final int targetTurns;
final HeroState targetState;
final RelativeRectTween currentRect;
final RectTween currentRect;
final Tween<double> currentTurns;
@override
......@@ -250,7 +251,7 @@ class _HeroQuestState implements HeroHandle {
bool _taken = false;
@override
_HeroManifest _takeChild(Rect animationArea, Animation<double> currentAnimation) {
_HeroManifest _takeChild(Animation<double> currentAnimation) {
assert(!taken);
_taken = true;
Set<HeroState> states = sourceStates;
......@@ -266,8 +267,9 @@ class _HeroQuestState implements HeroHandle {
}
Widget build(BuildContext context, Animation<double> animation) {
return new PositionedTransition(
return new RelativePositionedTransition(
rect: currentRect.animate(animation),
size: animationArea.size,
child: new RotationTransition(
turns: currentTurns.animate(animation),
child: new KeyedSubtree(
......@@ -286,10 +288,13 @@ class _HeroMatch {
final Object tag;
}
typedef RectTween CreateRectTween(Rect begin, Rect end);
class HeroParty {
HeroParty({ this.onQuestFinished });
HeroParty({ this.onQuestFinished, this.createRectTween });
final VoidCallback onQuestFinished;
final CreateRectTween createRectTween;
List<_HeroQuestState> _heroes = <_HeroQuestState>[];
bool get isEmpty => _heroes.isEmpty;
......@@ -302,8 +307,10 @@ class HeroParty {
return result;
}
RelativeRectTween createRectTween(RelativeRect begin, RelativeRect end) {
return new RelativeRectTween(begin: begin, end: end);
RectTween _doCreateRectTween(Rect begin, Rect end) {
if (createRectTween != null)
return createRectTween(begin, end);
return new RectTween(begin: begin, end: end);
}
Tween<double> createTurnsTween(double begin, double end) {
......@@ -331,30 +338,29 @@ class HeroParty {
if ((heroPair.from == null && !heroPair.to.alwaysAnimate) ||
(heroPair.to == null && !heroPair.from.alwaysAnimate))
continue;
_HeroManifest from = heroPair.from?._takeChild(animationArea, _currentAnimation);
_HeroManifest from = heroPair.from?._takeChild(_currentAnimation);
assert(heroPair.to == null || heroPair.to is HeroState);
_HeroManifest to = heroPair.to?._takeChild(animationArea, _currentAnimation);
_HeroManifest to = heroPair.to?._takeChild(_currentAnimation);
assert(from != null || to != null);
assert(to == null || to.sourceStates.length == 1);
assert(to == null || to.currentTurns.floor() == to.currentTurns);
HeroState targetState = to != null ? to.sourceStates.elementAt(0) : null;
Set<HeroState> sourceStates = from != null ? from.sourceStates : new HashSet<HeroState>();
sourceStates.remove(targetState);
RelativeRect sourceRect = from != null ? from.currentRect :
new RelativeRect.fromRect(to.currentRect.toRect(animationArea).center & Size.zero, animationArea);
RelativeRect targetRect = to != null ? to.currentRect :
new RelativeRect.fromRect(from.currentRect.toRect(animationArea).center & Size.zero, animationArea);
double sourceTurns = from != null ? from.currentTurns : 0.0;
double targetTurns = to != null ? to.currentTurns : 0.0;
Rect sourceRect = from?.currentRect ?? to.currentRect.center & Size.zero;
Rect targetRect = to?.currentRect ?? from.currentRect.center & Size.zero;
double sourceTurns = from?.currentTurns ?? 0.0;
double targetTurns = to?.currentTurns ?? 0.0;
_newHeroes.add(new _HeroQuestState(
tag: heroPair.tag,
key: from != null ? from.key : to.key,
child: to != null ? to.config : from.config,
key: from?.key ?? to.key,
child: to?.config ?? from.config,
sourceStates: sourceStates,
animationArea: animationArea,
targetRect: targetRect,
targetTurns: targetTurns.floor(),
targetState: targetState,
currentRect: createRectTween(sourceRect, targetRect),
currentRect: _doCreateRectTween(sourceRect, targetRect),
currentTurns: createTurnsTween(sourceTurns, targetTurns)
));
}
......@@ -400,8 +406,11 @@ class HeroParty {
}
class HeroController extends NavigatorObserver {
HeroController() {
_party = new HeroParty(onQuestFinished: _handleQuestFinished);
HeroController({ CreateRectTween createRectTween }) {
_party = new HeroParty(
onQuestFinished: _handleQuestFinished,
createRectTween: createRectTween
);
}
HeroParty _party;
......
......@@ -292,6 +292,10 @@ class RelativeRectTween extends Tween<RelativeRect> {
/// position to and end position over the lifetime of the animation.
///
/// Only works if it's the child of a [Stack].
///
/// See also:
///
/// * [RelativePositionedTransition]
class PositionedTransition extends AnimatedWidget {
/// Creates a transition for [Positioned].
///
......@@ -320,6 +324,46 @@ class PositionedTransition extends AnimatedWidget {
}
}
/// Animated version of [Positioned] which transitions the child's position
/// based on the value of [rect] relative to a bounding box with the
/// specified [size].
///
/// Only works if it's the child of a [Stack].
///
/// See also:
///
/// * [PositionedTransition]
class RelativePositionedTransition extends AnimatedWidget {
RelativePositionedTransition({
Key key,
@required Animation<Rect> rect,
@required this.size,
this.child
}) : super(key: key, animation: rect);
/// The animation that controls the child's size and position.
Animation<Rect> get rect => animation;
/// The [Positioned] widget's offsets are relative to a box of this
/// size whose origin is 0,0.
final Size size;
/// The widget below this widget in the tree.
final Widget child;
@override
Widget build(BuildContext context) {
final RelativeRect offsets = new RelativeRect.fromSize(rect.value, size);
return new Positioned(
top: offsets.top,
right: offsets.right,
bottom: offsets.bottom,
left: offsets.left,
child: child
);
}
}
/// A builder that builds a widget given a child.
typedef Widget TransitionBuilder(BuildContext context, Widget child);
......
// Copyright 2016 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_test/flutter_test.dart';
import 'package:flutter/material.dart';
void main() {
test('on-axis MaterialPointArcTween', () {
MaterialPointArcTween tween = new MaterialPointArcTween(
begin: Point.origin,
end: new Point(0.0, 10.0)
);
expect(tween.lerp(0.5), equals(new Point(0.0, 5.0)));
expect(tween, hasOneLineDescription);
tween = new MaterialPointArcTween(
begin: Point.origin,
end: new Point(10.0, 0.0)
);
expect(tween.lerp(0.5), equals(new Point(5.0, 0.0)));
});
test('on-axis MaterialRectArcTween', () {
MaterialRectArcTween tween = new MaterialRectArcTween(
begin: new Rect.fromLTWH(0.0, 0.0, 10.0, 10.0),
end: new Rect.fromLTWH(0.0, 10.0, 10.0, 10.0)
);
expect(tween.lerp(0.5), equals(new Rect.fromLTWH(0.0, 5.0, 10.0, 10.0)));
expect(tween, hasOneLineDescription);
tween = new MaterialRectArcTween(
begin: new Rect.fromLTWH(0.0, 0.0, 10.0, 10.0),
end: new Rect.fromLTWH(10.0, 0.0, 10.0, 10.0)
);
expect(tween.lerp(0.5), equals(new Rect.fromLTWH(5.0, 0.0, 10.0, 10.0)));
});
test('MaterialPointArcTween', () {
final Point begin = const Point(180.0, 110.0);
final Point end = const Point(37.0, 250.0);
MaterialPointArcTween tween = new MaterialPointArcTween(begin: begin, end: end);
expect(tween.lerp(0.0), begin);
expect((tween.lerp(0.25) - const Point(126.0, 120.0)).distance, closeTo(0.0, 2.0));
expect((tween.lerp(0.75) - const Point(48.0, 196.0)).distance, closeTo(0.0, 2.0));
expect(tween.lerp(1.0), end);
tween = new MaterialPointArcTween(begin: end, end: begin);
expect(tween.lerp(0.0), end);
expect((tween.lerp(0.25) - const Point(91.0, 239.0)).distance, closeTo(0.0, 2.0));
expect((tween.lerp(0.75) - const Point(168.3, 163.8)).distance, closeTo(0.0, 2.0));
expect(tween.lerp(1.0), begin);
});
test('MaterialRectArcTween', () {
final Rect begin = new Rect.fromLTRB(180.0, 100.0, 330.0, 200.0);
final Rect end = new Rect.fromLTRB(32.0, 275.0, 132.0, 425.0);
bool sameRect(Rect a, Rect b) {
return (a.left - b.left).abs() < 2.0
&& (a.top - b.top).abs() < 2.0
&& (a.right - b.right).abs() < 2.0
&& (a.bottom - b.bottom).abs() < 2.0;
}
MaterialRectArcTween tween = new MaterialRectArcTween(begin: begin, end: end);
expect(tween.lerp(0.0), begin);
expect(sameRect(tween.lerp(0.25), new Rect.fromLTRB(120.0, 113.0, 259.0, 237.0)), isTrue);
expect(sameRect(tween.lerp(0.75), new Rect.fromLTRB(42.3, 206.5, 153.5, 354.7)), isTrue);
expect(tween.lerp(1.0), end);
tween = new MaterialRectArcTween(begin: end, end: begin);
expect(tween.lerp(0.0), end);
expect(sameRect(tween.lerp(0.25), new Rect.fromLTRB(92.0, 262.0, 203.0, 388.0)), isTrue);
expect(sameRect(tween.lerp(0.75), new Rect.fromLTRB(169.7, 168.5, 308.5, 270.3)), isTrue);
expect(tween.lerp(1.0), begin);
});
}
......@@ -79,12 +79,16 @@ class ServiceProtocolDevFSOperations implements DevFSOperations {
return e;
}
String fileContents = BASE64.encode(bytes);
try {
return await serviceProtocol.sendRequest('_writeDevFSFile',
<String, dynamic> {
'fsName': fsName,
'path': entry.devicePath,
'fileContents': fileContents
});
} catch (e) {
print('failed on ${entry.devicePath} $e');
}
}
@override
......
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