| // 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.grey[400]); | |
| paint | |
| ..isAntiAlias = false // Work-around for github.com/flutter/flutter/issues/5720 | |
| ..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; | |
| Size _screenSize; | |
| Point _begin; | |
| Point _end; | |
| @override | |
| void initState() { | |
| super.initState(); | |
| _animation = new CurvedAnimation(parent: config.controller, curve: Curves.fastOutSlowIn); | |
| } | |
| @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 Size screenSize = MediaQuery.of(context).size; | |
| if (_screenSize == null || _screenSize != screenSize) { | |
| _screenSize = screenSize; | |
| _begin = new Point(screenSize.width * 0.5, screenSize.height * 0.2); | |
| _end = new Point(screenSize.width * 0.1, screenSize.height * 0.4); | |
| } | |
| 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) { // ignore: map_value_type_not_assignable, https://github.com/flutter/flutter/issues/5771 | |
| 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; | |
| Size _screenSize; | |
| Rect _begin; | |
| Rect _end; | |
| @override | |
| void initState() { | |
| super.initState(); | |
| _animation = new CurvedAnimation(parent: config.controller, curve: Curves.fastOutSlowIn); | |
| } | |
| @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 Size screenSize = MediaQuery.of(context).size; | |
| if (_screenSize == null || _screenSize != screenSize) { | |
| _screenSize = screenSize; | |
| _begin = new Rect.fromLTWH( | |
| screenSize.width * 0.5, screenSize.height * 0.2, | |
| screenSize.width * 0.4, screenSize.height * 0.2 | |
| ); | |
| _end = new Rect.fromLTWH( | |
| screenSize.width * 0.1, screenSize.height * 0.4, | |
| screenSize.width * 0.3, screenSize.height * 0.3 | |
| ); | |
| } | |
| 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) { // ignore: map_value_type_not_assignable, https://github.com/flutter/flutter/issues/5771 | |
| 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, TickerProvider vsync) | |
| : title = _title, | |
| controller = new AnimationController(duration: const Duration(milliseconds: 500), vsync: vsync), | |
| key = new GlobalKey(debugLabel: _title); | |
| final String title; | |
| final _DemoBuilder builder; | |
| final AnimationController controller; | |
| final GlobalKey key; | |
| } | |
| class AnimationDemo extends StatefulWidget { | |
| AnimationDemo({ Key key }) : super(key: key); | |
| @override | |
| _AnimationDemoState createState() => new _AnimationDemoState(); | |
| } | |
| class _AnimationDemoState extends State<AnimationDemo> with TickerProviderStateMixin { | |
| static final GlobalKey<TabBarSelectionState<_ArcDemo>> _tabsKey = new GlobalKey<TabBarSelectionState<_ArcDemo>>(); | |
| List<_ArcDemo> _allDemos; | |
| @override | |
| void initState() { | |
| super.initState(); | |
| _allDemos = <_ArcDemo>[ | |
| new _ArcDemo('POINT', (_ArcDemo demo) { | |
| return new _PointDemo( | |
| key: demo.key, | |
| controller: demo.controller | |
| ); | |
| }, this), | |
| new _ArcDemo('RECTANGLE', (_ArcDemo demo) { | |
| return new _RectangleDemo( | |
| key: demo.key, | |
| controller: demo.controller | |
| ); | |
| }, this), | |
| ]; | |
| } | |
| 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() | |
| ) | |
| ) | |
| ); | |
| } | |
| } | |
| void main() { | |
| runApp(new MaterialApp( | |
| home: new AnimationDemo() | |
| )); | |
| } |