Make animation_demo a manual test (#6117)

This demo doesn't work properly in landscape and is confusing. It's
really more of a test of the material arc code.

Fixes #5710
diff --git a/dev/manual_tests/material_arc.dart b/dev/manual_tests/material_arc.dart
new file mode 100644
index 0000000..8faaf1b
--- /dev/null
+++ b/dev/manual_tests/material_arc.dart
@@ -0,0 +1,475 @@
+// 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
+  void update(DragUpdateDetails details)  {
+    onUpdate(details);
+  }
+
+  @override
+  void cancel()  {
+    onCancel();
+  }
+
+  @override
+  void end(DragEndDetails details)  {
+    onEnd(details);
+  }
+}
+
+class _IgnoreDrag extends Drag {
+}
+
+class _PointDemoPainter extends CustomPainter {
+  _PointDemoPainter({
+    Animation<double> repaint,
+    this.arc
+  }) : _repaint = repaint, super(repaint: repaint);
+
+  final MaterialPointArcTween arc;
+  Animation<double> _repaint;
+
+  void drawPoint(Canvas canvas, Point point, Color color) {
+    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)
+      drawPoint(canvas, arc.center, Colors.grey[400]);
+
+    paint
+      ..isAntiAlias = false // Work-around for github.com/flutter/flutter/issues/5720
+      ..color = Colors.green[500].withOpacity(0.25)
+      ..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);
+
+    drawPoint(canvas, arc.begin, Colors.green[500]);
+    drawPoint(canvas, arc.end, Colors.red[500]);
+
+    paint
+      ..color = Colors.green[500]
+      ..style = PaintingStyle.fill;
+    canvas.drawCircle(arc.lerp(_repaint.value), _kPointRadius, paint);
+  }
+
+  @override
+  bool hitTest(Point position) {
+    return (arc.begin - position).distanceSquared < _kTargetSlop
+        || (arc.end - position).distanceSquared < _kTargetSlop;
+  }
+
+  @override
+  bool shouldRepaint(_PointDemoPainter oldPainter) => arc != oldPainter.arc;
+}
+
+class _PointDemo extends StatefulWidget {
+  _PointDemo({ Key key, this.controller }) : super(key: key);
+
+  final AnimationController controller;
+
+  @override
+  _PointDemoState createState() => new _PointDemoState();
+}
+
+class _PointDemoState extends State<_PointDemo> {
+  final GlobalKey _painterKey = new GlobalKey();
+
+  CurvedAnimation _animation;
+  _DragTarget _dragTarget;
+  Size _screenSize;
+  Point _begin;
+  Point _end;
+
+  @override
+  void initState() {
+    super.initState();
+    _animation = new CurvedAnimation(parent: config.controller, curve: Curves.fastOutSlowIn);
+  }
+
+  @override
+  void dispose() {
+    config.controller.value = 0.0;
+    super.dispose();
+  }
+
+  Drag _handleOnStart(Point position) {
+    // 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);
+  }
+
+  void _handleDragUpdate(DragUpdateDetails details)  {
+    switch (_dragTarget) {
+      case _DragTarget.start:
+        setState(() {
+          _begin = _begin + details.delta;
+        });
+        break;
+      case _DragTarget.end:
+        setState(() {
+          _end = _end + details.delta;
+        });
+        break;
+    }
+  }
+
+  void _handleDragCancel()  {
+    _dragTarget = null;
+    config.controller.value = 0.0;
+  }
+
+  void _handleDragEnd(DragEndDetails details)  {
+    _dragTarget = null;
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final Size screenSize = MediaQuery.of(context).size;
+    if (_screenSize == null || _screenSize != screenSize) {
+      _screenSize = screenSize;
+      _begin = new Point(screenSize.width * 0.5, screenSize.height * 0.2);
+      _end = new Point(screenSize.width * 0.1, screenSize.height * 0.4);
+    }
+
+    final MaterialPointArcTween arc = new MaterialPointArcTween(begin: _begin, end: _end);
+    return new RawGestureDetector(
+      behavior: _dragTarget == null ? HitTestBehavior.deferToChild : HitTestBehavior.opaque,
+      gestures: <Type, GestureRecognizerFactory>{
+        ImmediateMultiDragGestureRecognizer: (ImmediateMultiDragGestureRecognizer recognizer) { // ignore: map_value_type_not_assignable, https://github.com/flutter/flutter/issues/5771
+          return (recognizer ??= new ImmediateMultiDragGestureRecognizer())
+            ..onStart = _handleOnStart;
+        }
+      },
+      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(
+                "Tap the refresh button to run the animation. Drag the green "
+                "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;
+  Animation<double> _repaint;
+
+  void drawPoint(Canvas canvas, Point p, Color color) {
+    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) {
+    drawRect(canvas, arc.begin, Colors.green[500]);
+    drawRect(canvas, arc.end, Colors.red[500]);
+    drawRect(canvas, arc.lerp(_repaint.value), Colors.blue[500]);
+  }
+
+  @override
+  bool hitTest(Point position) {
+    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 {
+  _RectangleDemo({ Key key, this.controller }) : super(key: key);
+
+  final AnimationController controller;
+
+  @override
+  _RectangleDemoState createState() => new _RectangleDemoState();
+}
+
+class _RectangleDemoState extends State<_RectangleDemo> {
+  final GlobalKey _painterKey = new GlobalKey();
+
+  CurvedAnimation _animation;
+  _DragTarget _dragTarget;
+  Size _screenSize;
+  Rect _begin;
+  Rect _end;
+
+  @override
+  void initState() {
+    super.initState();
+    _animation = new CurvedAnimation(parent: config.controller, curve: Curves.fastOutSlowIn);
+  }
+
+  @override
+  void dispose() {
+    config.controller.value = 0.0;
+    super.dispose();
+  }
+
+  Drag _handleOnStart(Point position) {
+    // 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);
+  }
+
+  void _handleDragUpdate(DragUpdateDetails details)  {
+    switch (_dragTarget) {
+      case _DragTarget.start:
+        setState(() {
+          _begin = _begin.shift(details.delta);
+        });
+        break;
+      case _DragTarget.end:
+        setState(() {
+          _end = _end.shift(details.delta);
+        });
+        break;
+    }
+  }
+
+  void _handleDragCancel()  {
+    _dragTarget = null;
+    config.controller.value = 0.0;
+  }
+
+  void _handleDragEnd(DragEndDetails details)  {
+    _dragTarget = null;
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final Size screenSize = MediaQuery.of(context).size;
+    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
+      );
+    }
+
+    final MaterialRectArcTween arc = new MaterialRectArcTween(begin: _begin, end: _end);
+    return new RawGestureDetector(
+      behavior: _dragTarget == null ? HitTestBehavior.deferToChild : HitTestBehavior.opaque,
+      gestures: <Type, GestureRecognizerFactory>{
+        ImmediateMultiDragGestureRecognizer: (ImmediateMultiDragGestureRecognizer recognizer) { // ignore: map_value_type_not_assignable, https://github.com/flutter/flutter/issues/5771
+          return (recognizer ??= new ImmediateMultiDragGestureRecognizer())
+            ..onStart = _handleOnStart;
+        }
+      },
+      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(
+                "Tap the refresh button to run the animation. Drag the rectangles "
+                "to change the animation's path.",
+                style: Theme.of(context).textTheme.caption.copyWith(fontSize: 16.0)
+              )
+            )
+          )
+        )
+      )
+    );
+  }
+}
+
+typedef Widget _DemoBuilder(_ArcDemo demo);
+
+class _ArcDemo {
+  _ArcDemo(String _title, this.builder, TickerProvider vsync)
+    : title = _title,
+      controller = new AnimationController(duration: const Duration(milliseconds: 500), vsync: vsync),
+      key = new GlobalKey(debugLabel: _title);
+
+  final String title;
+  final _DemoBuilder builder;
+  final AnimationController controller;
+  final GlobalKey key;
+}
+
+class AnimationDemo extends StatefulWidget {
+  AnimationDemo({ Key key }) : super(key: key);
+
+  @override
+  _AnimationDemoState createState() => new _AnimationDemoState();
+}
+
+class _AnimationDemoState extends State<AnimationDemo> with TickerProviderStateMixin {
+  static final GlobalKey<TabBarSelectionState<_ArcDemo>> _tabsKey = new GlobalKey<TabBarSelectionState<_ArcDemo>>();
+
+  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),
+    ];
+  }
+
+  Future<Null> _play() async {
+    _ArcDemo demo = _tabsKey.currentState.value;
+    await demo.controller.forward();
+    if (demo.key.currentState != null && demo.key.currentState.mounted)
+      demo.controller.reverse();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return new TabBarSelection<_ArcDemo>(
+      key: _tabsKey,
+      values: _allDemos,
+      child: new Scaffold(
+        appBar: new AppBar(
+          title: new Text('Animation'),
+          bottom: new TabBar<_ArcDemo>(
+            labels: new Map<_ArcDemo, TabLabel>.fromIterable(_allDemos, value: (_ArcDemo demo) {
+              return new TabLabel(text: demo.title);
+            })
+          )
+        ),
+        floatingActionButton: new FloatingActionButton(
+          onPressed: _play,
+          child: new Icon(Icons.refresh)
+        ),
+        body: new TabBarView<_ArcDemo>(
+          children: _allDemos.map((_ArcDemo demo) => demo.builder(demo)).toList()
+        )
+      )
+    );
+  }
+}
+
+void main() {
+  runApp(new MaterialApp(
+    home: new AnimationDemo()
+  ));
+}