// 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 '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, required 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({ super.key, required this.controller }); 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; }); case _DragTarget.end: setState(() { _end = _end! + details.delta; }); } } 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.bodySmall?.copyWith(fontSize: 16.0), ), ), ), ), ), ); } } class _RectangleDemoPainter extends CustomPainter { _RectangleDemoPainter({ required Animation<double> repaint, required 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({ super.key, required this.controller }); final AnimationController controller; @override _RectangleDemoState createState() => _RectangleDemoState(); } class _RectangleDemoState extends State<_RectangleDemo> { final GlobalKey _painterKey = GlobalKey(); late final CurvedAnimation _animation = CurvedAnimation(parent: widget.controller, curve: Curves.fastOutSlowIn); _DragTarget? _dragTarget; Size? _screenSize; Rect? _begin; Rect? _end; @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); }); case _DragTarget.end: setState(() { _end = _end?.shift(details.delta); }); } } 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.bodySmall!.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({ super.key }); @override State<AnimationDemo> createState() => _AnimationDemoState(); } class _AnimationDemoState extends State<AnimationDemo> with TickerProviderStateMixin { late final List<_ArcDemo> _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(), ), ), ); } } void main() { runApp(const MaterialApp( home: AnimationDemo(), )); }