material_arc.dart 13.5 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
// 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
28
  void update(DragUpdateDetails details) {
29 30 31 32
    onUpdate(details);
  }

  @override
33
  void cancel() {
34 35 36 37
    onCancel();
  }

  @override
38
  void end(DragEndDetails details) {
39 40 41 42 43 44 45 46 47
    onEnd(details);
  }
}

class _IgnoreDrag extends Drag {
}

class _PointDemoPainter extends CustomPainter {
  _PointDemoPainter({
48 49
    Animation<double>? repaint,
    required this.arc,
50 51 52
  }) : _repaint = repaint, super(repaint: repaint);

  final MaterialPointArcTween arc;
53
  final Animation<double>? _repaint;
54

55
  void drawPoint(Canvas canvas, Offset point, Color color) {
56
    final Paint paint = Paint()
57 58 59 60 61 62 63 64 65 66 67 68
      ..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) {
69
    final Paint paint = Paint();
70

71
    if (arc.center != null) {
72
      drawPoint(canvas, arc.center!, Colors.grey.shade400);
73
    }
74 75

    paint
76
      ..isAntiAlias = false // Work-around for github.com/flutter/flutter/issues/5720
77
      ..color = Colors.green.withOpacity(0.25)
78 79
      ..strokeWidth = 4.0
      ..style = PaintingStyle.stroke;
80
    if (arc.center != null && arc.radius != null) {
81
      canvas.drawCircle(arc.center!, arc.radius!, paint);
82
    } else {
83
      canvas.drawLine(arc.begin!, arc.end!, paint);
84
    }
85

86 87
    drawPoint(canvas, arc.begin!, Colors.green);
    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 98
    return (arc.begin! - position).distanceSquared < _kTargetSlop
        || (arc.end! - position).distanceSquared < _kTargetSlop;
99 100 101 102 103 104 105
  }

  @override
  bool shouldRepaint(_PointDemoPainter oldPainter) => arc != oldPainter.arc;
}

class _PointDemo extends StatefulWidget {
106
  const _PointDemo({ super.key, required this.controller });
107 108 109 110

  final AnimationController controller;

  @override
111
  _PointDemoState createState() => _PointDemoState();
112 113 114
}

class _PointDemoState extends State<_PointDemo> {
115
  final GlobalKey _painterKey = GlobalKey();
116

117 118 119 120 121
  CurvedAnimation? _animation;
  _DragTarget? _dragTarget;
  Size? _screenSize;
  Offset? _begin;
  Offset? _end;
122 123 124 125

  @override
  void initState() {
    super.initState();
126
    _animation = CurvedAnimation(parent: widget.controller, curve: Curves.fastOutSlowIn);
127 128 129 130
  }

  @override
  void dispose() {
131
    widget.controller.value = 0.0;
132 133 134
    super.dispose();
  }

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

141 142 143
    final RenderBox? box = _painterKey.currentContext!.findRenderObject() as RenderBox?;
    final double startOffset = (box!.localToGlobal(_begin!) - position).distanceSquared;
    final double endOffset = (box.localToGlobal(_end!) - position).distanceSquared;
144
    setState(() {
145
      if (startOffset < endOffset && startOffset < _kTargetSlop) {
146
        _dragTarget = _DragTarget.start;
147
      } else if (endOffset < _kTargetSlop) {
148
        _dragTarget = _DragTarget.end;
149
      } else {
150
        _dragTarget = null;
151
      }
152 153
    });

154
    return _DragHandler(_handleDragUpdate, _handleDragCancel, _handleDragEnd);
155 156
  }

157
  void _handleDragUpdate(DragUpdateDetails details) {
158
    switch (_dragTarget!) {
159 160
      case _DragTarget.start:
        setState(() {
161
          _begin = _begin! + details.delta;
162 163 164
        });
      case _DragTarget.end:
        setState(() {
165
          _end = _end! + details.delta;
166 167 168 169
        });
    }
  }

170
  void _handleDragCancel() {
171
    _dragTarget = null;
172
    widget.controller.value = 0.0;
173 174
  }

175
  void _handleDragEnd(DragEndDetails details) {
176 177 178 179 180
    _dragTarget = null;
  }

  @override
  Widget build(BuildContext context) {
181
    final Size screenSize = MediaQuery.of(context).size;
182 183
    if (_screenSize == null || _screenSize != screenSize) {
      _screenSize = screenSize;
184 185
      _begin = Offset(screenSize.width * 0.5, screenSize.height * 0.2);
      _end = Offset(screenSize.width * 0.1, screenSize.height * 0.4);
186
    }
187

188 189
    final MaterialPointArcTween arc = MaterialPointArcTween(begin: _begin, end: _end);
    return RawGestureDetector(
190
      behavior: _dragTarget == null ? HitTestBehavior.deferToChild : HitTestBehavior.opaque,
191
      gestures: <Type, GestureRecognizerFactory>{
192 193
        ImmediateMultiDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<ImmediateMultiDragGestureRecognizer>(
          () => ImmediateMultiDragGestureRecognizer(),
194
          (ImmediateMultiDragGestureRecognizer instance) {
195
            instance.onStart = _handleOnStart;
196 197
          },
        ),
198
      },
199 200
      child: ClipRect(
        child: CustomPaint(
201
          key: _painterKey,
202
          foregroundPainter: _PointDemoPainter(
203
            repaint: _animation,
204
            arc: arc,
205 206 207 208
          ),
          // 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.
209 210
          child: IgnorePointer(
            child: Padding(
211
              padding: const EdgeInsets.all(16.0),
212
              child: Text(
213
                'Tap the refresh button to run the animation. Drag the green '
214
                "and red points to change the animation's path.",
215
                style: Theme.of(context).textTheme.bodySmall?.copyWith(fontSize: 16.0),
216 217 218 219 220
              ),
            ),
          ),
        ),
      ),
221 222 223 224 225 226
    );
  }
}

class _RectangleDemoPainter extends CustomPainter {
  _RectangleDemoPainter({
227 228
    required Animation<double> repaint,
    required this.arc,
229 230 231
  }) : _repaint = repaint, super(repaint: repaint);

  final MaterialRectArcTween arc;
232
  final Animation<double> _repaint;
233

234
  void drawPoint(Canvas canvas, Offset p, Color color) {
235
    final Paint paint = Paint()
236 237 238 239 240 241 242 243 244 245 246
      ..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) {
247
    final Paint paint = Paint()
248 249 250 251 252 253 254 255 256
      ..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) {
257 258
    drawRect(canvas, arc.begin!, Colors.green);
    drawRect(canvas, arc.end!, Colors.red);
259
    drawRect(canvas, arc.lerp(_repaint.value), Colors.blue);
260 261 262
  }

  @override
263
  bool hitTest(Offset position) {
264 265
    return (arc.begin!.center - position).distanceSquared < _kTargetSlop
        || (arc.end!.center - position).distanceSquared < _kTargetSlop;
266 267 268 269 270 271 272
  }

  @override
  bool shouldRepaint(_RectangleDemoPainter oldPainter) => arc != oldPainter.arc;
}

class _RectangleDemo extends StatefulWidget {
273
  const _RectangleDemo({ super.key, required this.controller });
274 275 276 277

  final AnimationController controller;

  @override
278
  _RectangleDemoState createState() => _RectangleDemoState();
279 280 281
}

class _RectangleDemoState extends State<_RectangleDemo> {
282
  final GlobalKey _painterKey = GlobalKey();
283

284 285 286 287 288
  late final CurvedAnimation _animation = CurvedAnimation(parent: widget.controller, curve: Curves.fastOutSlowIn);
  _DragTarget? _dragTarget;
  Size? _screenSize;
  Rect? _begin;
  Rect? _end;
289 290 291

  @override
  void dispose() {
292
    widget.controller.value = 0.0;
293 294 295
    super.dispose();
  }

296
  Drag _handleOnStart(Offset position) {
297
    // TODO(hansmuller): allow the user to drag both points at the same time.
298
    if (_dragTarget != null) {
299
      return _IgnoreDrag();
300
    }
301

302 303 304
    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;
305
    setState(() {
306
      if (startOffset < endOffset && startOffset < _kTargetSlop) {
307
        _dragTarget = _DragTarget.start;
308
      } else if (endOffset < _kTargetSlop) {
309
        _dragTarget = _DragTarget.end;
310
      } else {
311
        _dragTarget = null;
312
      }
313
    });
314
    return _DragHandler(_handleDragUpdate, _handleDragCancel, _handleDragEnd);
315 316
  }

317
  void _handleDragUpdate(DragUpdateDetails details) {
318
    switch (_dragTarget!) {
319 320
      case _DragTarget.start:
        setState(() {
321
          _begin = _begin?.shift(details.delta);
322 323 324
        });
      case _DragTarget.end:
        setState(() {
325
          _end = _end?.shift(details.delta);
326 327 328 329
        });
    }
  }

330
  void _handleDragCancel() {
331
    _dragTarget = null;
332
    widget.controller.value = 0.0;
333 334
  }

335
  void _handleDragEnd(DragEndDetails details) {
336 337 338 339 340
    _dragTarget = null;
  }

  @override
  Widget build(BuildContext context) {
341
    final Size screenSize = MediaQuery.of(context).size;
342 343
    if (_screenSize == null || _screenSize != screenSize) {
      _screenSize = screenSize;
344
      _begin = Rect.fromLTWH(
345
        screenSize.width * 0.5, screenSize.height * 0.2,
346
        screenSize.width * 0.4, screenSize.height * 0.2,
347
      );
348
      _end = Rect.fromLTWH(
349
        screenSize.width * 0.1, screenSize.height * 0.4,
350
        screenSize.width * 0.3, screenSize.height * 0.3,
351 352
      );
    }
353

354 355
    final MaterialRectArcTween arc = MaterialRectArcTween(begin: _begin, end: _end);
    return RawGestureDetector(
356 357
      behavior: _dragTarget == null ? HitTestBehavior.deferToChild : HitTestBehavior.opaque,
      gestures: <Type, GestureRecognizerFactory>{
358 359
        ImmediateMultiDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<ImmediateMultiDragGestureRecognizer>(
          () => ImmediateMultiDragGestureRecognizer(),
360
          (ImmediateMultiDragGestureRecognizer instance) {
361
            instance.onStart = _handleOnStart;
362 363
          },
        ),
364
      },
365 366
      child: ClipRect(
        child: CustomPaint(
367
          key: _painterKey,
368
          foregroundPainter: _RectangleDemoPainter(
369
            repaint: _animation,
370
            arc: arc,
371 372 373 374
          ),
          // 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.
375 376
          child: IgnorePointer(
            child: Padding(
377
              padding: const EdgeInsets.all(16.0),
378
              child: Text(
379
                'Tap the refresh button to run the animation. Drag the rectangles '
380
                "to change the animation's path.",
381
                style: Theme.of(context).textTheme.bodySmall!.copyWith(fontSize: 16.0),
382 383 384 385 386
              ),
            ),
          ),
        ),
      ),
387 388 389 390
    );
  }
}

391
typedef _DemoBuilder = Widget Function(_ArcDemo demo);
392 393

class _ArcDemo {
394
  _ArcDemo(this.title, this.builder, TickerProvider vsync)
395 396
    : controller = AnimationController(duration: const Duration(milliseconds: 500), vsync: vsync),
      key = GlobalKey(debugLabel: title);
397 398 399

  final String title;
  final _DemoBuilder builder;
400
  final AnimationController controller;
401 402 403 404
  final GlobalKey key;
}

class AnimationDemo extends StatefulWidget {
405
  const AnimationDemo({ super.key });
406 407

  @override
408
  State<AnimationDemo> createState() => _AnimationDemoState();
409 410
}

411
class _AnimationDemoState extends State<AnimationDemo> with TickerProviderStateMixin {
412 413 414 415 416 417 418 419 420 421 422 423 424 425
  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),
  ];
426

427
  Future<void> _play(_ArcDemo demo) async {
428
    await demo.controller.forward();
429
    if (demo.key.currentState != null && demo.key.currentState!.mounted) {
430
      demo.controller.reverse();
431
    }
432 433 434 435
  }

  @override
  Widget build(BuildContext context) {
436
    return DefaultTabController(
Hans Muller's avatar
Hans Muller committed
437
      length: _allDemos.length,
438 439
      child: Scaffold(
        appBar: AppBar(
440
          title: const Text('Animation'),
441
          bottom: TabBar(
442
            tabs: _allDemos.map<Tab>((_ArcDemo demo) => Tab(text: demo.title)).toList(),
Hans Muller's avatar
Hans Muller committed
443
          ),
444
        ),
445
        floatingActionButton: Builder(
Hans Muller's avatar
Hans Muller committed
446
          builder: (BuildContext context) {
447
            return FloatingActionButton(
448
              child: const Icon(Icons.refresh),
Hans Muller's avatar
Hans Muller committed
449
              onPressed: () {
450
                _play(_allDemos[DefaultTabController.of(context).index]);
Hans Muller's avatar
Hans Muller committed
451 452 453
              },
            );
          },
454
        ),
455
        body: TabBarView(
456 457 458
          children: _allDemos.map<Widget>((_ArcDemo demo) => demo.builder(demo)).toList(),
        ),
      ),
459 460 461
    );
  }
}
462

463
void main() {
464 465
  runApp(const MaterialApp(
    home: AnimationDemo(),
466 467
  ));
}