使用物理模拟为小部件设置动画
物理模拟可以使应用程序交互感觉真实且具有交互性。例如,您可能希望为小部件设置动画,使其表现得像连接到弹簧或在重力作用下坠落一样。
此食谱演示了如何使用弹簧模拟将小部件从拖动点移动回中心。
此食谱使用以下步骤
- 设置动画控制器
- 使用手势移动小部件
- 为小部件设置动画
- 计算模拟弹簧运动的速度
步骤 1:设置动画控制器
#从名为 DraggableCard
的有状态小部件开始
import 'package:flutter/material.dart';
void main() {
runApp(const MaterialApp(home: PhysicsCardDragDemo()));
}
class PhysicsCardDragDemo extends StatelessWidget {
const PhysicsCardDragDemo({super.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, super.key});
final Widget child;
@override
State<DraggableCard> 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
。
@@ -29,14 +29,20 @@ | |
29 29 | State<DraggableCard> createState() => _DraggableCardState(); |
30 30 | } |
31 | - class _DraggableCardState extends State<DraggableCard> |
31 | + class _DraggableCardState extends State<DraggableCard> |
32 | + with SingleTickerProviderStateMixin { |
33 | + late AnimationController _controller; |
34 | + |
32 35 | @override |
33 36 | void initState() { |
34 37 | super.initState(); |
38 | + _controller = |
39 | + AnimationController(vsync: this, duration: const Duration(seconds: 1)); |
35 40 | } |
36 41 | @override |
37 42 | void dispose() { |
43 | + _controller.dispose(); |
38 44 | super.dispose(); |
39 45 | } |
步骤 2:使用手势移动小部件
#当小部件被拖动时使其移动,并在 _DraggableCardState
类中添加一个 Alignment 字段
@@ -1,3 +1,4 @@ | |
1 1 | class _DraggableCardState extends State<DraggableCard> |
2 2 | with SingleTickerProviderStateMixin { |
3 3 | late AnimationController _controller; |
4 | + Alignment _dragAlignment = Alignment.center; |
添加一个 GestureDetector 来处理 onPanDown
、onPanUpdate
和 onPanEnd
回调。要调整对齐方式,请使用 MediaQuery 获取小部件的大小,并除以 2。(这将“拖动像素”单位转换为 Align 使用的坐标。)然后,将 Align
小部件的 alignment
设置为 _dragAlignment
@@ -1,8 +1,22 @@ | |
1 1 | @override |
2 2 | Widget build(BuildContext context) { |
3 | - |
4 | - |
5 | - |
3 | + var size = MediaQuery.of(context).size; |
4 | + return GestureDetector( |
5 | + onPanDown: (details) {}, |
6 | + onPanUpdate: (details) { |
7 | + setState(() { |
8 | + _dragAlignment += Alignment( |
9 | + details.delta.dx / (size.width / 2), |
10 | + details.delta.dy / (size.height / 2), |
11 | + ); |
12 | + }); |
13 | + }, |
14 | + onPanEnd: (details) {}, |
15 | + child: Align( |
16 | + alignment: _dragAlignment, |
17 | + child: Card( |
18 | + child: widget.child, |
19 | + ), |
6 20 | ), |
7 21 | ); |
8 22 | } |
步骤 3:为小部件设置动画
#当小部件被释放时,它应该弹回中心。
添加一个Animation<Alignment>
字段和一个_runAnimation
方法。此方法定义一个Tween
,它在小部件被拖动到的点和中心点之间进行插值。
@@ -1,4 +1,5 @@ | |
1 1 | class _DraggableCardState extends State<DraggableCard> |
2 2 | with SingleTickerProviderStateMixin { |
3 3 | late AnimationController _controller; |
4 | + late Animation<Alignment> _animation; |
4 5 | Alignment _dragAlignment = Alignment.center; |
void _runAnimation() {
_animation = _controller.drive(
AlignmentTween(
begin: _dragAlignment,
end: Alignment.center,
),
);
_controller.reset();
_controller.forward();
}
接下来,当AnimationController
产生一个值时,更新_dragAlignment
@@ -3,4 +3,9 @@ | |
3 3 | super.initState(); |
4 4 | _controller = |
5 5 | AnimationController(vsync: this, duration: const Duration(seconds: 1)); |
6 | + _controller.addListener(() { |
7 | + setState(() { |
8 | + _dragAlignment = _animation.value; |
9 | + }); |
10 | + }); |
6 11 | } |
接下来,让Align
小部件使用_dragAlignment
字段
child: Align(
alignment: _dragAlignment,
child: Card(
child: widget.child,
),
),
最后,更新GestureDetector
来管理动画控制器
@@ -1,5 +1,7 @@ | |
1 1 | return GestureDetector( |
2 | - onPanDown: (details) { |
2 | + onPanDown: (details) { |
3 | + _controller.stop(); |
4 | + }, |
3 5 | onPanUpdate: (details) { |
4 6 | setState(() { |
5 7 | _dragAlignment += Alignment( |
@@ -8,7 +10,9 @@ | |
8 10 | ); |
9 11 | }); |
10 12 | }, |
11 | - onPanEnd: (details) { |
13 | + onPanEnd: (details) { |
14 | + _runAnimation(); |
15 | + }, |
12 16 | child: Align( |
13 17 | alignment: _dragAlignment, |
14 18 | child: Card( |
步骤 4:计算速度以模拟弹簧运动
#最后一步是做一些数学运算,以计算小部件在拖动完成后时的速度。这样,小部件就可以以该速度真实地继续移动,然后再弹回。(_runAnimation
方法已经通过设置动画的开始和结束对齐方式来设置方向。)
首先,导入physics
包
import 'package:flutter/physics.dart';
onPanEnd
回调提供了一个DragEndDetails 对象。此对象提供了指针停止接触屏幕时的速度。速度以像素/秒为单位,但Align
小部件不使用像素。它使用[-1.0, -1.0] 和 [1.0, 1.0] 之间的坐标值,其中 [0.0, 0.0] 表示中心。在步骤 2 中计算的size
用于将像素转换为此范围内的坐标值。
最后,AnimationController
有一个 animateWith()
方法,可以接受一个 SpringSimulation
/// 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()
onPanEnd: (details) {
_runAnimation(details.velocity.pixelsPerSecond, size);
},
交互式示例
#import 'package:flutter/material.dart';
import 'package:flutter/physics.dart';
void main() {
runApp(const MaterialApp(home: PhysicsCardDragDemo()));
}
class PhysicsCardDragDemo extends StatelessWidget {
const PhysicsCardDragDemo({super.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, super.key});
final Widget child;
@override
State<DraggableCard> 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,
),
),
);
}
}