|  | // Copyright 2014 The Flutter 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 'dart:io'; | 
|  |  | 
|  | import 'package:flutter/foundation.dart'; | 
|  | 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; | 
|  | final Animation<double> _repaint; | 
|  |  | 
|  | void drawPoint(Canvas canvas, Offset point, Color color) { | 
|  | final Paint paint = 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 = Paint(); | 
|  |  | 
|  | if (arc.center != null) | 
|  | drawPoint(canvas, arc.center, Colors.grey.shade400); | 
|  |  | 
|  | paint | 
|  | ..isAntiAlias = false // Work-around for github.com/flutter/flutter/issues/5720 | 
|  | ..color = Colors.green.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); | 
|  | drawPoint(canvas, arc.end, Colors.red); | 
|  |  | 
|  | paint | 
|  | ..color = Colors.green | 
|  | ..style = PaintingStyle.fill; | 
|  | canvas.drawCircle(arc.lerp(_repaint.value), _kPointRadius, paint); | 
|  | } | 
|  |  | 
|  | @override | 
|  | bool hitTest(Offset position) { | 
|  | return (arc.begin - position).distanceSquared < _kTargetSlop | 
|  | || (arc.end - position).distanceSquared < _kTargetSlop; | 
|  | } | 
|  |  | 
|  | @override | 
|  | bool shouldRepaint(_PointDemoPainter oldPainter) => arc != oldPainter.arc; | 
|  | } | 
|  |  | 
|  | class _PointDemo extends StatefulWidget { | 
|  | const _PointDemo({ Key key, this.controller }) : super(key: key); | 
|  |  | 
|  | final AnimationController controller; | 
|  |  | 
|  | @override | 
|  | _PointDemoState createState() => _PointDemoState(); | 
|  | } | 
|  |  | 
|  | class _PointDemoState extends State<_PointDemo> { | 
|  | final GlobalKey _painterKey = GlobalKey(); | 
|  |  | 
|  | CurvedAnimation _animation; | 
|  | _DragTarget _dragTarget; | 
|  | Size _screenSize; | 
|  | Offset _begin; | 
|  | Offset _end; | 
|  |  | 
|  | @override | 
|  | void initState() { | 
|  | super.initState(); | 
|  | _animation = CurvedAnimation(parent: widget.controller, curve: Curves.fastOutSlowIn); | 
|  | } | 
|  |  | 
|  | @override | 
|  | void dispose() { | 
|  | widget.controller.value = 0.0; | 
|  | super.dispose(); | 
|  | } | 
|  |  | 
|  | Drag _handleOnStart(Offset position) { | 
|  | // TODO(hansmuller): allow the user to drag both points at the same time. | 
|  | if (_dragTarget != null) | 
|  | return _IgnoreDrag(); | 
|  |  | 
|  | final RenderBox box = _painterKey.currentContext.findRenderObject() as RenderBox; | 
|  | 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 _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; | 
|  | widget.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 = Offset(screenSize.width * 0.5, screenSize.height * 0.2); | 
|  | _end = Offset(screenSize.width * 0.1, screenSize.height * 0.4); | 
|  | } | 
|  |  | 
|  | final MaterialPointArcTween arc = MaterialPointArcTween(begin: _begin, end: _end); | 
|  | return RawGestureDetector( | 
|  | behavior: _dragTarget == null ? HitTestBehavior.deferToChild : HitTestBehavior.opaque, | 
|  | gestures: <Type, GestureRecognizerFactory>{ | 
|  | ImmediateMultiDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<ImmediateMultiDragGestureRecognizer>( | 
|  | () => ImmediateMultiDragGestureRecognizer(), | 
|  | (ImmediateMultiDragGestureRecognizer instance) { | 
|  | instance | 
|  | ..onStart = _handleOnStart; | 
|  | }, | 
|  | ), | 
|  | }, | 
|  | child: ClipRect( | 
|  | child: CustomPaint( | 
|  | key: _painterKey, | 
|  | foregroundPainter: _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: IgnorePointer( | 
|  | child: Padding( | 
|  | padding: const EdgeInsets.all(16.0), | 
|  | child: 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; | 
|  | final Animation<double> _repaint; | 
|  |  | 
|  | void drawPoint(Canvas canvas, Offset p, Color color) { | 
|  | final Paint paint = 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 = 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); | 
|  | drawRect(canvas, arc.end, Colors.red); | 
|  | drawRect(canvas, arc.lerp(_repaint.value), Colors.blue); | 
|  | } | 
|  |  | 
|  | @override | 
|  | bool hitTest(Offset 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 { | 
|  | const _RectangleDemo({ Key key, this.controller }) : super(key: key); | 
|  |  | 
|  | final AnimationController controller; | 
|  |  | 
|  | @override | 
|  | _RectangleDemoState createState() => _RectangleDemoState(); | 
|  | } | 
|  |  | 
|  | class _RectangleDemoState extends State<_RectangleDemo> { | 
|  | final GlobalKey _painterKey = GlobalKey(); | 
|  |  | 
|  | CurvedAnimation _animation; | 
|  | _DragTarget _dragTarget; | 
|  | Size _screenSize; | 
|  | Rect _begin; | 
|  | Rect _end; | 
|  |  | 
|  | @override | 
|  | void initState() { | 
|  | super.initState(); | 
|  | _animation = CurvedAnimation(parent: widget.controller, curve: Curves.fastOutSlowIn); | 
|  | } | 
|  |  | 
|  | @override | 
|  | void dispose() { | 
|  | widget.controller.value = 0.0; | 
|  | super.dispose(); | 
|  | } | 
|  |  | 
|  | Drag _handleOnStart(Offset position) { | 
|  | // TODO(hansmuller): allow the user to drag both points at the same time. | 
|  | if (_dragTarget != null) | 
|  | return _IgnoreDrag(); | 
|  |  | 
|  | final RenderBox box = _painterKey.currentContext.findRenderObject() as RenderBox; | 
|  | 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 _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; | 
|  | widget.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 = Rect.fromLTWH( | 
|  | screenSize.width * 0.5, screenSize.height * 0.2, | 
|  | screenSize.width * 0.4, screenSize.height * 0.2, | 
|  | ); | 
|  | _end = Rect.fromLTWH( | 
|  | screenSize.width * 0.1, screenSize.height * 0.4, | 
|  | screenSize.width * 0.3, screenSize.height * 0.3, | 
|  | ); | 
|  | } | 
|  |  | 
|  | final MaterialRectArcTween arc = MaterialRectArcTween(begin: _begin, end: _end); | 
|  | return RawGestureDetector( | 
|  | behavior: _dragTarget == null ? HitTestBehavior.deferToChild : HitTestBehavior.opaque, | 
|  | gestures: <Type, GestureRecognizerFactory>{ | 
|  | ImmediateMultiDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<ImmediateMultiDragGestureRecognizer>( | 
|  | () => ImmediateMultiDragGestureRecognizer(), | 
|  | (ImmediateMultiDragGestureRecognizer instance) { | 
|  | instance | 
|  | ..onStart = _handleOnStart; | 
|  | }, | 
|  | ), | 
|  | }, | 
|  | child: ClipRect( | 
|  | child: CustomPaint( | 
|  | key: _painterKey, | 
|  | foregroundPainter: _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: IgnorePointer( | 
|  | child: Padding( | 
|  | padding: const EdgeInsets.all(16.0), | 
|  | child: 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 _DemoBuilder = Widget Function(_ArcDemo demo); | 
|  |  | 
|  | class _ArcDemo { | 
|  | _ArcDemo(this.title, this.builder, TickerProvider vsync) | 
|  | : controller = AnimationController(duration: const Duration(milliseconds: 500), vsync: vsync), | 
|  | key = GlobalKey(debugLabel: title); | 
|  |  | 
|  | final String title; | 
|  | final _DemoBuilder builder; | 
|  | final AnimationController controller; | 
|  | final GlobalKey key; | 
|  | } | 
|  |  | 
|  | class AnimationDemo extends StatefulWidget { | 
|  | const AnimationDemo({ Key key }) : super(key: key); | 
|  |  | 
|  | @override | 
|  | _AnimationDemoState createState() => _AnimationDemoState(); | 
|  | } | 
|  |  | 
|  | class _AnimationDemoState extends State<AnimationDemo> with TickerProviderStateMixin { | 
|  | List<_ArcDemo> _allDemos; | 
|  |  | 
|  | @override | 
|  | void initState() { | 
|  | super.initState(); | 
|  | _allDemos = <_ArcDemo>[ | 
|  | _ArcDemo('POINT', (_ArcDemo demo) { | 
|  | return _PointDemo( | 
|  | key: demo.key, | 
|  | controller: demo.controller, | 
|  | ); | 
|  | }, this), | 
|  | _ArcDemo('RECTANGLE', (_ArcDemo demo) { | 
|  | return _RectangleDemo( | 
|  | key: demo.key, | 
|  | controller: demo.controller, | 
|  | ); | 
|  | }, this), | 
|  | ]; | 
|  | } | 
|  |  | 
|  | Future<void> _play(_ArcDemo demo) async { | 
|  | await demo.controller.forward(); | 
|  | if (demo.key.currentState != null && demo.key.currentState.mounted) | 
|  | demo.controller.reverse(); | 
|  | } | 
|  |  | 
|  | @override | 
|  | Widget build(BuildContext context) { | 
|  | return DefaultTabController( | 
|  | length: _allDemos.length, | 
|  | child: Scaffold( | 
|  | appBar: AppBar( | 
|  | title: const Text('Animation'), | 
|  | bottom: TabBar( | 
|  | tabs: _allDemos.map<Tab>((_ArcDemo demo) => Tab(text: demo.title)).toList(), | 
|  | ), | 
|  | ), | 
|  | floatingActionButton: Builder( | 
|  | builder: (BuildContext context) { | 
|  | return FloatingActionButton( | 
|  | child: const Icon(Icons.refresh), | 
|  | onPressed: () { | 
|  | _play(_allDemos[DefaultTabController.of(context).index]); | 
|  | }, | 
|  | ); | 
|  | }, | 
|  | ), | 
|  | body: TabBarView( | 
|  | children: _allDemos.map<Widget>((_ArcDemo demo) => demo.builder(demo)).toList(), | 
|  | ), | 
|  | ), | 
|  | ); | 
|  | } | 
|  | } | 
|  |  | 
|  | // Sets a platform override for desktop to avoid exceptions. See | 
|  | // https://flutter.dev/desktop#target-platform-override for more info. | 
|  | // TODO(gspencergoog): Remove once TargetPlatform includes all desktop platforms. | 
|  | void _enablePlatformOverrideForDesktop() { | 
|  | if (!kIsWeb && (Platform.isWindows || Platform.isLinux)) { | 
|  | debugDefaultTargetPlatformOverride = TargetPlatform.fuchsia; | 
|  | } | 
|  | } | 
|  |  | 
|  | void main() { | 
|  | _enablePlatformOverrideForDesktop(); | 
|  | runApp(const MaterialApp( | 
|  | home: AnimationDemo(), | 
|  | )); | 
|  | } |