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 165
        });
        break;
      case _DragTarget.end:
        setState(() {
166
          _end = _end! + details.delta;
167 168 169 170 171
        });
        break;
    }
  }

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

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

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

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

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

  final MaterialRectArcTween arc;
234
  final Animation<double> _repaint;
235

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

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

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

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

  final AnimationController controller;

  @override
280
  _RectangleDemoState createState() => _RectangleDemoState();
281 282 283
}

class _RectangleDemoState extends State<_RectangleDemo> {
284
  final GlobalKey _painterKey = GlobalKey();
285

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

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

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

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

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

334
  void _handleDragCancel() {
335
    _dragTarget = null;
336
    widget.controller.value = 0.0;
337 338
  }

339
  void _handleDragEnd(DragEndDetails details) {
340 341 342 343 344
    _dragTarget = null;
  }

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

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

395
typedef _DemoBuilder = Widget Function(_ArcDemo demo);
396 397

class _ArcDemo {
398
  _ArcDemo(this.title, this.builder, TickerProvider vsync)
399 400
    : controller = AnimationController(duration: const Duration(milliseconds: 500), vsync: vsync),
      key = GlobalKey(debugLabel: title);
401 402 403

  final String title;
  final _DemoBuilder builder;
404
  final AnimationController controller;
405 406 407 408
  final GlobalKey key;
}

class AnimationDemo extends StatefulWidget {
409
  const AnimationDemo({ super.key });
410 411

  @override
412
  State<AnimationDemo> createState() => _AnimationDemoState();
413 414
}

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

431
  Future<void> _play(_ArcDemo demo) async {
432
    await demo.controller.forward();
433
    if (demo.key.currentState != null && demo.key.currentState!.mounted) {
434
      demo.controller.reverse();
435
    }
436 437 438 439
  }

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

467
void main() {
468 469
  runApp(const MaterialApp(
    home: AnimationDemo(),
470 471
  ));
}