使用物理模擬來製作 widget 動畫
物理模擬可以使應用程式的互動感覺真實且具有互動性。例如,您可能希望製作 widget 動畫,使其表現得像是連接到彈簧或以重力墜落。
這個範例示範如何使用彈簧模擬將 widget 從拖曳點移回中心。
這個範例使用以下步驟
- 設定動畫控制器
- 使用手勢移動 widget
- 製作 widget 動畫
- 計算速度以模擬彈簧運動
步驟 1:設定動畫控制器
#從一個名為 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
。
class _DraggableCardState extends State<DraggableCard> {
class _DraggableCardState extends State<DraggableCard>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller =
AnimationController(vsync: this, duration: const Duration(seconds: 1));
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
步驟 2:使用手勢移動 Widget
#讓 widget 在拖曳時移動,並將一個 Alignment 欄位新增到 _DraggableCardState
類別
class _DraggableCardState extends State<DraggableCard>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
Alignment _dragAlignment = Alignment.center;
新增一個處理 onPanDown
、onPanUpdate
和 onPanEnd
回呼的 GestureDetector。要調整對齊方式,請使用 MediaQuery 來取得 widget 的大小,然後除以 2。(這會將「拖曳的像素」單位轉換為 Align 使用的座標。)然後,將 Align
widget 的 alignment
設定為 _dragAlignment
@override
Widget build(BuildContext context) {
return Align(
child: Card(
child: widget.child,
var size = MediaQuery.of(context).size;
return GestureDetector(
onPanDown: (details) {},
onPanUpdate: (details) {
setState(() {
_dragAlignment += Alignment(
details.delta.dx / (size.width / 2),
details.delta.dy / (size.height / 2),
);
});
},
onPanEnd: (details) {},
child: Align(
alignment: _dragAlignment,
child: Card(
child: widget.child,
),
),
);
}
步驟 3:為 Widget 添加動畫效果
#當 widget 被釋放時,它應該彈回中心。
新增一個 Animation<Alignment>
欄位和一個 _runAnimation
方法。此方法定義一個 Tween
,它在 widget 被拖曳到的點與中心點之間進行插值。
class _DraggableCardState extends State<DraggableCard>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<Alignment> _animation;
Alignment _dragAlignment = Alignment.center;
void _runAnimation() {
_animation = _controller.drive(
AlignmentTween(
begin: _dragAlignment,
end: Alignment.center,
),
);
_controller.reset();
_controller.forward();
}
接下來,當 AnimationController
產生一個值時,更新 _dragAlignment
@override
void initState() {
super.initState();
_controller =
AnimationController(vsync: this, duration: const Duration(seconds: 1));
_controller.addListener(() {
setState(() {
_dragAlignment = _animation.value;
});
});
}
接下來,讓 Align
widget 使用 _dragAlignment
欄位
child: Align(
alignment: _dragAlignment,
child: Card(
child: widget.child,
),
),
最後,更新 GestureDetector
以管理動畫控制器
return GestureDetector(
onPanDown: (details) {},
onPanDown: (details) {
_controller.stop();
},
onPanUpdate: (details) {
// ...
},
onPanEnd: (details) {},
onPanEnd: (details) {
_runAnimation();
},
child: Align(
步驟 4:計算速度以模擬彈簧運動
#最後一步是做一些數學計算,以計算 widget 在拖曳完成後的速度。這樣做是為了讓 widget 在彈回之前,能夠以真實的速度繼續移動。(_runAnimation
方法已經透過設定動畫的起始和結束對齊方式來設定方向。)
首先,匯入 physics
套件
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,
),
),
);
}
}
除非另有說明,否則本網站上的文件反映了 Flutter 的最新穩定版本。頁面最後更新於 2024-08-16。 檢視原始碼 或 回報問題。