【摸鱼研究所】让你的屏幕下个雪呗 (前篇)

1,514 阅读5分钟

这篇只是动画的初级,你翻到最后看如果你一看就知道那是怎么做的,就可以到下一篇啦

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

看到这个效果,你是不是很期待的搓着手手准备下一章了呢~

rda67q.md.gif

摸更多鱼?

如果你对相关内容感兴趣呢,欢迎来一起摸鱼鸭: github.com/SpiciedCrab…