使用物理模拟动画化部件
物理模拟可以让应用程序交互感觉更真实和互动。例如,您可能希望动画化一个部件,使其表现得像连接到弹簧或在重力作用下下落一样。
此示例演示了如何使用弹簧模拟将部件从拖动点移回中心。
此示例使用以下步骤:
- 设置动画控制器
- 使用手势移动部件
- 动画化部件
- 计算速度以模拟弹簧运动
步骤 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
。
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:使用手势移动部件
#拖动部件时使其移动,并向 _DraggableCardState
类添加一个 Alignment 字段:
class _DraggableCardState extends State<DraggableCard>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
Alignment _dragAlignment = Alignment.center;
添加一个 GestureDetector,它处理 onPanDown
、onPanUpdate
和 onPanEnd
回调。要调整对齐方式,请使用 MediaQuery 获取部件的大小,然后除以 2。(这将“拖动的像素”单位转换为 Align 使用的坐标。)然后,将 Align
部件的 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:动画化部件
#释放部件时,它应该弹回中心。
添加一个 Animation<Alignment>
字段和一个 _runAnimation
方法。此方法定义一个 Tween
,它在部件被拖动到的点与中心点之间进行插值。
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
部件使用 _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:计算速度以模拟弹簧运动
#最后一步是进行一些计算,以计算部件在拖动完成后的速度。这样,部件就可以以该速度真实地继续移动,然后再弹回。(_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:
/// 计算并运行 [SpringSimulation]。
void _runAnimation(Offset pixelsPerSecond, Size size) {
_animation = _controller.drive(
AlignmentTween(
begin: _dragAlignment,
end: Alignment.center,
),
);
// 计算相对于动画控制器使用的单位区间 [0,1] 的速度。
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,
),
),
);
}
}
/// 一个可拖动的卡片,当它被释放时会返回到 [Alignment.center]。
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;
/// 卡片在被拖动或正在动画化时的对齐方式。
///
/// 当卡片被拖动时,此值设置为在 GestureDetector onPanUpdate 回调中计算的值。如果动画正在运行,则此值设置为 [_animation] 的值。
Alignment _dragAlignment = Alignment.center;
late Animation<Alignment> _animation;
/// 计算并运行 [SpringSimulation]。
void _runAnimation(Offset pixelsPerSecond, Size size) {
_animation = _controller.drive(
AlignmentTween(
begin: _dragAlignment,
end: Alignment.center,
),
);
// 计算相对于动画控制器使用的单位区间 [0,1] 的速度。
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 的最新稳定版本。页面最后更新于 2025-01-30。 查看源代码 或 报告问题。