这篇只是动画的初级,你翻到最后看如果你一看就知道那是怎么做的,就可以到下一篇啦
1. 动画场景准备
首先动画是怎么动的,无非就是在非常短的间隔内连续刷新屏幕,然后每一次刷新都让某个元素有一个新的位移,连续刷新起来就像动画一样了
@override
void initState() {
// TODO: implement initState
super.initState();
_updateTimer();
}
var _now = DateTime.now();
_updateTimer() {
setState(() {
_now = DateTime.now();
_timer = Timer((Duration(milliseconds: 1)), (){
_updateTimer();
});
});
}
2. 从自由落体画起来吧
越简单的东西越不容易动,所以我们用最简单的一个球从天上掉下来的场景来玩,当你画完后,可能你就真的发现鱼在哪了~
从现在起我们把刷新的间隔当成1个时间间隔,(其实他就是1微秒),我们在画布上画个圆,因为每个时间间隔都会刷新画布,所以我们设置圆的 y 坐标 : y = y + 0.1 ( 如果写 1 ,那球会跑太快...)
double y = 10;
class FalldownPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
y = y + 1;
canvas.drawCircle(Offset(100, y), 10, Paint()..color = Colors.white..strokeWidth = 10);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
// TODO: implement shouldRepaint
return true;
}
}
好了,是不是你的球掉下来了呢,那当然不够,你会发现你的球掉着掉着就掉出屏幕了,我们当然希望球在屏幕里上下弹,到底了就弹回来,我们假设 球的掉落速度 = v,在上面的公式里 v = 1,
如果 y < 屏幕高,那 y = y + v 当 y 高出屏幕时候,让 v 转向,即 v = -v, 当 y < 0的时候,再让 v 转向,以此类推
当然为了让后面的截图看起来快点,我们让 v = 10,球到屏幕当中就弹回来
double y = 10;
double v = 10;
class FalldownPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
if(y > size.height/2 || y < 0) {
v = -v;
}
y = y + v;
canvas.drawCircle(Offset(100, y), 10, Paint()..color = Colors.white..strokeWidth = 10);
}
}
好了,你的球已经上下抖起来了,是不是慢慢有意思起来了
但是看着是抖起来了,你没觉得不正常嘛?这只是普通数学上的变化,但是我们可是坚定的唯物主义者,所以如果物体下落都是这样的【匀速运动】,那看着也不好看,回想下,我们仿照自由落体,给小球一个加速度,翻一翻百度,这是最简单(可能也是我们接受的上限了):
v = gt,
g在这里我们取10,t因为是1个时间间隔,所以第10个t的时候,v = 10g,即我们可以得到 v = v + g的公式,所以:
v = 0;
...
@override
void paint(Canvas canvas, Size size) {
g = 30 / 1000;
v = v + g;
if(y > size.height/2 || y < 0) {
v = -v;
}
y = y + v;
canvas.drawCircle(Offset(100, y), 10, Paint()..color = Colors.white..strokeWidth = 10);
}
同时我们把v的初始值变成0,让他自然下落,前面刚说过g=10,但是代码里变成了 g = 30/1000,因为明显g=10的时候速度太快了把我闪瞎了,不信你自己试试~
嗯,写完我自己想了半天,g永远朝下,只是v相反了,所以向上移动时候小球获的加速度g依旧朝下,是的我能回忆的起来的只有这么多了。

仔细看,是不是变得好看多了~
所以你家的球是永动机嘛?
当然不是,我们用 isUp 来表示球是否正在向上弹:
if(v > 0) {
isUp = false;
}
if(y >= size.height/2){
isUp = true;
times = times + 1;
}
所以当 isUp = true,给他一个更大的向下的加速度,让他回不到原来高度,速度就降成0,
并且,我们需要让球每次弹一次,高度变低一点,即向下的加速度每弹一次会变大一点:
嗯,网上找了个函数图,大概就是右半部分,
g = isUp ? math.pow(1.2, times) * 60 / 1000 : 60 / 1000;
嗯,这里加速度变成了 60/1000 ,是不是很随性~ 同时g会随times变大而变得越来越大,嗯,是越来越大~
@override
void paint(Canvas canvas, Size size) {
g = isUp ? math.pow(1.2, times) * 60 / 1000 : 60 / 1000;
if(y > size.height/2) {
v = -v;
}
v = v + g;
y = y + v;
if(v > 0) {
isUp = false;
}
if(y >= size.height/2){
isUp = true;
times = times + 1;
}
print(v);
canvas.drawCircle(Offset(100, y), 10, Paint()..color = Colors.white..strokeWidth = 10);
}
最终,因为球向上弹的加速度不断变大,导致球没法弹到顶上最后球会越来越弱,越来越弱....

3. 该起风了
所以落体你已经会了,你可以多放几个球放到场景里然后让他们一个个掉下去,就不在这多写了
我们用同样的逻辑给小球加上个横向的速度,简单点就不给他加速度了,单纯的左右运动,vx = vx + 0.01,同时把之前的 v 变成 vy,
@override
void paint(Canvas canvas, Size size) {
g = isUp ? math.pow(1.2, times) * 60 / 1000 : 60 / 1000;
if(y > size.height/2) {
vy = -vy;
}
if(x > size.width || x < 0) {
vx = -vx;
}
vy = vy + g;
y = y + vy;
if(vy > 0) {
isUp = false;
}
if(y >= size.height/2){
isUp = true;
times = times + 1;
}
// 横向移动
if(x > size.width || x < 0) {
vx = -vx;
}
vx = vx + 0.01;
x = x + vx;
canvas.drawCircle(Offset(x, y), 10, Paint()..color = Colors.white..strokeWidth = 10);
}
4. 来 多放点球
好了,最基础篇最后,我们把一大堆球放到场景里,让自己满意一下。所以我们要一本正经的面向对象,给球做一个对象来存每个球的xy距离和速度:
class SnowBasicInfo {
double x, y, r;
double vx, vy, vr;
double ax, ay, ar;
}
ax, ay 是 x , y方向上的加速度,因为上面我们只提到了重力加速度,所以只有ay = g,
然后我们在 painter 中传入一个数组集合,来分别画这些球,嗯从现在开始他叫雪了:
List<SnowBasicInfo> snows;
@override
void paint(Canvas canvas, Size size) {
snows.forEach((element) {
element.ay = element.isUp ? math.pow(1.2, element.upTimes) * 60 / 1000 : 60 / 1000;
if(element.y > size.height) {
element.vy = -element.vy;
}
element.vy = element.vy + element.ay;
element.y = element.y + element.vy;
if(element.vy > 0) {
element.isUp = false;
}
if(element.y >= size.height){
element.isUp = true;
element.upTimes = element.upTimes + 1;
}
if(element.x > size.width || element.x < 0) {
element.vx = -element.vx;
}
element.vx = element.vx + 0.01;
element.x = element.x + element.vx;
canvas.drawCircle(Offset(element.x, element.y), 10, Paint()..color = Colors.white..strokeWidth = 10);
});
}
这只是把上面的单个球的运动变成了多个球一起动,然后我们在外面定义一个随机生成的球集合:
var snows = List.generate(20, (index) => SnowBasicInfo(x: Random().nextInt(400).toDouble() , y: - Random().nextInt(50).toDouble(), r: 10));
这里是这篇中第一次出现随机数,其实也就是球的初始x是400中随机,y是50中随机,然后我们再做一个简单的方法来生成随机的雪:
List<SnowBasicInfo> moreSnow() {
return List.generate(Random().nextInt(5), (index) => SnowBasicInfo(x: Random().nextInt(400).toDouble() , y: - Random().nextInt(100).toDouble(), r: SnowInfoUtils.snowRadius));
}
好了,有意思的时刻到了,我们把这个雪的列表塞入上面那个 painter, 同时,我们在每个时间周期中,都在原本的 snows 中再加入一波随机雪,即不断的刷新,不断的有雪加入:
所以整个场景会变成:
@override
Widget build(BuildContext context) {
// 这样分割时间的目的是为了让新的雪加入变得慢一点,否则一会会就满屏都是雪然后卡了
if(_now.millisecond % 10 == 0) {
snow.addAll(moreSnow());
}
return Scaffold(
appBar: AppBar(
title: Text(_now.millisecondsSinceEpoch.toString()),
),
body: Container(
color: Colors.black,
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: CustomPaint(
painter: ManyBallsPainter(snows: snow),
),
)
);
}
看到这个效果,你是不是很期待的搓着手手准备下一章了呢~
摸更多鱼?
如果你对相关内容感兴趣呢,欢迎来一起摸鱼鸭: github.com/SpiciedCrab…