物理模拟能够让应用富有真实感和更好的交互性。例如,你可能会为一个 widget 添加动画,让它看起来就像安着弹簧,或是在随重力下落。
这个指南演示了如何将 widget 从拖动的点移回到中心,并使用弹簧模拟效果。
这个演示将进行下面几步:
- 创建一个动画控制器
- 使用手势移动 widget
- 对 widget 进行动画
- 计算速度以模拟弹跳运动
第一步:创建一个动画控制器
首先,创建一个叫做 DraggableCard
的 stateful widget:
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
import 'package:flutter/material.dart'; void main() { runApp(const MaterialApp(home: PhysicsCardDragDemo())); } class PhysicsCardDragDemo extends StatelessWidget { const PhysicsCardDragDemo({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(), body: const DraggableCard( child: FlutterLogo( size: 128, ), ), ); } } class DraggableCard extends StatefulWidget { const DraggableCard({required this.child, Key? key}) : super(key: key); final Widget child; @override _DraggableCardState createState() => _DraggableCardState(); } class _DraggableCardState extends State<DraggableCard> { @override void initState() { super.initState(); } @override void dispose() { super.dispose(); } @override Widget build(BuildContext context) { return Align( child: Card( child: widget.child, ), ); } } |
让 _DraggableCardState
类继承至 SingleTickerProviderStateMixin。然后在 initState
中构造一个 AnimationController,并将其 vsync
属性设为 this
。
提示
继承的
SingleTickerProviderStateMixin
让 state 对象为AnimationController
提供了TickerProvider
的能力。要获得更多信息,请查看 TickerProvider 文档。
第二步:使用手势移动 widget
让 widget 可以被拖拽,并为 _DraggableCardState
类添加一个 Alignment 范围。
添加一个 GestureDetector 来捕获 onPanDown
、onPanUpdate
,以及 onPanEnd
回调。为了调整对齐方式,请使用 MediaQuery 来获得 widget 的大小,然后除以 2。(这会将「拖动的像素」单位转为 Align 使用的坐标。)然后,将 Align
widget 的 alignmnt
属性设为 _dragAlignment
。
第三步:对 widget 进行动画
当一个 widget 被释放,它应该就会弹回中心。
添加一个 Animation<Alignment>
,以及 _runAnimation
方法。此方法定义了一个 Tween
,它在 widget 被拖动到的点之间插入到中心点。
1 2 3 4 5 6 7 8 9 10 |
void _runAnimation() { _animation = _controller.drive( AlignmentTween( begin: _dragAlignment, end: Alignment.center, ), ); _controller.reset(); _controller.forward(); } |
接下来,当 AnimationController
产生一个值时,更新 _dragAlignment
:
下一步,让 Align
widget 使用 _dragAlignment
字段:
1 2 3 4 5 6 |
child: Align( alignment: _dragAlignment, child: Card( child: widget.child, ), ), |
最后,更新 GestureDetector
来管理动画控制器:
第四步:计算速度以模拟弹跳运动
最后一步时做一些简单的数学计算,计算小部件被拖动完成之后的速度。这样小部件在被快速恢复之前实际上以该速度继续运动。(_runAnimation
方法已经通过设置动画的开始和结束对齐方式来设置方向。)
首先,引入 physics
这个 package:
1 |
import 'package:flutter/physics.dart'; |
onPanEnd
回调提供了一个 DragEndDetails 对象。此对象提供指针停止接触屏幕时的速度。速度以每秒像素为单位,但 Align
widget 不使用像素。它使用 [-1.0,-1.0] 和 [1.0,1.0] 之间的坐标值,其中 [0.0,0.0] 表示中心。在步骤 2 中计算的 size
用于将像素转换为该范围内的坐标值。
最后,AnimationController
有一个 animateWith()
方法可以产生 SpringSimulation:
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 |
/// Calculates and runs a [SpringSimulation]. void _runAnimation(Offset pixelsPerSecond, Size size) { _animation = _controller.drive( AlignmentTween( begin: _dragAlignment, end: Alignment.center, ), ); // Calculate the velocity relative to the unit interval, [0,1], // used by the animation controller. final unitsPerSecondX = pixelsPerSecond.dx / size.width; final unitsPerSecondY = pixelsPerSecond.dy / size.height; final unitsPerSecond = Offset(unitsPerSecondX, unitsPerSecondY); final unitVelocity = unitsPerSecond.distance; const spring = SpringDescription( mass: 30, stiffness: 1, damping: 1, ); final simulation = SpringSimulation(spring, 0, 1, -unitVelocity); _controller.animateWith(simulation); } |
不要忘记调用 _runAnimation()
,并传入速度和大小:
1 2 3 |
onPanEnd: (details) { _runAnimation(details.velocity.pixelsPerSecond, size); }, |
提示
既然动画控制器使用了模拟,就不再需要指定
duration
参数。
交互式样例
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
import 'package:flutter/material.dart'; import 'package:flutter/physics.dart'; void main() { runApp(const MaterialApp(home: PhysicsCardDragDemo())); } class PhysicsCardDragDemo extends StatelessWidget { const PhysicsCardDragDemo({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(), body: const DraggableCard( child: FlutterLogo( size: 128, ), ), ); } } /// A draggable card that moves back to [Alignment.center] when it's /// released. class DraggableCard extends StatefulWidget { const DraggableCard({required this.child, Key? key}) : super(key: key); final Widget child; @override _DraggableCardState createState() => _DraggableCardState(); } class _DraggableCardState extends State<DraggableCard> with SingleTickerProviderStateMixin { late AnimationController _controller; /// The alignment of the card as it is dragged or being animated. /// /// While the card is being dragged, this value is set to the values computed /// in the GestureDetector onPanUpdate callback. If the animation is running, /// this value is set to the value of the [_animation]. Alignment _dragAlignment = Alignment.center; late Animation<Alignment> _animation; /// Calculates and runs a [SpringSimulation]. void _runAnimation(Offset pixelsPerSecond, Size size) { _animation = _controller.drive( AlignmentTween( begin: _dragAlignment, end: Alignment.center, ), ); // Calculate the velocity relative to the unit interval, [0,1], // used by the animation controller. final unitsPerSecondX = pixelsPerSecond.dx / size.width; final unitsPerSecondY = pixelsPerSecond.dy / size.height; final unitsPerSecond = Offset(unitsPerSecondX, unitsPerSecondY); final unitVelocity = unitsPerSecond.distance; const spring = SpringDescription( mass: 30, stiffness: 1, damping: 1, ); final simulation = SpringSimulation(spring, 0, 1, -unitVelocity); _controller.animateWith(simulation); } @override void initState() { super.initState(); _controller = AnimationController(vsync: this); _controller.addListener(() { setState(() { _dragAlignment = _animation.value; }); }); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final size = MediaQuery.of(context).size; return GestureDetector( onPanDown: (details) { _controller.stop(); }, onPanUpdate: (details) { setState(() { _dragAlignment += Alignment( details.delta.dx / (size.width / 2), details.delta.dy / (size.height / 2), ); }); }, onPanEnd: (details) { _runAnimation(details.velocity.pixelsPerSecond, size); }, child: Align( alignment: _dragAlignment, child: Card( child: widget.child, ), ), ); } } |
上述代码可以在 https://www.dartpad.dev/ 进行在线测试。