blob: 8eb731392432241c8b5c08f215d55ed0e1b3784b [file] [log] [blame] [edit]
// Copyright 2014 The Flutter 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 '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,
required this.arc,
}) : _repaint = repaint, super(repaint: repaint);
final MaterialPointArcTween arc;
final Animation<double>? _repaint;
void drawPoint(Canvas canvas, Offset point, Color color) {
final Paint paint = 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 = Paint();
if (arc.center != null) {
drawPoint(canvas, arc.center!, Colors.grey.shade400);
}
paint
..isAntiAlias = false // Work-around for github.com/flutter/flutter/issues/5720
..color = Colors.green.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);
drawPoint(canvas, arc.end!, Colors.red);
paint
..color = Colors.green
..style = PaintingStyle.fill;
canvas.drawCircle(arc.lerp(_repaint!.value), _kPointRadius, paint);
}
@override
bool hitTest(Offset position) {
return (arc.begin! - position).distanceSquared < _kTargetSlop
|| (arc.end! - position).distanceSquared < _kTargetSlop;
}
@override
bool shouldRepaint(_PointDemoPainter oldPainter) => arc != oldPainter.arc;
}
class _PointDemo extends StatefulWidget {
const _PointDemo({ super.key, required this.controller });
final AnimationController controller;
@override
_PointDemoState createState() => _PointDemoState();
}
class _PointDemoState extends State<_PointDemo> {
final GlobalKey _painterKey = GlobalKey();
CurvedAnimation? _animation;
_DragTarget? _dragTarget;
Size? _screenSize;
Offset? _begin;
Offset? _end;
@override
void initState() {
super.initState();
_animation = CurvedAnimation(parent: widget.controller, curve: Curves.fastOutSlowIn);
}
@override
void dispose() {
widget.controller.value = 0.0;
super.dispose();
}
Drag _handleOnStart(Offset position) {
// TODO(hansmuller): allow the user to drag both points at the same time.
if (_dragTarget != null) {
return _IgnoreDrag();
}
final RenderBox? box = _painterKey.currentContext!.findRenderObject() as RenderBox?;
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 _DragHandler(_handleDragUpdate, _handleDragCancel, _handleDragEnd);
}
void _handleDragUpdate(DragUpdateDetails details) {
switch (_dragTarget!) {
case _DragTarget.start:
setState(() {
_begin = _begin! + details.delta;
});
case _DragTarget.end:
setState(() {
_end = _end! + details.delta;
});
}
}
void _handleDragCancel() {
_dragTarget = null;
widget.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 = Offset(screenSize.width * 0.5, screenSize.height * 0.2);
_end = Offset(screenSize.width * 0.1, screenSize.height * 0.4);
}
final MaterialPointArcTween arc = MaterialPointArcTween(begin: _begin, end: _end);
return RawGestureDetector(
behavior: _dragTarget == null ? HitTestBehavior.deferToChild : HitTestBehavior.opaque,
gestures: <Type, GestureRecognizerFactory>{
ImmediateMultiDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<ImmediateMultiDragGestureRecognizer>(
() => ImmediateMultiDragGestureRecognizer(),
(ImmediateMultiDragGestureRecognizer instance) {
instance.onStart = _handleOnStart;
},
),
},
child: ClipRect(
child: CustomPaint(
key: _painterKey,
foregroundPainter: _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: IgnorePointer(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: 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.bodySmall?.copyWith(fontSize: 16.0),
),
),
),
),
),
);
}
}
class _RectangleDemoPainter extends CustomPainter {
_RectangleDemoPainter({
required Animation<double> repaint,
required this.arc,
}) : _repaint = repaint, super(repaint: repaint);
final MaterialRectArcTween arc;
final Animation<double> _repaint;
void drawPoint(Canvas canvas, Offset p, Color color) {
final Paint paint = 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 = 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);
drawRect(canvas, arc.end!, Colors.red);
drawRect(canvas, arc.lerp(_repaint.value), Colors.blue);
}
@override
bool hitTest(Offset 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 {
const _RectangleDemo({ super.key, required this.controller });
final AnimationController controller;
@override
_RectangleDemoState createState() => _RectangleDemoState();
}
class _RectangleDemoState extends State<_RectangleDemo> {
final GlobalKey _painterKey = GlobalKey();
late final CurvedAnimation _animation = CurvedAnimation(parent: widget.controller, curve: Curves.fastOutSlowIn);
_DragTarget? _dragTarget;
Size? _screenSize;
Rect? _begin;
Rect? _end;
@override
void dispose() {
widget.controller.value = 0.0;
super.dispose();
}
Drag _handleOnStart(Offset position) {
// TODO(hansmuller): allow the user to drag both points at the same time.
if (_dragTarget != null) {
return _IgnoreDrag();
}
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;
setState(() {
if (startOffset < endOffset && startOffset < _kTargetSlop) {
_dragTarget = _DragTarget.start;
} else if (endOffset < _kTargetSlop) {
_dragTarget = _DragTarget.end;
} else {
_dragTarget = null;
}
});
return _DragHandler(_handleDragUpdate, _handleDragCancel, _handleDragEnd);
}
void _handleDragUpdate(DragUpdateDetails details) {
switch (_dragTarget!) {
case _DragTarget.start:
setState(() {
_begin = _begin?.shift(details.delta);
});
case _DragTarget.end:
setState(() {
_end = _end?.shift(details.delta);
});
}
}
void _handleDragCancel() {
_dragTarget = null;
widget.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 = Rect.fromLTWH(
screenSize.width * 0.5, screenSize.height * 0.2,
screenSize.width * 0.4, screenSize.height * 0.2,
);
_end = Rect.fromLTWH(
screenSize.width * 0.1, screenSize.height * 0.4,
screenSize.width * 0.3, screenSize.height * 0.3,
);
}
final MaterialRectArcTween arc = MaterialRectArcTween(begin: _begin, end: _end);
return RawGestureDetector(
behavior: _dragTarget == null ? HitTestBehavior.deferToChild : HitTestBehavior.opaque,
gestures: <Type, GestureRecognizerFactory>{
ImmediateMultiDragGestureRecognizer: GestureRecognizerFactoryWithHandlers<ImmediateMultiDragGestureRecognizer>(
() => ImmediateMultiDragGestureRecognizer(),
(ImmediateMultiDragGestureRecognizer instance) {
instance.onStart = _handleOnStart;
},
),
},
child: ClipRect(
child: CustomPaint(
key: _painterKey,
foregroundPainter: _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: IgnorePointer(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'Tap the refresh button to run the animation. Drag the rectangles '
"to change the animation's path.",
style: Theme.of(context).textTheme.bodySmall!.copyWith(fontSize: 16.0),
),
),
),
),
),
);
}
}
typedef _DemoBuilder = Widget Function(_ArcDemo demo);
class _ArcDemo {
_ArcDemo(this.title, this.builder, TickerProvider vsync)
: controller = AnimationController(duration: const Duration(milliseconds: 500), vsync: vsync),
key = GlobalKey(debugLabel: title);
final String title;
final _DemoBuilder builder;
final AnimationController controller;
final GlobalKey key;
}
class AnimationDemo extends StatefulWidget {
const AnimationDemo({ super.key });
@override
State<AnimationDemo> createState() => _AnimationDemoState();
}
class _AnimationDemoState extends State<AnimationDemo> with TickerProviderStateMixin {
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),
];
Future<void> _play(_ArcDemo demo) async {
await demo.controller.forward();
if (demo.key.currentState != null && demo.key.currentState!.mounted) {
demo.controller.reverse();
}
}
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: _allDemos.length,
child: Scaffold(
appBar: AppBar(
title: const Text('Animation'),
bottom: TabBar(
tabs: _allDemos.map<Tab>((_ArcDemo demo) => Tab(text: demo.title)).toList(),
),
),
floatingActionButton: Builder(
builder: (BuildContext context) {
return FloatingActionButton(
child: const Icon(Icons.refresh),
onPressed: () {
_play(_allDemos[DefaultTabController.of(context).index]);
},
);
},
),
body: TabBarView(
children: _allDemos.map<Widget>((_ArcDemo demo) => demo.builder(demo)).toList(),
),
),
);
}
}
void main() {
runApp(const MaterialApp(
home: AnimationDemo(),
));
}