目录
前言
牛顿摆大家应该都不陌生,也叫碰碰球、永动球(理论情况下),那么今天我们用Flutter实现这么一个理论中的永动球,可以作为加载Loading使用。
- 知识点:绘制、动画曲线、多动画状态更新
效果图:
实现步骤
1、绘制静态效果
首先我们需要把线和小圆球绘制出来,对于看过我之前文章的小伙伴来说这个就很简单了,效果图:
关键代码:
// 小圆球半径 double radius = 6; /// 小球圆心和直线终点一致 //左边小球圆心 Offset offset = Offset(20, 60); //右边小球圆心 Offset offset2 = Offset(20 * 6 * 8, 60); Paint paint = Paint() ..color = Colors.black87 ..strokeWidth = 2; /// 绘制线 canvas.drawLine(Offset.zero, Offset(90, 0), paint); canvas.drawLine(Offset(20, 0), offset, paint); canvas.drawLine( Offset(20 + radius * 2, 0), Offset(20 + radius * 2, 60), paint); canvas.drawLine( Offset(20 + radius * 4, 0), Offset(20 + radius * 4, 60), paint); canvas.drawLine( Offset(20 + radius * 6, 0), Offset(20 + radius * 6, 60), paint); canvas.drawLine(Offset(20 + radius * 8, 0), offset2, paint); /// 绘制小圆球 canvas.drawCircle(offset, radius, paint); canvas.drawCircle(Offset(20 + radius * 2, 60), radius, paint); canvas.drawCircle(Offset(20 + radius * 4, 60), radius, paint); canvas.drawCircle(Offset(20 + radius * 6, 60), radius, paint); canvas.drawCircle(offset2, radius, paint);
2、加入动画
思路: 我们可以看到5个小球一共2个小球在运动,左边小球运动一个来回之后传递给右边小球,右边小球开始运动,右边一个来回再传递给左边开始,也就是左边运动周期是:0-1-0,正向运动一次,反向再运动一次,这样就是一个周期,右边也是一样,左边运动完传递给右边,右边运动完传递给左边,这样就简单实现了牛顿摆的效果。
两个关键点
小球运动路径: 小球的运动路径是一个弧度,以竖线的起点为圆心,终点为半径,那么我们只需要设置小球运动至最高点的角度即可,通过角度就可计算出小球的坐标点。
运动曲线: 当然我们知道牛顿摆小球的运动曲线并不是匀速的,他是有一个加速减速过程的,撞击之后,小球先加速然后减速达到最高点速度为0,之后速度再从0慢慢加速进行撞击小球,周而复始。
下面的运动曲线就是先加速再减速,大概符合牛顿摆的运动曲线。我们就使用这个曲线看看效果。
完整源码
class OvalLoading extends StatefulWidget { const OvalLoading({Key? key}) : super(key: key); @override _OvalLoadingState createState() => _OvalLoadingState(); } class _OvalLoadingState extends State<OvalLoading> with TickerProviderStateMixin { // 左边小球 late AnimationController _controller = AnimationController(vsync: this, duration: Duration(milliseconds: 300)) ..addStatusListener((status) { if (status == AnimationStatus.completed) { _controller.reverse(); //反向执行 1-0 } else if (status == AnimationStatus.dismissed) { _controller2.forward(); } }) ..forward(); // 右边小球 late AnimationController _controller2 = AnimationController(vsync: this, duration: Duration(milliseconds: 300)) ..addStatusListener((status) { // dismissed 动画在起始点停止 // forward 动画正在正向执行 // reverse 动画正在反向执行 // completed 动画在终点停止 if (status == AnimationStatus.completed) { _controller2.reverse(); //反向执行 1-0 } else if (status == AnimationStatus.dismissed) { // 反向执行完毕左边小球执行 _controller.forward(); } }); late var cure = CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic); late var cure2 = CurvedAnimation(parent: _controller2, curve: Curves.easeOutCubic); late Animation<double> animation = Tween(begin: 0.0, end: 1.0).animate(cure); late Animation<double> animation2 = Tween(begin: 0.0, end: 1.0).animate(cure2); @override Widget build(BuildContext context) { return Container( margin: EdgeInsetsDirectional.only(top: 300, start: 150), child: CustomPaint( size: Size(100, 100), painter: _OvalLoadingPainter( animation, animation2, Listenable.merge([animation, animation2])), ), ); } @override void dispose() { _controller.dispose(); _controller2.dispose(); super.dispose(); } } class _OvalLoadingPainter extends CustomPainter { double radius = 6; final Animation<double> animation; final Animation<double> animation2; final Listenable listenable; late Offset offset; // 左边小球圆心 late Offset offset2; // 右边小球圆心 final double lineLength = 60; // 线长 _OvalLoadingPainter(this.animation, this.animation2, this.listenable) : super(repaint: listenable) { offset = Offset(20, lineLength); offset2 = Offset(20 * radius * 8, lineLength); } // 摆动角度 double angle = pi / 180 * 30; // 30° @override void paint(Canvas canvas, Size size) { Paint paint = Paint() ..color = Colors.black87 ..strokeWidth = 2; // 左边小球 默认坐标 下方是90度 需要+pi/2 var dx = 20 + 60 * cos(pi / 2 + angle * animation.value); var dy = 60 * sin(pi / 2 + angle * animation.value); // 右边小球 var dx2 = 20 + radius * 8 - 60 * cos(pi / 2 + angle * animation2.value); var dy2 = 60 * sin(pi / 2 + angle * animation2.value); offset = Offset(dx, dy); offset2 = Offset(dx2, dy2); /// 绘制线 canvas.drawLine(Offset.zero, Offset(90, 0), paint); canvas.drawLine(Offset(20, 0), offset, paint); canvas.drawLine( Offset(20 + radius * 2, 0), Offset(20 + radius * 2, 60), paint); canvas.drawLine( Offset(20 + radius * 4, 0), Offset(20 + radius * 4, 60), paint); canvas.drawLine( Offset(20 + radius * 6, 0), Offset(20 + radius * 6, 60), paint); canvas.drawLine(Offset(20 + radius * 8, 0), offset2, paint); /// 绘制球 canvas.drawCircle(offset, radius, paint); canvas.drawCircle( Offset(20 + radius * 2, 60), radius, paint); canvas.drawCircle(Offset(20 + radius * 4, 60), radius, paint); canvas.drawCircle(Offset(20 + radius * 6, 60), radius, paint); canvas.drawCircle(offset2, radius, paint); } @override bool shouldRepaint(covariant _OvalLoadingPainter oldDelegate) { return oldDelegate.listenable != listenable; } }
去掉线的效果
总结
本文展示了实现牛顿摆的原理,其实并不复杂,关键点就是小球的运动轨迹和运动速度曲线,如果用到项目中当做Loading还有很多优化的空间,比如加上小球影子、修改小球颜色或者把小球换成好玩的图片等等操作会看起来更好看一点