注册

Flutter 手指拖动实现弹簧动画交互

物理模拟可以让应用程序的交互感觉逼真和互动,例如,你可能希望为一个 Widget 设置动画,使其看起来像是附着在弹簧上或是重力下落。本文章实现了演示了如何使用弹簧模拟将小部件从拖动的点移回中心。
实现步骤如下



  1. 设置动画控制器
  2. 使用手势移动小部件
  3. 为小部件制作动画
  4. 计算速度以模拟弹簧运动



1 创建一个动画控制器


首页创建一个测试使用的Demo页面


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,
),
),
);
}
}

DraggableCard 是自定义的一个 StatefulWidget,代码如下:


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 中创建一个动画控制器,并在页面销毁的时候释放动画控制器,代码如下:


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();
}

@override
Widget build(BuildContext context) {
return Align(
child: Card(
child: widget.child,
),
);
}
}

SingleTickerProviderStateMixin是用来在StatefulWidget中管理单个AnimationController的Mixin;它提供了一个TickerProvider,用于将AnimationController与TickerProviderStateMixin一起使用。


TickerProviderStateMixin提供了一个Ticker,它可以在每个frame中调用AnimationController的方法,这使得AnimationController可以在每个frame中更新动画。


2 使用手势移动Widget


在 _DraggableCardState 中,结合使用 Alignment 与 GestureDetector,代码如下:


class _DraggableCardState extends State<DraggableCard>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
Alignment _dragAlignment = const Alignment(0, 0);
@override
Widget build(BuildContext context) {
var size = MediaQuery.of(context).size;
return GestureDetector(
onPanDown: (details) {},
onPanUpdate: (details) {
_dragAlignment += Alignment(
details.delta.dx / (size.width / 2),
details.delta.dy / (size.height / 2),
);
setState(() {
});
},
onPanEnd: (details) {},
child: Align(
alignment: _dragAlignment,
child: Card(
child: widget.child,
),
),
);
}

GestureDetector用来检测手势,例如轻触、滑动、拖动等,可以用来实现各种交互效果。


Alignment用于控制子widget在父widget中的位置。可以通过Alignment的构造函数来指定子widget相对于父widget的位置,如Alignment.topLeft表示子widget位于父widget的左上角。也可以通过FractionalOffset来指定子widget相对于父widget的位置,如FractionalOffset(0.5, 0.5)表示子widget位于父widget的中心。Alignment还可以与Stack一起使用,实现多个子widget的定位。
在这里插入图片描述


3 创建一个动画Widget


我们需要实现,当手指抬起时,被移动的 Widget 动画的方式弹回去。


在这里需要一个 Animation ,再定义一个 runAnimation 方法,同时为 第一步创建的动画控制器添加一个更新监听。


class _DraggableCardState extends State<DraggableCard>
with SingleTickerProviderStateMixin {

late AnimationController _controller;
late Animation<Alignment> _animation;
@override
void initState() {
super.initState();
_controller =
AnimationController(vsync: this, duration: const Duration(seconds: 1));

_controller.addListener(() {
setState(() {
_dragAlignment = _animation.value;
});
});
}

void _runAnimation() {
_animation = _controller.drive(
AlignmentTween(
begin: _dragAlignment,
end: Alignment.center,
),
);
_controller.reset();
_controller.forward();
}
}

然后在手指抬起的时候,执行动画,将被移动的 Widget (如这里的图片)以动画的方式移动回原位:


@override
Widget build(BuildContext context) {
var size = MediaQuery.of(context).size;
return GestureDetector(
onPanDown: (details) {
_controller.stop();
},
onPanUpdate: (details) {
_dragAlignment += Alignment(
details.delta.dx / (size.width / 2),
details.delta.dy / (size.height / 2),
);
setState(() {

});
},
onPanEnd: (details) {
_runAnimation();
},
child: Align(
alignment: _dragAlignment,
child: Card(
child: widget.child,
),
),
);
}

在这里插入图片描述


4 计算速度以模拟弹簧运动


最后一步是做一些数学运算,计算小部件完成拖动后的速度。这是为了使小部件在被拍回之前能够以这种速度逼真地继续。(_runAnimation方法已经通过设置动画的开始和结束对齐来设置方向。)


导入包如下:


import 'package:flutter/physics.dart';

onPanEnd回调提供了一个DragEndDetails对象。此对象提供指针停止接触屏幕时的速度。速度以像素每秒为单位,但Align小部件不使用像素。它使用介于[-1.0,-1.0]和[1.0,1.0]之间的坐标值,其中[0.0,0.0]表示中心。步骤2中计算的大小用于将像素转换为该范围内的坐标值。


然后修改 runAnimation 执行动画函数如下:


void _runAnimation(Offset pixelsPerSecond, Size size) {
_animation = _controller.drive(
AlignmentTween(
begin: _dragAlignment,
end: Alignment.center,
),
);

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,
);
//SpringSimulation用来模拟一个弹簧的运动,可以用于创建具有弹性的动画效果。
final simulation = SpringSimulation(spring, 0, 1, -unitVelocity);

_controller.animateWith(simulation);
}

然后在手指抬起的时候调用


onPanEnd: (details) {
_runAnimation(details.velocity.pixelsPerSecond, size);
},

作者:早起的年轻人
链接:https://juejin.cn/post/7224433107776389157
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册