From 36eb4a066f41004fa4eb99f41f074307eea2852e Mon Sep 17 00:00:00 2001 From: Hans Muller Date: Thu, 21 Jul 2016 10:48:41 -0700 Subject: [PATCH] Support for Material arc point and rect transitions (#4938) --- examples/flutter_gallery/lib/demo/all.dart | 1 + .../lib/demo/animation_demo.dart | 439 ++++++++++++++++++ .../flutter_gallery/lib/gallery/item.dart | 6 + packages/flutter/lib/material.dart | 1 + packages/flutter/lib/src/material/app.dart | 13 +- packages/flutter/lib/src/material/arc.dart | 253 ++++++++++ packages/flutter/lib/src/material/page.dart | 2 +- packages/flutter/lib/src/widgets/heroes.dart | 59 ++- .../flutter/lib/src/widgets/transitions.dart | 44 ++ packages/flutter/test/material/arc_test.dart | 80 ++++ packages/flutter_tools/lib/src/devfs.dart | 4 + 11 files changed, 875 insertions(+), 27 deletions(-) create mode 100644 examples/flutter_gallery/lib/demo/animation_demo.dart create mode 100644 packages/flutter/lib/src/material/arc.dart create mode 100644 packages/flutter/test/material/arc_test.dart diff --git a/examples/flutter_gallery/lib/demo/all.dart b/examples/flutter_gallery/lib/demo/all.dart index a8cd463374e..fd301169739 100644 --- a/examples/flutter_gallery/lib/demo/all.dart +++ b/examples/flutter_gallery/lib/demo/all.dart @@ -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'; diff --git a/examples/flutter_gallery/lib/demo/animation_demo.dart b/examples/flutter_gallery/lib/demo/animation_demo.dart new file mode 100644 index 00000000000..ce95382b59f --- /dev/null +++ b/examples/flutter_gallery/lib/demo/animation_demo.dart @@ -0,0 +1,439 @@ +// 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 repaint, + this.arc + }) : _repaint = repaint, super(repaint: repaint); + + final MaterialPointArcTween arc; + Animation _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: { + 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 repaint, + this.arc + }) : _repaint = repaint, super(repaint: repaint); + + final MaterialRectArcTween arc; + Animation _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: { + 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 { + static final GlobalKey> _tabsKey = new GlobalKey>(); + + 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 _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() + ) + ) + ); + } +} diff --git a/examples/flutter_gallery/lib/gallery/item.dart b/examples/flutter_gallery/lib/gallery/item.dart index ab65da30348..1458c267120 100644 --- a/examples/flutter_gallery/lib/gallery/item.dart +++ b/examples/flutter_gallery/lib/gallery/item.dart @@ -66,6 +66,12 @@ final List kAllGalleryItems = [ 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', diff --git a/packages/flutter/lib/material.dart b/packages/flutter/lib/material.dart index d1d357b934a..f69ffd00a1f 100644 --- a/packages/flutter/lib/material.dart +++ b/packages/flutter/lib/material.dart @@ -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'; diff --git a/packages/flutter/lib/src/material/app.dart b/packages/flutter/lib/src/material/app.dart index 93486369c13..83b215a1005 100644 --- a/packages/flutter/lib/src/material/app.dart +++ b/packages/flutter/lib/src/material/app.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 { - 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 _onGenerateRoute(RouteSettings settings) { WidgetBuilder builder = config.routes[settings.name]; diff --git a/packages/flutter/lib/src/material/arc.dart b/packages/flutter/lib/src/material/arc.dart new file mode 100644 index 00000000000..02c865b6cb2 --- /dev/null +++ b/packages/flutter/lib/src/material/arc.dart @@ -0,0 +1,253 @@ +// 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 { + 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)'; + } +} diff --git a/packages/flutter/lib/src/material/page.dart b/packages/flutter/lib/src/material/page.dart index bf69278dfa9..6754c5062e7 100644 --- a/packages/flutter/lib/src/material/page.dart +++ b/packages/flutter/lib/src/material/page.dart @@ -61,7 +61,7 @@ class MaterialPageRoute extends PageRoute { final WidgetBuilder builder; @override - Duration get transitionDuration => const Duration(milliseconds: 150); + Duration get transitionDuration => const Duration(milliseconds: 300); @override Color get barrierColor => null; diff --git a/packages/flutter/lib/src/widgets/heroes.dart b/packages/flutter/lib/src/widgets/heroes.dart index 3aaa766ec13..a64c7f835d7 100644 --- a/packages/flutter/lib/src/widgets/heroes.dart +++ b/packages/flutter/lib/src/widgets/heroes.dart @@ -66,13 +66,13 @@ class _HeroManifest { final GlobalKey key; final Widget config; final Set sourceStates; - final RelativeRect currentRect; + final Rect currentRect; final double currentTurns; } abstract class HeroHandle { bool get alwaysAnimate; - _HeroManifest _takeChild(Rect animationArea, Animation currentAnimation); + _HeroManifest _takeChild(Animation currentAnimation); } class Hero extends StatefulWidget { @@ -161,7 +161,7 @@ class HeroState extends State implements HeroHandle { bool get alwaysAnimate => config.alwaysAnimate; @override - _HeroManifest _takeChild(Rect animationArea, Animation currentAnimation) { + _HeroManifest _takeChild(Animation currentAnimation) { assert(mounted); final RenderBox renderObject = context.findRenderObject(); assert(renderObject != null); @@ -175,12 +175,11 @@ class HeroState extends State 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.from([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 sourceStates; - final RelativeRect targetRect; + final Rect animationArea; + final Rect targetRect; final int targetTurns; final HeroState targetState; - final RelativeRectTween currentRect; + final RectTween currentRect; final Tween currentTurns; @override @@ -250,7 +251,7 @@ class _HeroQuestState implements HeroHandle { bool _taken = false; @override - _HeroManifest _takeChild(Rect animationArea, Animation currentAnimation) { + _HeroManifest _takeChild(Animation currentAnimation) { assert(!taken); _taken = true; Set states = sourceStates; @@ -266,8 +267,9 @@ class _HeroQuestState implements HeroHandle { } Widget build(BuildContext context, Animation 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 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 sourceStates = from != null ? from.sourceStates : new HashSet(); 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; diff --git a/packages/flutter/lib/src/widgets/transitions.dart b/packages/flutter/lib/src/widgets/transitions.dart index 76755184340..345865efa7f 100644 --- a/packages/flutter/lib/src/widgets/transitions.dart +++ b/packages/flutter/lib/src/widgets/transitions.dart @@ -292,6 +292,10 @@ class RelativeRectTween extends Tween { /// 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, + @required this.size, + this.child + }) : super(key: key, animation: rect); + + /// The animation that controls the child's size and position. + Animation 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); diff --git a/packages/flutter/test/material/arc_test.dart b/packages/flutter/test/material/arc_test.dart new file mode 100644 index 00000000000..abfc0a1b4f2 --- /dev/null +++ b/packages/flutter/test/material/arc_test.dart @@ -0,0 +1,80 @@ +// 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); + }); + +} diff --git a/packages/flutter_tools/lib/src/devfs.dart b/packages/flutter_tools/lib/src/devfs.dart index 09b7f8eaa43..7aab766c5c6 100644 --- a/packages/flutter_tools/lib/src/devfs.dart +++ b/packages/flutter_tools/lib/src/devfs.dart @@ -79,12 +79,16 @@ class ServiceProtocolDevFSOperations implements DevFSOperations { return e; } String fileContents = BASE64.encode(bytes); + try { return await serviceProtocol.sendRequest('_writeDevFSFile', { 'fsName': fsName, 'path': entry.devicePath, 'fileContents': fileContents }); + } catch (e) { + print('failed on ${entry.devicePath} $e'); + } } @override