material_arc.dart 13.8 KB
Newer Older
1 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 28 29
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

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
30
  void update(DragUpdateDetails details) {
31 32 33 34
    onUpdate(details);
  }

  @override
35
  void cancel() {
36 37 38 39
    onCancel();
  }

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

class _IgnoreDrag extends Drag {
}

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

  final MaterialPointArcTween arc;
55
  final Animation<double> _repaint;
56

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

    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 80 81 82 83 84 85
      ..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);

86 87
    drawPoint(canvas, arc.begin, Colors.green);
    drawPoint(canvas, arc.end, Colors.red);
88 89

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

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

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

class _PointDemo extends StatefulWidget {
106
  const _PointDemo({ Key key, this.controller }) : super(key: key);
107 108 109 110 111 112 113 114 115 116 117 118

  final AnimationController controller;

  @override
  _PointDemoState createState() => new _PointDemoState();
}

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

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

  @override
  void initState() {
    super.initState();
126
    _animation = new 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 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154
    // TODO(hansmuller): allow the user to drag both points at the same time.
    if (_dragTarget != null)
      return new _IgnoreDrag();

    final RenderBox box = _painterKey.currentContext.findRenderObject();
    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;
    });

    return new _DragHandler(_handleDragUpdate, _handleDragCancel, _handleDragEnd);
  }

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

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 = new Offset(screenSize.width * 0.5, screenSize.height * 0.2);
      _end = new Offset(screenSize.width * 0.1, screenSize.height * 0.4);
186
    }
187

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

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

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

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

  @override
264
  bool hitTest(Offset position) {
265 266 267 268 269 270 271 272 273
    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 {
274
  const _RectangleDemo({ Key key, this.controller }) : super(key: key);
275 276 277 278 279 280 281 282 283 284 285 286

  final AnimationController controller;

  @override
  _RectangleDemoState createState() => new _RectangleDemoState();
}

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

  CurvedAnimation _animation;
  _DragTarget _dragTarget;
287
  Size _screenSize;
288 289
  Rect _begin;
  Rect _end;
290 291 292 293

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

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

303
  Drag _handleOnStart(Offset position) {
304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321
    // TODO(hansmuller): allow the user to drag both points at the same time.
    if (_dragTarget != null)
      return new _IgnoreDrag();

    final RenderBox box = _painterKey.currentContext.findRenderObject();
    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;
    });
    return new _DragHandler(_handleDragUpdate, _handleDragCancel, _handleDragEnd);
  }

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

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

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

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

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

typedef Widget _DemoBuilder(_ArcDemo demo);

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

  final String title;
  final _DemoBuilder builder;
408
  final AnimationController controller;
409 410 411 412
  final GlobalKey key;
}

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

  @override
  _AnimationDemoState createState() => new _AnimationDemoState();
}

419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439
class _AnimationDemoState extends State<AnimationDemo> with TickerProviderStateMixin {
  List<_ArcDemo> _allDemos;

  @override
  void initState() {
    super.initState();
    _allDemos = <_ArcDemo>[
      new _ArcDemo('POINT', (_ArcDemo demo) {
        return new _PointDemo(
          key: demo.key,
          controller: demo.controller
        );
      }, this),
      new _ArcDemo('RECTANGLE', (_ArcDemo demo) {
        return new _RectangleDemo(
          key: demo.key,
          controller: demo.controller
        );
      }, this),
    ];
  }
440

Hans Muller's avatar
Hans Muller committed
441
  Future<Null> _play(_ArcDemo demo) async {
442 443 444 445 446 447 448
    await demo.controller.forward();
    if (demo.key.currentState != null && demo.key.currentState.mounted)
      demo.controller.reverse();
  }

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

void main() {
  runApp(new MaterialApp(
478
    home: const AnimationDemo(),
479 480
  ));
}