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

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

84 85
    drawPoint(canvas, arc.begin!, Colors.green);
    drawPoint(canvas, arc.end!, Colors.red);
86 87

    paint
88
      ..color = Colors.green
89
      ..style = PaintingStyle.fill;
90
    canvas.drawCircle(arc.lerp(_repaint!.value), _kPointRadius, paint);
91 92 93
  }

  @override
94
  bool hitTest(Offset position) {
95 96
    return (arc.begin! - position).distanceSquared < _kTargetSlop
        || (arc.end! - position).distanceSquared < _kTargetSlop;
97 98 99 100 101 102 103
  }

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

class _PointDemo extends StatefulWidget {
104
  const _PointDemo({ Key? key, required this.controller }) : super(key: key);
105 106 107 108

  final AnimationController controller;

  @override
109
  _PointDemoState createState() => _PointDemoState();
110 111 112
}

class _PointDemoState extends State<_PointDemo> {
113
  final GlobalKey _painterKey = GlobalKey();
114

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

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

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

133
  Drag _handleOnStart(Offset position) {
134 135
    // TODO(hansmuller): allow the user to drag both points at the same time.
    if (_dragTarget != null)
136
      return _IgnoreDrag();
137

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

150
    return _DragHandler(_handleDragUpdate, _handleDragCancel, _handleDragEnd);
151 152
  }

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

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

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

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

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

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

  final MaterialRectArcTween arc;
230
  final Animation<double> _repaint;
231

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

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

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

class _RectangleDemo extends StatefulWidget {
271
  const _RectangleDemo({ Key? key, required this.controller }) : super(key: key);
272 273 274 275

  final AnimationController controller;

  @override
276
  _RectangleDemoState createState() => _RectangleDemoState();
277 278 279
}

class _RectangleDemoState extends State<_RectangleDemo> {
280
  final GlobalKey _painterKey = GlobalKey();
281

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

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

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

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

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

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

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

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

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

389
typedef _DemoBuilder = Widget Function(_ArcDemo demo);
390 391

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

  final String title;
  final _DemoBuilder builder;
398
  final AnimationController controller;
399 400 401 402
  final GlobalKey key;
}

class AnimationDemo extends StatefulWidget {
403
  const AnimationDemo({ Key? key }) : super(key: key);
404 405

  @override
406
  State<AnimationDemo> createState() => _AnimationDemoState();
407 408
}

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

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

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

460
void main() {
461 462
  runApp(const MaterialApp(
    home: AnimationDemo(),
463 464
  ));
}