1// Copyright 2016 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5import 'dart:async'; 6 7import 'package:flutter/gestures.dart'; 8import 'package:flutter/material.dart'; 9 10enum _DragTarget { 11 start, 12 end 13} 14 15// How close a drag's start position must be to the target point. This is 16// a distance squared. 17const double _kTargetSlop = 2500.0; 18 19// Used by the Painter classes. 20const double _kPointRadius = 6.0; 21 22class _DragHandler extends Drag { 23 _DragHandler(this.onUpdate, this.onCancel, this.onEnd); 24 25 final GestureDragUpdateCallback onUpdate; 26 final GestureDragCancelCallback onCancel; 27 final GestureDragEndCallback onEnd; 28 29 @override 30 void update(DragUpdateDetails details) { 31 onUpdate(details); 32 } 33 34 @override 35 void cancel() { 36 onCancel(); 37 } 38 39 @override 40 void end(DragEndDetails details) { 41 onEnd(details); 42 } 43} 44 45class _IgnoreDrag extends Drag { 46} 47 48class _PointDemoPainter extends CustomPainter { 49 _PointDemoPainter({ 50 Animation<double> repaint, 51 this.arc, 52 }) : _repaint = repaint, super(repaint: repaint); 53 54 final MaterialPointArcTween arc; 55 final Animation<double> _repaint; 56 57 void drawPoint(Canvas canvas, Offset point, Color color) { 58 final Paint paint = Paint() 59 ..color = color.withOpacity(0.25) 60 ..style = PaintingStyle.fill; 61 canvas.drawCircle(point, _kPointRadius, paint); 62 paint 63 ..color = color 64 ..style = PaintingStyle.stroke 65 ..strokeWidth = 2.0; 66 canvas.drawCircle(point, _kPointRadius + 1.0, paint); 67 } 68 69 @override 70 void paint(Canvas canvas, Size size) { 71 final Paint paint = Paint(); 72 73 if (arc.center != null) 74 drawPoint(canvas, arc.center, Colors.grey.shade400); 75 76 paint 77 ..isAntiAlias = false // Work-around for github.com/flutter/flutter/issues/5720 78 ..color = Colors.green.withOpacity(0.25) 79 ..strokeWidth = 4.0 80 ..style = PaintingStyle.stroke; 81 if (arc.center != null && arc.radius != null) 82 canvas.drawCircle(arc.center, arc.radius, paint); 83 else 84 canvas.drawLine(arc.begin, arc.end, paint); 85 86 drawPoint(canvas, arc.begin, Colors.green); 87 drawPoint(canvas, arc.end, Colors.red); 88 89 paint 90 ..color = Colors.green 91 ..style = PaintingStyle.fill; 92 canvas.drawCircle(arc.lerp(_repaint.value), _kPointRadius, paint); 93 } 94 95 @override 96 bool hitTest(Offset position) { 97 return (arc.begin - position).distanceSquared < _kTargetSlop 98 || (arc.end - position).distanceSquared < _kTargetSlop; 99 } 100 101 @override 102 bool shouldRepaint(_PointDemoPainter oldPainter) => arc != oldPainter.arc; 103} 104 105class _PointDemo extends StatefulWidget { 106 const _PointDemo({ Key key, this.controller }) : super(key: key); 107 108 final AnimationController controller; 109 110 @override 111 _PointDemoState createState() => _PointDemoState(); 112} 113 114class _PointDemoState extends State<_PointDemo> { 115 final GlobalKey _painterKey = GlobalKey(); 116 117 CurvedAnimation _animation; 118 _DragTarget _dragTarget; 119 Size _screenSize; 120 Offset _begin; 121 Offset _end; 122 123 @override 124 void initState() { 125 super.initState(); 126 _animation = CurvedAnimation(parent: widget.controller, curve: Curves.fastOutSlowIn); 127 } 128 129 @override 130 void dispose() { 131 widget.controller.value = 0.0; 132 super.dispose(); 133 } 134 135 Drag _handleOnStart(Offset position) { 136 // TODO(hansmuller): allow the user to drag both points at the same time. 137 if (_dragTarget != null) 138 return _IgnoreDrag(); 139 140 final RenderBox box = _painterKey.currentContext.findRenderObject(); 141 final double startOffset = (box.localToGlobal(_begin) - position).distanceSquared; 142 final double endOffset = (box.localToGlobal(_end) - position).distanceSquared; 143 setState(() { 144 if (startOffset < endOffset && startOffset < _kTargetSlop) 145 _dragTarget = _DragTarget.start; 146 else if (endOffset < _kTargetSlop) 147 _dragTarget = _DragTarget.end; 148 else 149 _dragTarget = null; 150 }); 151 152 return _DragHandler(_handleDragUpdate, _handleDragCancel, _handleDragEnd); 153 } 154 155 void _handleDragUpdate(DragUpdateDetails details) { 156 switch (_dragTarget) { 157 case _DragTarget.start: 158 setState(() { 159 _begin = _begin + details.delta; 160 }); 161 break; 162 case _DragTarget.end: 163 setState(() { 164 _end = _end + details.delta; 165 }); 166 break; 167 } 168 } 169 170 void _handleDragCancel() { 171 _dragTarget = null; 172 widget.controller.value = 0.0; 173 } 174 175 void _handleDragEnd(DragEndDetails details) { 176 _dragTarget = null; 177 } 178 179 @override 180 Widget build(BuildContext context) { 181 final Size screenSize = MediaQuery.of(context).size; 182 if (_screenSize == null || _screenSize != screenSize) { 183 _screenSize = screenSize; 184 _begin = Offset(screenSize.width * 0.5, screenSize.height * 0.2); 185 _end = Offset(screenSize.width * 0.1, screenSize.height * 0.4); 186 } 187 188 final MaterialPointArcTween arc = MaterialPointArcTween(begin: _begin, end: _end); 189 return RawGestureDetector( 190 behavior: _dragTarget == null ? HitTestBehavior.deferToChild : HitTestBehavior.opaque, 191 gestures: <Type, GestureRecognizerFactory>{ 192 ImmediateMultiDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<ImmediateMultiDragGestureRecognizer>( 193 () => ImmediateMultiDragGestureRecognizer(), 194 (ImmediateMultiDragGestureRecognizer instance) { 195 instance 196 ..onStart = _handleOnStart; 197 }, 198 ), 199 }, 200 child: ClipRect( 201 child: CustomPaint( 202 key: _painterKey, 203 foregroundPainter: _PointDemoPainter( 204 repaint: _animation, 205 arc: arc, 206 ), 207 // Watch out: if this IgnorePointer is left out, then gestures that 208 // fail _PointDemoPainter.hitTest() will still be recognized because 209 // they do overlap this child, which is as big as the CustomPaint. 210 child: IgnorePointer( 211 child: Padding( 212 padding: const EdgeInsets.all(16.0), 213 child: Text( 214 'Tap the refresh button to run the animation. Drag the green ' 215 "and red points to change the animation's path.", 216 style: Theme.of(context).textTheme.caption.copyWith(fontSize: 16.0), 217 ), 218 ), 219 ), 220 ), 221 ), 222 ); 223 } 224} 225 226class _RectangleDemoPainter extends CustomPainter { 227 _RectangleDemoPainter({ 228 Animation<double> repaint, 229 this.arc, 230 }) : _repaint = repaint, super(repaint: repaint); 231 232 final MaterialRectArcTween arc; 233 final Animation<double> _repaint; 234 235 void drawPoint(Canvas canvas, Offset p, Color color) { 236 final Paint paint = Paint() 237 ..color = color.withOpacity(0.25) 238 ..style = PaintingStyle.fill; 239 canvas.drawCircle(p, _kPointRadius, paint); 240 paint 241 ..color = color 242 ..style = PaintingStyle.stroke 243 ..strokeWidth = 2.0; 244 canvas.drawCircle(p, _kPointRadius + 1.0, paint); 245 } 246 247 void drawRect(Canvas canvas, Rect rect, Color color) { 248 final Paint paint = Paint() 249 ..color = color.withOpacity(0.25) 250 ..strokeWidth = 4.0 251 ..style = PaintingStyle.stroke; 252 canvas.drawRect(rect, paint); 253 drawPoint(canvas, rect.center, color); 254 } 255 256 @override 257 void paint(Canvas canvas, Size size) { 258 drawRect(canvas, arc.begin, Colors.green); 259 drawRect(canvas, arc.end, Colors.red); 260 drawRect(canvas, arc.lerp(_repaint.value), Colors.blue); 261 } 262 263 @override 264 bool hitTest(Offset position) { 265 return (arc.begin.center - position).distanceSquared < _kTargetSlop 266 || (arc.end.center - position).distanceSquared < _kTargetSlop; 267 } 268 269 @override 270 bool shouldRepaint(_RectangleDemoPainter oldPainter) => arc != oldPainter.arc; 271} 272 273class _RectangleDemo extends StatefulWidget { 274 const _RectangleDemo({ Key key, this.controller }) : super(key: key); 275 276 final AnimationController controller; 277 278 @override 279 _RectangleDemoState createState() => _RectangleDemoState(); 280} 281 282class _RectangleDemoState extends State<_RectangleDemo> { 283 final GlobalKey _painterKey = GlobalKey(); 284 285 CurvedAnimation _animation; 286 _DragTarget _dragTarget; 287 Size _screenSize; 288 Rect _begin; 289 Rect _end; 290 291 @override 292 void initState() { 293 super.initState(); 294 _animation = CurvedAnimation(parent: widget.controller, curve: Curves.fastOutSlowIn); 295 } 296 297 @override 298 void dispose() { 299 widget.controller.value = 0.0; 300 super.dispose(); 301 } 302 303 Drag _handleOnStart(Offset position) { 304 // TODO(hansmuller): allow the user to drag both points at the same time. 305 if (_dragTarget != null) 306 return _IgnoreDrag(); 307 308 final RenderBox box = _painterKey.currentContext.findRenderObject(); 309 final double startOffset = (box.localToGlobal(_begin.center) - position).distanceSquared; 310 final double endOffset = (box.localToGlobal(_end.center) - position).distanceSquared; 311 setState(() { 312 if (startOffset < endOffset && startOffset < _kTargetSlop) 313 _dragTarget = _DragTarget.start; 314 else if (endOffset < _kTargetSlop) 315 _dragTarget = _DragTarget.end; 316 else 317 _dragTarget = null; 318 }); 319 return _DragHandler(_handleDragUpdate, _handleDragCancel, _handleDragEnd); 320 } 321 322 void _handleDragUpdate(DragUpdateDetails details) { 323 switch (_dragTarget) { 324 case _DragTarget.start: 325 setState(() { 326 _begin = _begin.shift(details.delta); 327 }); 328 break; 329 case _DragTarget.end: 330 setState(() { 331 _end = _end.shift(details.delta); 332 }); 333 break; 334 } 335 } 336 337 void _handleDragCancel() { 338 _dragTarget = null; 339 widget.controller.value = 0.0; 340 } 341 342 void _handleDragEnd(DragEndDetails details) { 343 _dragTarget = null; 344 } 345 346 @override 347 Widget build(BuildContext context) { 348 final Size screenSize = MediaQuery.of(context).size; 349 if (_screenSize == null || _screenSize != screenSize) { 350 _screenSize = screenSize; 351 _begin = Rect.fromLTWH( 352 screenSize.width * 0.5, screenSize.height * 0.2, 353 screenSize.width * 0.4, screenSize.height * 0.2, 354 ); 355 _end = Rect.fromLTWH( 356 screenSize.width * 0.1, screenSize.height * 0.4, 357 screenSize.width * 0.3, screenSize.height * 0.3, 358 ); 359 } 360 361 final MaterialRectArcTween arc = MaterialRectArcTween(begin: _begin, end: _end); 362 return RawGestureDetector( 363 behavior: _dragTarget == null ? HitTestBehavior.deferToChild : HitTestBehavior.opaque, 364 gestures: <Type, GestureRecognizerFactory>{ 365 ImmediateMultiDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<ImmediateMultiDragGestureRecognizer>( 366 () => ImmediateMultiDragGestureRecognizer(), 367 (ImmediateMultiDragGestureRecognizer instance) { 368 instance 369 ..onStart = _handleOnStart; 370 }, 371 ), 372 }, 373 child: ClipRect( 374 child: CustomPaint( 375 key: _painterKey, 376 foregroundPainter: _RectangleDemoPainter( 377 repaint: _animation, 378 arc: arc, 379 ), 380 // Watch out: if this IgnorePointer is left out, then gestures that 381 // fail _RectDemoPainter.hitTest() will still be recognized because 382 // they do overlap this child, which is as big as the CustomPaint. 383 child: IgnorePointer( 384 child: Padding( 385 padding: const EdgeInsets.all(16.0), 386 child: Text( 387 'Tap the refresh button to run the animation. Drag the rectangles ' 388 "to change the animation's path.", 389 style: Theme.of(context).textTheme.caption.copyWith(fontSize: 16.0), 390 ), 391 ), 392 ), 393 ), 394 ), 395 ); 396 } 397} 398 399typedef _DemoBuilder = Widget Function(_ArcDemo demo); 400 401class _ArcDemo { 402 _ArcDemo(this.title, this.builder, TickerProvider vsync) 403 : controller = AnimationController(duration: const Duration(milliseconds: 500), vsync: vsync), 404 key = GlobalKey(debugLabel: title); 405 406 final String title; 407 final _DemoBuilder builder; 408 final AnimationController controller; 409 final GlobalKey key; 410} 411 412class AnimationDemo extends StatefulWidget { 413 const AnimationDemo({ Key key }) : super(key: key); 414 415 @override 416 _AnimationDemoState createState() => _AnimationDemoState(); 417} 418 419class _AnimationDemoState extends State<AnimationDemo> with TickerProviderStateMixin { 420 List<_ArcDemo> _allDemos; 421 422 @override 423 void initState() { 424 super.initState(); 425 _allDemos = <_ArcDemo>[ 426 _ArcDemo('POINT', (_ArcDemo demo) { 427 return _PointDemo( 428 key: demo.key, 429 controller: demo.controller, 430 ); 431 }, this), 432 _ArcDemo('RECTANGLE', (_ArcDemo demo) { 433 return _RectangleDemo( 434 key: demo.key, 435 controller: demo.controller, 436 ); 437 }, this), 438 ]; 439 } 440 441 Future<void> _play(_ArcDemo demo) async { 442 await demo.controller.forward(); 443 if (demo.key.currentState != null && demo.key.currentState.mounted) 444 demo.controller.reverse(); 445 } 446 447 @override 448 Widget build(BuildContext context) { 449 return DefaultTabController( 450 length: _allDemos.length, 451 child: Scaffold( 452 appBar: AppBar( 453 title: const Text('Animation'), 454 bottom: TabBar( 455 tabs: _allDemos.map<Tab>((_ArcDemo demo) => Tab(text: demo.title)).toList(), 456 ), 457 ), 458 floatingActionButton: Builder( 459 builder: (BuildContext context) { 460 return FloatingActionButton( 461 child: const Icon(Icons.refresh), 462 onPressed: () { 463 _play(_allDemos[DefaultTabController.of(context).index]); 464 }, 465 ); 466 }, 467 ), 468 body: TabBarView( 469 children: _allDemos.map<Widget>((_ArcDemo demo) => demo.builder(demo)).toList(), 470 ), 471 ), 472 ); 473 } 474} 475 476void main() { 477 runApp(const MaterialApp( 478 home: AnimationDemo(), 479 )); 480} 481