Widget 的物理模擬動畫效果
物理模擬能夠讓應用富有真實感和更好的互動性。例如,你可能會為一個 widget 新增動畫,讓它看起來就像安著彈簧,或是在隨重力下落。
這個指南示範了如何將 widget 從拖動的點移回到中心,並使用彈簧模擬效果。
這個示範將進行下面幾步:
-
建立一個動畫控制器
-
使用手勢移動 widget
-
對 widget 進行動畫
-
計算速度以模擬彈跳運動
第一步:建立一個動畫控制器
首先,建立一個叫做 DraggableCard
的 stateful widget:
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
|
}
|
第二步:使用手勢移動 widget
讓 widget 可以被拖拽,併為 _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
來獲得 widget 的大小,然後除以 2。(這會將「拖動的畫素」單位轉為 Align 使用的坐標。)然後,將 Align
widget 的 alignmnt
屬性設為 _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 進行動畫
當一個 widget 被釋放,它應該就會彈回中心。
新增一個 Animation<Alignment>
,以及 _runAnimation
方法。此方法定義了一個 Tween
,它在 widget 被拖動到的點之間插入到中心點。
@@ -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
widget 使用 _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(
|
第四步:計算速度以模擬彈跳運動
最後一步時做一些簡單的數學計算,計算小部件被拖動完成之後的速度。這樣小部件在被快速恢復之前實際上以該速度繼續運動。(_runAnimation
方法已經透過設定動畫的開始和結束對齊方式來設定方向。)
首先,引入 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
用於將畫素轉換為該範圍內的坐標值。
最後,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,
),
),
);
}
}