Flutter实现牛顿摆动画效果的示例代码
作者:老李code 发布时间:2022-10-23 12:58:40
标签:Flutter,牛顿摆,动画
前言
牛顿摆大家应该都不陌生,也叫碰碰球、永动球(理论情况下),那么今天我们用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;
}
}
去掉线的效果
来源:https://juejin.cn/post/7090123854135164935


猜你喜欢
- 本文实例为大家分享了Android实现按钮滚动选择效果的具体代码,供大家参考,具体内容如下效果图代码实现package com.demo.u
- 序言因为之前在项目中使用看groovy对业务进行一些抽象,效果比较好,过程中踩过一些坑,所以简单记录分享一下自己如何一步一步去实现的为什么用
- 重载1.构造器的重载因为构造器的名字必须与类名相同,所以同一个类的所有构造器名肯定相同,构成重载;为了让系统能区分不同的构造器,多个构造器的
- Hystrix Dashboard,它主要用来实时监控Hystrix的各项指标信息。通过Hystrix Dashboard反馈的实时信息,可
- 记事本涉及到的仅仅是对string 的存储,而且在读取上并不存在什么难点,直接用textview显示便可以了。需要做的主要是使用SQLite
- 在spring中有很多以XXXAware命名的接口,很多人也不清楚这些接口都是做什么用的,这篇文章将描述常用的一些接口。一,Applicat
- 这篇文章主要介绍了mybatis insert返回主键代码实例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,
- 本文实例为大家分享了Android仿QQ讨论组头像展示的具体代码,供大家参考,具体内容如下一、效果图二、实现基本实现过程:1.将原图片读取为
- 本文实例为大家分享了C语言实现Flappy Bird小游戏的具体代码,供大家参考,具体内容如下#include<stdio.h>
- 一、流程和任务的关系以下是一个简单的请假流程图,其中有一个开始事件,两个用户任务,一个结束事件。启动流程后,activiti会自动创建第一个
- 前言平时做一些统计数据,经常从数据库或者是从接口获取出来的数据,单位是跟业务需求不一致的。比如, 我们拿出来的 分, 实际上要是元又比如,我
- 1. 生命周期感知1.1 生命周期感知组件我们知道,Controller(Activity or Fragment) 都是有生命周期的,但是
- 前言最近想体验下最新版本的SpringBoot,逛了下官网,发现SpringBoot目前最新版本已经是2.6.4了,版本更新确实够快的。之前
- 说明在学习jvm相关知识时,一般会讲到类字节码相关内容,为了更清晰的了解类字码具体内容,一般我们会使用javap命令进行查看,但是仍然不够直
- 前言C#基于NAudio工具对Wav音频文件进行剪切,将一个音频文件剪切成多个音频文件注:调用方法前需要导入NAudio.dll或者在NuG
- RecyclerView 是 android-support-v7-21 版本中新增的一个 Widgets, 还有一个 CardView 会
- 代码编译运行环境:VS2017+Debug+Win32按照参数形式的不同,C++应该有三种函数调用方式:传值调用、引用调用和指针调用。对于基
- 你以前听到的谈论关于Java8的所有都是围绕lambda表达式. 但它仅仅是Java8的一部分. Java 8 有许多新特性—一些强大的新类
- 前言 因为自己在做的一个小软件里面需要用到从A-Z排序的ListView,所以自然而然的想到了微信的联系人,我想要的就是那样的效果。本来没
- 本文实例讲述了Android编程常用技巧。分享给大家供大家参考,具体如下:1. 登录的时候,如果输入有误,则输入框左右震动,表示输入有误在r