material_arc.dart 14.1 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';
6
import 'dart:io';
7

8
import 'package:flutter/foundation.dart';
9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
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
32
  void update(DragUpdateDetails details) {
33 34 35 36
    onUpdate(details);
  }

  @override
37
  void cancel() {
38 39 40 41
    onCancel();
  }

  @override
42
  void end(DragEndDetails details) {
43 44 45 46 47 48 49 50 51 52
    onEnd(details);
  }
}

class _IgnoreDrag extends Drag {
}

class _PointDemoPainter extends CustomPainter {
  _PointDemoPainter({
    Animation<double> repaint,
53
    this.arc,
54 55 56
  }) : _repaint = repaint, super(repaint: repaint);

  final MaterialPointArcTween arc;
57
  final Animation<double> _repaint;
58

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

    if (arc.center != null)
76
      drawPoint(canvas, arc.center, Colors.grey.shade400);
77 78

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

88 89
    drawPoint(canvas, arc.begin, Colors.green);
    drawPoint(canvas, arc.end, Colors.red);
90 91

    paint
92
      ..color = Colors.green
93 94 95 96 97
      ..style = PaintingStyle.fill;
    canvas.drawCircle(arc.lerp(_repaint.value), _kPointRadius, paint);
  }

  @override
98
  bool hitTest(Offset position) {
99 100 101 102 103 104 105 106 107
    return (arc.begin - position).distanceSquared < _kTargetSlop
        || (arc.end - position).distanceSquared < _kTargetSlop;
  }

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

class _PointDemo extends StatefulWidget {
108
  const _PointDemo({ Key key, this.controller }) : super(key: key);
109 110 111 112

  final AnimationController controller;

  @override
113
  _PointDemoState createState() => _PointDemoState();
114 115 116
}

class _PointDemoState extends State<_PointDemo> {
117
  final GlobalKey _painterKey = GlobalKey();
118 119 120

  CurvedAnimation _animation;
  _DragTarget _dragTarget;
121
  Size _screenSize;
122 123
  Offset _begin;
  Offset _end;
124 125 126 127

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

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

137
  Drag _handleOnStart(Offset position) {
138 139
    // TODO(hansmuller): allow the user to drag both points at the same time.
    if (_dragTarget != null)
140
      return _IgnoreDrag();
141

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

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

157
  void _handleDragUpdate(DragUpdateDetails details) {
158 159 160 161 162 163 164 165 166 167 168 169 170 171
    switch (_dragTarget) {
      case _DragTarget.start:
        setState(() {
          _begin = _begin + details.delta;
        });
        break;
      case _DragTarget.end:
        setState(() {
          _end = _end + details.delta;
        });
        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 197 198 199 200
          (ImmediateMultiDragGestureRecognizer instance) {
            instance
              ..onStart = _handleOnStart;
          },
        ),
201
      },
202 203
      child: ClipRect(
        child: CustomPaint(
204
          key: _painterKey,
205
          foregroundPainter: _PointDemoPainter(
206
            repaint: _animation,
207
            arc: arc,
208 209 210 211
          ),
          // 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.
212 213
          child: IgnorePointer(
            child: Padding(
214
              padding: const EdgeInsets.all(16.0),
215
              child: Text(
216
                'Tap the refresh button to run the animation. Drag the green '
217
                "and red points to change the animation's path.",
218 219 220 221 222 223
                style: Theme.of(context).textTheme.caption.copyWith(fontSize: 16.0),
              ),
            ),
          ),
        ),
      ),
224 225 226 227 228 229 230
    );
  }
}

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

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

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

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

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

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

  final AnimationController controller;

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

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

  CurvedAnimation _animation;
  _DragTarget _dragTarget;
289
  Size _screenSize;
290 291
  Rect _begin;
  Rect _end;
292 293 294 295

  @override
  void initState() {
    super.initState();
296
    _animation = CurvedAnimation(parent: widget.controller, curve: Curves.fastOutSlowIn);
297 298 299 300
  }

  @override
  void dispose() {
301
    widget.controller.value = 0.0;
302 303 304
    super.dispose();
  }

305
  Drag _handleOnStart(Offset position) {
306 307
    // TODO(hansmuller): allow the user to drag both points at the same time.
    if (_dragTarget != null)
308
      return _IgnoreDrag();
309

310
    final RenderBox box = _painterKey.currentContext.findRenderObject() as RenderBox;
311 312 313 314 315 316 317 318 319 320
    final double startOffset = (box.localToGlobal(_begin.center) - position).distanceSquared;
    final double endOffset = (box.localToGlobal(_end.center) - position).distanceSquared;
    setState(() {
      if (startOffset < endOffset && startOffset < _kTargetSlop)
        _dragTarget = _DragTarget.start;
      else if (endOffset < _kTargetSlop)
        _dragTarget = _DragTarget.end;
      else
        _dragTarget = null;
    });
321
    return _DragHandler(_handleDragUpdate, _handleDragCancel, _handleDragEnd);
322 323
  }

324
  void _handleDragUpdate(DragUpdateDetails details) {
325 326 327 328 329 330 331 332 333 334 335 336 337 338
    switch (_dragTarget) {
      case _DragTarget.start:
        setState(() {
          _begin = _begin.shift(details.delta);
        });
        break;
      case _DragTarget.end:
        setState(() {
          _end = _end.shift(details.delta);
        });
        break;
    }
  }

339
  void _handleDragCancel() {
340
    _dragTarget = null;
341
    widget.controller.value = 0.0;
342 343
  }

344
  void _handleDragEnd(DragEndDetails details) {
345 346 347 348 349
    _dragTarget = null;
  }

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

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

401
typedef _DemoBuilder = Widget Function(_ArcDemo demo);
402 403

class _ArcDemo {
404
  _ArcDemo(this.title, this.builder, TickerProvider vsync)
405 406
    : controller = AnimationController(duration: const Duration(milliseconds: 500), vsync: vsync),
      key = GlobalKey(debugLabel: title);
407 408 409

  final String title;
  final _DemoBuilder builder;
410
  final AnimationController controller;
411 412 413 414
  final GlobalKey key;
}

class AnimationDemo extends StatefulWidget {
415
  const AnimationDemo({ Key key }) : super(key: key);
416 417

  @override
418
  _AnimationDemoState createState() => _AnimationDemoState();
419 420
}

421 422 423 424 425 426 427
class _AnimationDemoState extends State<AnimationDemo> with TickerProviderStateMixin {
  List<_ArcDemo> _allDemos;

  @override
  void initState() {
    super.initState();
    _allDemos = <_ArcDemo>[
428 429
      _ArcDemo('POINT', (_ArcDemo demo) {
        return _PointDemo(
430
          key: demo.key,
431
          controller: demo.controller,
432 433
        );
      }, this),
434 435
      _ArcDemo('RECTANGLE', (_ArcDemo demo) {
        return _RectangleDemo(
436
          key: demo.key,
437
          controller: demo.controller,
438 439 440 441
        );
      }, this),
    ];
  }
442

443
  Future<void> _play(_ArcDemo demo) async {
444 445 446 447 448 449 450
    await demo.controller.forward();
    if (demo.key.currentState != null && demo.key.currentState.mounted)
      demo.controller.reverse();
  }

  @override
  Widget build(BuildContext context) {
451
    return DefaultTabController(
Hans Muller's avatar
Hans Muller committed
452
      length: _allDemos.length,
453 454
      child: Scaffold(
        appBar: AppBar(
455
          title: const Text('Animation'),
456
          bottom: TabBar(
457
            tabs: _allDemos.map<Tab>((_ArcDemo demo) => Tab(text: demo.title)).toList(),
Hans Muller's avatar
Hans Muller committed
458
          ),
459
        ),
460
        floatingActionButton: Builder(
Hans Muller's avatar
Hans Muller committed
461
          builder: (BuildContext context) {
462
            return FloatingActionButton(
463
              child: const Icon(Icons.refresh),
Hans Muller's avatar
Hans Muller committed
464 465 466 467 468
              onPressed: () {
                _play(_allDemos[DefaultTabController.of(context).index]);
              },
            );
          },
469
        ),
470
        body: TabBarView(
471 472 473
          children: _allDemos.map<Widget>((_ArcDemo demo) => demo.builder(demo)).toList(),
        ),
      ),
474 475 476
    );
  }
}
477

478 479 480 481 482
// Sets a platform override for desktop to avoid exceptions. See
// https://flutter.dev/desktop#target-platform-override for more info.
// TODO(gspencergoog): Remove once TargetPlatform includes all desktop platforms.
void _enablePlatformOverrideForDesktop() {
  if (!kIsWeb && (Platform.isWindows || Platform.isLinux)) {
483 484
    debugDefaultTargetPlatformOverride = TargetPlatform.fuchsia;
  }
485
}
486

487 488
void main() {
  _enablePlatformOverrideForDesktop();
489 490
  runApp(const MaterialApp(
    home: AnimationDemo(),
491 492
  ));
}