• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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