mirror of
https://github.com/flutter/flutter.git
synced 2026-02-13 14:21:39 +08:00
Support for Material arc point and rect transitions (#4938)
This commit is contained in:
parent
4abaf64ccb
commit
36eb4a066f
@ -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';
|
||||
|
||||
439
examples/flutter_gallery/lib/demo/animation_demo.dart
Normal file
439
examples/flutter_gallery/lib/demo/animation_demo.dart
Normal file
@ -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<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];
|
||||
|
||||
253
packages/flutter/lib/src/material/arc.dart
Normal file
253
packages/flutter/lib/src/material/arc.dart
Normal file
@ -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<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);
|
||||
|
||||
|
||||
80
packages/flutter/test/material/arc_test.dart
Normal file
80
packages/flutter/test/material/arc_test.dart
Normal file
@ -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);
|
||||
});
|
||||
|
||||
}
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user