Flutter动画之粒子精讲
本文所有源码见
github/flutter_journey
1.何为动画
image1.1:动画说明
见字如面,会动的画面。画面连续渲染,当速度快到一定程度,大脑就会呈现动感
1).何为运动:视觉上看是一个物体在不同的时间轴上表现出不同的物理位置
2).位移 = 初位移 + 速度 * 时间 小学生的知识不多说
3).速度 = 初速度 + 加速度 * 时间 初中生的知识不多说
4).时间、位移、速度、加速度构成了现代科学的运动体系
1.2:关于FPS
那刷新要有多快呢?不知你是否听过FPS,对就是那个游戏里很重要的FPS
FPS : Frames Per Second 画面每秒传输帧数(新率) 单位赫兹(Hz)
60Hz的刷新率刷也就是指屏幕一秒内刷新60次,即60帧/秒
其中常见的电影24fps,也就是一秒钟刷新24次。
要达到流畅,需要60fps,这也是游戏中的一个指标,否则就会感觉不流畅
一秒钟刷新60次,即16.66667ms刷新一次,这也是一个常见的值
1.3:代码中的动画
可以用代码模拟运动,不断刷新的同时改变运动物体的属性从而形成动画
在Android中有ValueAnimator
,JavaScript(浏览器)中有``.
1.时间:无限执行----模拟时间流,每次刷新时间间隔,记为:1T
2.位移:物体在屏幕像素位置----模拟世界,每个像素距离记为:1px
3.速度(单位px/T)、加速度(px/T^2)
注意:无论什么语言,只要能够模拟时间与位移,本篇的思想都可以适用,只是语法不同罢了
2.粒子动画
2.1:Flutter中的时间流
image通过AnimationController来实现一个不断刷新的舞台,那么表演就交给你了
class RunBall extends StatefulWidget {
@override
_RunBallState createState() => _RunBallState();
}
class _RunBallState extends State<RunBall> with SingleTickerProviderStateMixin {
AnimationController controller;
var _oldTime = DateTime.now().millisecondsSinceEpoch;//首次运行时时间
@override
Widget build(BuildContext context) {
var child = Scaffold(
);
return GestureDetector(//手势组件,做点击响应
child: child,
onTap: () {
controller.forward();//执行动画
},
);
}
@override
void initState() {
controller =//创建AnimationController对象
AnimationController(duration: Duration(days: 999 * 365), vsync: this);
controller.addListener(() {//添加监听,执行渲染
_render();
});
}
@override
void dispose() {
controller.dispose(); // 资源释放
}
//渲染方法,更新状态
_render() {
setState(() {
var now = DateTime.now().millisecondsSinceEpoch;//每一刷新时间
print("时间差:${now - _oldTime}ms");//打印时间差
_oldTime = now;//重新赋值
});
}
}
2.2:静态小球的绘制
小球.png又到了我们的Canvas了
///小球信息描述类
class Ball {
double aX; //加速度
double aY; //加速度Y
double vX; //速度X
double vY; //速度Y
double x; //点位X
double y; //点位Y
Color color; //颜色
double r;//小球半径
Ball({this.x=0, this.y=0, this.color, this.r=10,
this.aX=0, this.aY=0, this.vX=0, this.vY=0});
}
///画板Painter
class RunBallView extends CustomPainter {
Ball _ball; //小球
Rect _area;//运动区域
Paint mPaint; //主画笔
Paint bgPaint; //背景画笔
RunBallView(this._ball,this._area) {
mPaint = new Paint();
bgPaint = new Paint()..color = Color.fromARGB(148, 198, 246, 248);
}
@override
void paint(Canvas canvas, Size size) {
canvas.drawRect(_area, bgPaint);
_drawBall(canvas, _ball);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
///使用[canvas] 绘制某个[ball]
void _drawBall(Canvas canvas, Ball ball) {
canvas.drawCircle(
Offset(ball.x, ball.y), ball.r, mPaint..color = ball.color);
}
}
var _area= Rect.fromLTRB(0+40.0,0+200.0,280+40.0,200+200.0);
var _ball = Ball(color: Colors.blueAccent, r: 10,x: 40.0+140,y:200.0+100);
---->[使用:_RunBallState#build]----
var child = Scaffold(
body: CustomPaint(
painter: RunBallView(_ball,_area),
),
);
2.3:远动盒
速度的合成.png 碰撞分析png 运动盒.gif也就是控制小球在每次刷新时改变其属性,这样视觉上就是运动状态
在边界碰撞后,改变方向即可,通过下面三步,一个运动盒就完成了
//[1].为小球附上初始速度和加速度
var _ball = Ball(color: Colors.blueAccent, r: 10,aY: 0.1, vX: 2, vY: -2,x: 40.0+140,y:200.0+100);
//[2].核心渲染方法,每次调用时更新小球信息
_render() {
updateBall();
setState(() {
var now = DateTime.now().millisecondsSinceEpoch;
print("时间差:${now - _oldTime}ms,帧率:${1000/(now - _oldTime)}");
_oldTime = now;
});
}
//[3].更新小球的信息
void updateBall() {
//运动学公式
_ball.x += _ball.vX;
_ball.y += _ball.vY;
_ball.vX += _ball.aX;
_ball.vY += _ball.aY;
//限定下边界
if (_ball.y > _area.bottom - _ball.r) {
_ball.y = _area.bottom - _ball.r;
_ball.vY = -_ball.vY;
_ball.color=randomRGB();//碰撞后随机色
}
//限定上边界
if (_ball.y < _area.top + _ball.r) {
_ball.y = _area.top + _ball.r;
_ball.vY = -_ball.vY;
_ball.color=randomRGB();//碰撞后随机色
}
//限定左边界
if (_ball.x < _area.left + _ball.r) {
_ball.x = _area.left + _ball.r;
_ball.vX = -_ball.vX;
_ball.color=randomRGB();//碰撞后随机色
}
//限定右边界
if (_ball.x > _area.right - _ball.r) {
_ball.x = _area.right - _ball.r;
_ball.vX= -_ball.vX;
_ball.color=randomRGB();//碰撞后随机色
}
}
}
2.4:让小球按照指定的函数图像运动
image给定一个较小的dx,随着dx增加,根据函数求出dy,然后更新小球信息
如下面的sin图像,随着每次更新,根据函数关系约束小球坐标值
double dx=0.0;
void updateBall(){
dx+=pi/180;//每次dx增加pi/180
_ball.x+=dx;
_ball.y+=f(dx);
}
f(x){
var y= 5*sin(4*x);//函数表达式
return y;
}
image或者让小球按圆形轨迹运动,下面是通过参数方程让呈圆形轨迹
也就是数学学得好,想怎么跑怎么跑。
double dx=0.0;
void updateBall(){
dx+=pi/180;//每次dx增加pi/180
_ball.x+=cos(dx);
_ball.y+=sin(dx);
}
3.粒子束
3.1:多个粒子运动
image一个粒子运动已经够好玩的,那么许多粒子会怎么样?
需要改变的是RunBallView的入参,由一个球换成小球列表,
绘画时批量绘制,更新信息时批量更新
//[1].单体改成列表
class RunBallView extends CustomPainter {
List<Ball> _balls; //小球列表
//[2].绘画时批量绘制
void paint(Canvas canvas, Size size) {
_balls.forEach((ball) {
_drawBall(canvas, ball);
});
}
//[3].渲染时批量更改信息
_render() {
for (var i = 0; i < _balls.length; i++) {
updateBall(i);
}
setState(() {
});
}
//[4]._RunBallState中初始化时生成随机信息的小球
for (var i = 0; i < 30; i++) {
_balls.add(Ball(
color: randomRGB(),
r: 5 + 4 * random.nextDouble(),
vX: 3*random.nextDouble()*pow(-1, random.nextInt(20)),
vY: 3*random.nextDouble()*pow(-1, random.nextInt(20)),
aY: 0.1,
x: 200,
y: 300));
}
也许你觉得画小球没什么,但要知道,小球只是单体,
你可以换成任意你能绘制的东西,甚至是图片或组件
3.2:撞击分裂的效果
image也就是在恰当的时机可以添加粒子而达到一定的视觉效果
核心是当到达边界后进行处理,将原来的粒子半径减半,再添加一个等大反向的粒子
//限定下边界
if (ball.y > _area.bottom) {
var newBall = Ball.fromBall(ball);
newBall.r = newBall.r / 2;
newBall.vX = -newBall.vX;
newBall.vY = -newBall.vY;
_balls.add(newBall);
ball.r = ball.r / 2;
ball.y = _area.bottom;
ball.vY = -ball.vY;
ball.color = randomRGB(); //碰撞后随机色
}
当越分越多时,会存在大量绘制,这时可以控制一下条件来移除
void updateBall(int i) {
var ball = _balls[i];
if (ball.r < 0.3) {
//半径小于0.3就移除
_balls.removeAt(i);
}
//略...
}
3.3:特定粒子
image 点阵分析.png现在可以感受到,动画就是元素的信息在不断变化,给人产生的感觉
只要将信息描述好,那么你可以完成任何动画,你就是创造者与主宰者
/**
* 渲染数字
* @param num 要显示的数字
* @param canvas 画布
*/
void renderDigit(double radius) {
var one = [
[0, 0, 0, 1, 1, 0, 0],
[0, 1, 1, 1, 1, 0, 0],
[0, 0, 0, 1, 1, 0, 0],
[0, 0, 0, 1, 1, 0, 0],
[0, 0, 0, 1, 1, 0, 0],
[0, 0, 0, 1, 1, 0, 0],
[0, 0, 0, 1, 1, 0, 0],
[0, 0, 0, 1, 1, 0, 0],
[0, 0, 0, 1, 1, 0, 0],
[1, 1, 1, 1, 1, 1, 1]
]; //1
for (int i = 0; i < one.length; i++) {
for (int j = 0; j < one[j].length; j++) {
if (one[i][j] == 1) {
double rX = j * 2 * (radius + 1) + (radius + 1); //第(i,j)个点圆心横坐标
double rY = i * 2 * (radius + 1) + (radius + 1); //第(i,j)个点圆心纵坐标
_balls.add(Ball(
r: radius,
x: rX,
y: rY,
color: randomRGB(),
vX: 3 * random.nextDouble() * pow(-1, random.nextInt(20)),
vY: 3 * random.nextDouble() * pow(-1, random.nextInt(20))));
}
}
}
}
image通过一个二维数组记录点位信息,在绘制的时候判断绘制就能呈现既定效果
然后通过信息创建小球,通过渲染展现出来,通过动画将其运动。
其实通过像素点也可以记录这些信息,就可以将图片进行粒子画,
之前在Android粒子篇之Bitmap像素级操作 写得很信息,这里不展开了
总的来说,动画包括三个重要的条件
时间流,渲染绘制,信息更新逻辑
这并不只是对于Flutter,任何语言只要满足这三点,粒子动画就可以跑起来
至于有什么用,也许可以提醒我,我不是搬砖的,而是程序设计师一个Creater...
结语
本文到此接近尾声了,如果想快速尝鲜Flutter,《Flutter七日》会是你的必备佳品;如果想细细探究它,那就跟随我的脚步,完成一次Flutter之旅。
另外本人有一个Flutter微信交流群,欢迎小伙伴加入,共同探讨Flutter的问题,本人微信号:zdl1994328
,期待与你的交流与切磋。
本文所有源码见
github/flutter_journey