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';
......
// 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:async';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
enum _DragTarget {
start,
end
}
// How close a drag's start position must be to the target point. This is
// a distance squared.
const double _kTargetSlop = 2500.0;
// Used by the Painter classes.
const double _kPointRadius = 6.0;
class _DragHandler extends Drag {
_DragHandler(this.onUpdate, this.onCancel, this.onEnd);
final GestureDragUpdateCallback onUpdate;
final GestureDragCancelCallback onCancel;
final GestureDragEndCallback onEnd;
@override
void update(DragUpdateDetails details) {
onUpdate(details);
}
@override
void cancel() {
onCancel();
}
@override
void end(DragEndDetails details) {
onEnd(details);
}
}
class _IgnoreDrag extends Drag {
}
class _PointDemoPainter extends CustomPainter {
_PointDemoPainter({
Animation<double> repaint,
this.arc
}) : _repaint = repaint, super(repaint: repaint);
final MaterialPointArcTween arc;
Animation<double> _repaint;
void drawPoint(Canvas canvas, Point point, Color color) {
final Paint paint = new Paint()
..color = color.withOpacity(0.25)
..style = PaintingStyle.fill;
canvas.drawCircle(point, _kPointRadius, paint);
paint
..color = color
..style = PaintingStyle.stroke
..strokeWidth = 2.0;
canvas.drawCircle(point, _kPointRadius + 1.0, paint);
}
@override
void paint(Canvas canvas, Size size) {
final Paint paint = new Paint();
if (arc.center != null)
drawPoint(canvas, arc.center, Colors.blue[400]);
paint
..color = Colors.green[500].withOpacity(0.25)
..strokeWidth = 4.0
..style = PaintingStyle.stroke;
if (arc.center != null && arc.radius != null)
canvas.drawCircle(arc.center, arc.radius, paint);
else
canvas.drawLine(arc.begin, arc.end, paint);
drawPoint(canvas, arc.begin, Colors.green[500]);
drawPoint(canvas, arc.end, Colors.red[500]);
paint
..color = Colors.green[500]
..style = PaintingStyle.fill;
canvas.drawCircle(arc.lerp(_repaint.value), _kPointRadius, paint);
}
@override
bool hitTest(Point position) {
return (arc.begin - position).distanceSquared < _kTargetSlop
|| (arc.end - position).distanceSquared < _kTargetSlop;
}
@override
bool shouldRepaint(_PointDemoPainter oldPainter) => arc != oldPainter.arc;
}
class _PointDemo extends StatefulWidget {
_PointDemo({ Key key, this.controller }) : super(key: key);
final AnimationController controller;
@override
_PointDemoState createState() => new _PointDemoState();
}
class _PointDemoState extends State<_PointDemo> {
final GlobalKey _painterKey = new GlobalKey();
CurvedAnimation _animation;
_DragTarget _dragTarget;
Point _begin = const Point(180.0, 110.0);
Point _end = const Point(37.0, 250.0);
@override
void initState() {
super.initState();
_animation = new CurvedAnimation(parent: config.controller, curve: Curves.ease);
}
@override
void dispose() {
config.controller.value = 0.0;
super.dispose();
}
Drag _handleOnStart(Point position) {
// TODO(hansmuller): allow the user to drag both points at the same time.
if (_dragTarget != null)
return new _IgnoreDrag();
final RenderBox box = _painterKey.currentContext.findRenderObject();
final double startOffset = (box.localToGlobal(_begin) - position).distanceSquared;
final double endOffset = (box.localToGlobal(_end) - position).distanceSquared;
setState(() {
if (startOffset < endOffset && startOffset < _kTargetSlop)
_dragTarget = _DragTarget.start;
else if (endOffset < _kTargetSlop)
_dragTarget = _DragTarget.end;
else
_dragTarget = null;
});
return new _DragHandler(_handleDragUpdate, _handleDragCancel, _handleDragEnd);
}
void _handleDragUpdate(DragUpdateDetails details) {
switch (_dragTarget) {
case _DragTarget.start:
setState(() {
_begin = _begin + details.delta;
});
break;
case _DragTarget.end:
setState(() {
_end = _end + details.delta;
});
break;
}
}
void _handleDragCancel() {
_dragTarget = null;
config.controller.value = 0.0;
}
void _handleDragEnd(DragEndDetails details) {
_dragTarget = null;
}
@override
Widget build(BuildContext context) {
final MaterialPointArcTween arc = new MaterialPointArcTween(begin: _begin, end: _end);
return new RawGestureDetector(
behavior: _dragTarget == null ? HitTestBehavior.deferToChild : HitTestBehavior.opaque,
gestures: <Type, GestureRecognizerFactory>{
ImmediateMultiDragGestureRecognizer: (ImmediateMultiDragGestureRecognizer recognizer) {
return (recognizer ??= new ImmediateMultiDragGestureRecognizer())
..onStart = _handleOnStart;
}
},
child: new ClipRect(
child: new CustomPaint(
key: _painterKey,
foregroundPainter: new _PointDemoPainter(
repaint: _animation,
arc: arc
),
// Watch out: if this IgnorePointer is left out, then gestures that
// fail _PointDemoPainter.hitTest() will still be recognized because
// they do overlap this child, which is as big as the CustomPaint.
child: new IgnorePointer(
child: new Padding(
padding: const EdgeInsets.all(16.0),
child: new Text(
"Tap the refresh button to run the animation. Drag the green "
"and red points to change the animation's path.",
style: Theme.of(context).textTheme.caption.copyWith(fontSize: 16.0)
)
)
)
)
)
);
}
}
class _RectangleDemoPainter extends CustomPainter {
_RectangleDemoPainter({
Animation<double> repaint,
this.arc
}) : _repaint = repaint, super(repaint: repaint);
final MaterialRectArcTween arc;
Animation<double> _repaint;
void drawPoint(Canvas canvas, Point p, Color color) {
final Paint paint = new Paint()
..color = color.withOpacity(0.25)
..style = PaintingStyle.fill;
canvas.drawCircle(p, _kPointRadius, paint);
paint
..color = color
..style = PaintingStyle.stroke
..strokeWidth = 2.0;
canvas.drawCircle(p, _kPointRadius + 1.0, paint);
}
void drawRect(Canvas canvas, Rect rect, Color color) {
final Paint paint = new Paint()
..color = color.withOpacity(0.25)
..strokeWidth = 4.0
..style = PaintingStyle.stroke;
canvas.drawRect(rect, paint);
drawPoint(canvas, rect.center, color);
}
@override
void paint(Canvas canvas, Size size) {
drawRect(canvas, arc.begin, Colors.green[500]);
drawRect(canvas, arc.end, Colors.red[500]);
drawRect(canvas, arc.lerp(_repaint.value), Colors.blue[500]);
}
@override
bool hitTest(Point position) {
return (arc.begin.center - position).distanceSquared < _kTargetSlop
|| (arc.end.center - position).distanceSquared < _kTargetSlop;
}
@override
bool shouldRepaint(_RectangleDemoPainter oldPainter) => arc != oldPainter.arc;
}
class _RectangleDemo extends StatefulWidget {
_RectangleDemo({ Key key, this.controller }) : super(key: key);
final AnimationController controller;
@override
_RectangleDemoState createState() => new _RectangleDemoState();
}
class _RectangleDemoState extends State<_RectangleDemo> {
final GlobalKey _painterKey = new GlobalKey();
CurvedAnimation _animation;
_DragTarget _dragTarget;
Rect _begin = new Rect.fromLTRB(180.0, 100.0, 330.0, 200.0);
Rect _end = new Rect.fromLTRB(32.0, 275.0, 132.0, 425.0);
@override
void initState() {
super.initState();
_animation = new CurvedAnimation(parent: config.controller, curve: Curves.ease);
}
@override
void dispose() {
config.controller.value = 0.0;
super.dispose();
}
Drag _handleOnStart(Point position) {
// TODO(hansmuller): allow the user to drag both points at the same time.
if (_dragTarget != null)
return new _IgnoreDrag();
final RenderBox box = _painterKey.currentContext.findRenderObject();
final double startOffset = (box.localToGlobal(_begin.center) - position).distanceSquared;
final double endOffset = (box.localToGlobal(_end.center) - position).distanceSquared;
setState(() {
if (startOffset < endOffset && startOffset < _kTargetSlop)
_dragTarget = _DragTarget.start;
else if (endOffset < _kTargetSlop)
_dragTarget = _DragTarget.end;
else
_dragTarget = null;
});
return new _DragHandler(_handleDragUpdate, _handleDragCancel, _handleDragEnd);
}
void _handleDragUpdate(DragUpdateDetails details) {
switch (_dragTarget) {
case _DragTarget.start:
setState(() {
_begin = _begin.shift(details.delta);
});
break;
case _DragTarget.end:
setState(() {
_end = _end.shift(details.delta);
});
break;
}
}
void _handleDragCancel() {
_dragTarget = null;
config.controller.value = 0.0;
}
void _handleDragEnd(DragEndDetails details) {
_dragTarget = null;
}
@override
Widget build(BuildContext context) {
final MaterialRectArcTween arc = new MaterialRectArcTween(begin: _begin, end: _end);
return new RawGestureDetector(
behavior: _dragTarget == null ? HitTestBehavior.deferToChild : HitTestBehavior.opaque,
gestures: <Type, GestureRecognizerFactory>{
ImmediateMultiDragGestureRecognizer: (ImmediateMultiDragGestureRecognizer recognizer) {
return (recognizer ??= new ImmediateMultiDragGestureRecognizer())
..onStart = _handleOnStart;
}
},
child: new ClipRect(
child: new CustomPaint(
key: _painterKey,
foregroundPainter: new _RectangleDemoPainter(
repaint: _animation,
arc: arc
),
// Watch out: if this IgnorePointer is left out, then gestures that
// fail _RectDemoPainter.hitTest() will still be recognized because
// they do overlap this child, which is as big as the CustomPaint.
child: new IgnorePointer(
child: new Padding(
padding: const EdgeInsets.all(16.0),
child: new Text(
"Tap the refresh button to run the animation. Drag the rectangles "
"to change the animation's path.",
style: Theme.of(context).textTheme.caption.copyWith(fontSize: 16.0)
)
)
)
)
)
);
}
}
typedef Widget _DemoBuilder(_ArcDemo demo);
class _ArcDemo {
_ArcDemo(String _title, this.builder) : title = _title, key = new GlobalKey(debugLabel: _title);
final AnimationController controller = new AnimationController(duration: const Duration(milliseconds: 500));
final String title;
final _DemoBuilder builder;
final GlobalKey key;
}
class AnimationDemo extends StatefulWidget {
AnimationDemo({ Key key }) : super(key: key);
static const String routeName = '/animation';
@override
_AnimationDemoState createState() => new _AnimationDemoState();
}
class _AnimationDemoState extends State<AnimationDemo> {
static final GlobalKey<TabBarSelectionState<_ArcDemo>> _tabsKey = new GlobalKey<TabBarSelectionState<_ArcDemo>>();
static final List<_ArcDemo> _allDemos = <_ArcDemo>[
new _ArcDemo('POINT', (_ArcDemo demo) {
return new _PointDemo(
key: demo.key,
controller: demo.controller
);
}),
new _ArcDemo('RECTANGLE', (_ArcDemo demo) {
return new _RectangleDemo(
key: demo.key,
controller: demo.controller
);
})
];
Future<Null> _play() async {
_ArcDemo demo = _tabsKey.currentState.value;
await demo.controller.forward();
if (demo.key.currentState != null && demo.key.currentState.mounted)
demo.controller.reverse();
}
@override
Widget build(BuildContext context) {
return new TabBarSelection<_ArcDemo>(
key: _tabsKey,
values: _allDemos,
child: new Scaffold(
appBar: new AppBar(
title: new Text('Animation'),
bottom: new TabBar<_ArcDemo>(
labels: new Map<_ArcDemo, TabLabel>.fromIterable(_allDemos, value: (_ArcDemo demo) {
return new TabLabel(text: demo.title);
})
)
),
floatingActionButton: new FloatingActionButton(
onPressed: _play,
child: new Icon(Icons.refresh)
),
body: new TabBarView<_ArcDemo>(
children: _allDemos.map((_ArcDemo demo) => demo.builder(demo)).toList()
)
)
);
}
}
......@@ -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