Widget 的物理模拟动画效果
物理模拟能够让应用富有真实感和更好的交互性。例如,你可能会为一个 widget 添加动画,让它看起来就像安着弹簧,或是在随重力下落。
Physics simulations can make app interactions feel realistic and interactive. For example, you might want to animate a widget to act as if it were attached to a spring or falling with gravity.
这个指南演示了如何将 widget 从拖动的点移回到中心,并使用弹簧模拟效果。
This recipe demonstrates how to move a widget from a dragged point back to the center using a spring simulation.
这个演示将进行下面几步:
This recipe uses these steps:
-
创建一个动画控制器
Set up an animation controller
-
使用手势移动 widget
Move the widget using gestures
-
对 widget 进行动画
Animate the widget
-
计算速度以模拟弹跳运动
Calculate the velocity to simulate a springing motion
第一步:创建一个动画控制器
Step 1: Set up an animation controller
首先,创建一个叫做 DraggableCard
的 stateful widget:
Start with a stateful widget called DraggableCard
:
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
。
Make the _DraggableCardState
class extend from
SingleTickerProviderStateMixin.
Then construct an AnimationController in
initState
and set vsync
to this
.
@@ -29,14 +29,20 @@
|
|
29
29
|
_DraggableCardState 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
|
}
|
第二步:使用手势移动 widget
Step 2: Move the widget using gestures
让 widget 可以被拖拽,并为 _DraggableCardState
类添加一个 Alignment 范围。
Make the widget move when it’s dragged, and add an Alignment field to the
_DraggableCardState
class:
@@ -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
来获得 widget 的大小,然后除以 2。(这会将「拖动的像素」单位转为 Align 使用的坐标。)然后,将 Align
widget 的 alignmnt
属性设为 _dragAlignment
。
Add a GestureDetector that handles the onPanDown
, onPanUpdate
, and
onPanEnd
callbacks. To adjust the alignment, use a MediaQuery to get the
size of the widget, and divide by 2. (This converts units of “pixels dragged” to
coordinates that Align uses.) Then, set the Align
widget’s alignment
to
_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
|
}
|
第三步:对 widget 进行动画
Step 3: Animate the widget
当一个 widget 被释放,它应该就会弹回中心。
When the widget is released, it should spring back to the center.
添加一个 Animation<Alignment>
,以及 _runAnimation
方法。此方法定义了一个 Tween
,它在 widget 被拖动到的点之间插入到中心点。
Add an Animation<Alignment>
field and an _runAnimation
method. This
method defines a Tween
that interpolates between the point the widget was
dragged to, to the point in the center.
@@ -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
:
Next, update _dragAlignment
when the AnimationController
produces a
value:
@@ -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
widget 使用 _dragAlignment
字段:
Next, make the Align
widget use the _dragAlignment
field:
child: Align( alignment: _dragAlignment, child: Card( child: widget.child, ), ),
最后,更新 GestureDetector
来管理动画控制器:
Finally, update the GestureDetector
to manage the animation controller:
@@ -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(
|
第四步:计算速度以模拟弹跳运动
Step 4: Calculate the velocity to simulate a springing motion
最后一步时做一些简单的数学计算,计算小部件被拖动完成之后的速度。这样小部件在被快速恢复之前实际上以该速度继续运动。(_runAnimation
方法已经通过设置动画的开始和结束对齐方式来设置方向。)
The last step is to do a little math, to calculate the velocity of the widget
after it’s finished being dragged. This is so that the widget realistically
continues at that speed before being snapped back. (The _runAnimation
method
already sets the direction by setting the animation’s start and end alignment.)
首先,引入 physics
这个 package:
First, import the physics
package:
import 'package:flutter/physics.dart';
onPanEnd
回调提供了一个 DragEndDetails 对象。此对象提供指针停止接触屏幕时的速度。速度以每秒像素为单位,但 Align
widget 不使用像素。它使用 [-1.0,-1.0] 和 [1.0,1.0] 之间的坐标值,其中 [0.0,0.0] 表示中心。在步骤 2 中计算的 size
用于将像素转换为该范围内的坐标值。
The onPanEnd
callback provides a DragEndDetails object. This object
provides the velocity of the pointer when it stopped contacting the screen. The
velocity is in pixels per second, but the Align
widget doesn’t use pixels. It
uses coordinate values between [-1.0, -1.0] and [1.0, 1.0], where [0.0, 0.0]
represents the center. The size
calculated in step 2 is used to convert pixels
to coordinate values in this range.
最后,AnimationController
有一个 animateWith()
方法可以产生 SpringSimulation:
Finally, AnimationController
has an animateWith()
method that can be given a
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()
,并传入速度和大小:
Don’t forget to call _runAnimation()
with the velocity and size:
onPanEnd: (details) { _runAnimation(details.velocity.pixelsPerSecond, size); },
交互式样例
Interactive Example
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,
),
),
);
}
}