Android 粒子中心扩散动画

前言

粒子动画效果相比其他动画来说是非常复杂了的,主要涉及三个方面,粒子初始化、粒子位移、粒子回收等问题,其中特别是粒子位移是最复杂的,涉及到的数学逻辑非常多,主要是各种三角函数、物理学公式等。

本篇将实现两种动画效果,代码基本相同,只是旋转速度不一样,因此,本篇其实可以看作一篇模板文章,具体效果可以通过调节参数生成各种动画

第一种动画

第二种动画

实现步骤

其实和以往的粒子效果一样,粒子需要被管理起来,因此我们需要有容器、也需要粒子对象

粒子对象定义

下面是创建粒子对象的逻辑,基本属性在注释中了

java 复制代码
static class Circle {
    int maxLength;  //最大运行距离
    float speed; //外扩速度
    float rotate; // 角速度
    private float degree; //起始角度
    private int y; //y坐标
    private int x; //x坐标
    private int color; //颜色
    private float radius; //小圆半径
    private float drawRadius; //绘制时的小圆半径
    
   
  public Circle(int color, int maxLength, float radius, float degree) {
    this.color = color;
    this.radius = radius;
    this.maxLength = maxLength;
    this.degree = degree;
    this.x = (int) (radius * Math.cos(degree));
    this.y = (int) (radius * Math.sin(degree));
    this.rotate = 0.35f;  //触角效果
    this.speed = 0.2f;
 }
}

粒子更新

在任何动画中,粒子运动必须具备时间属性,任何符合物理学的位移运动,速度和时间的关系是位移计算的方法。下面,我们继续给Circle类添加更新方法。

这里一个重要的知识点是

  • Math.hypot(x, y) :平方根计算
  • Math.atan2(y, x): 斜率计算,注意,此角度具备方向
java 复制代码
public boolean update(long timeline) {
    float length = (float) Math.hypot(x, y);  //计算当前移动的距离(距离中心点)
    float center = length + this.speed * timeline; //计算即将到达的距离
    float ratio = center / maxLength;  //计算与最远距离的比值

    this.drawRadius = (1f - ratio) * radius;  //距离越远,圆的半径越小

    double degree = Math.atan2(y, x) + rotate;  //即将旋转的角度

    this.x = (int) (center * Math.cos(degree)); //新的x
    this.y = (int) (center * Math.sin(degree)); //新的y

    if (drawRadius <= 0) {
        return false; //如果半径为0时,意味着圆看不见了,因此要坐下标记
    }
    return true;
}

粒子绘制方法

绘制自身其实很简单,只需要简单的调用Canvas相关逻辑即可

java 复制代码
public void draw(Canvas canvas, TextPaint paint) {
    paint.setColor(color);
    canvas.drawCircle(x, y, drawRadius, paint);
}

粒子回收

为了减少内存申请频率,我们对跑出边界的粒子进行重置

java 复制代码
public void reset() {
    this.x = (int) (radius * Math.cos(degree));
    this.y = (int) (radius * Math.sin(degree));
}

View逻辑

以上是完整的粒子对象逻辑,接下来我们实现一个View,用来管理和绘制粒子。

java 复制代码
int maxCircleRadius = 20;  //粒子初始半径
List<Circle> circleList = new ArrayList<>(); //容器
int maxCircleNum = 300; //最大数量

绘制逻辑

首先是初始化,我们这里设置了3种粒子,因此间隔角度是120度,而我们每次增加三种,防止出现混乱的问题。

java 复制代码
   final float rotateDegree = (float) Math.toRadians(120f); //间隔角度
    if (circleList.size() < maxCircleNum) {
    //每次增加三种
        circleList.add(new Circle(Color.RED, (int) maxRadius, maxCircleRadius, 0 * rotateDegree));
        circleList.add(new Circle(Color.GREEN, (int) maxRadius, maxCircleRadius, 1 * rotateDegree));
        circleList.add(new Circle(Color.CYAN, (int) maxRadius, maxCircleRadius, 2 * rotateDegree));
    }

下面是每个粒子的绘制逻辑

java 复制代码
for (int i = 0; i < circleList.size(); i++) {
    Circle circle = circleList.get(i);
    circle.draw(canvas, mPaint); //绘制方法
}

更新粒子

下面有个重要的逻辑,其实前面也提到过,就是重置跑出边界的粒子

java 复制代码
for (int i = 0; i < circleList.size(); i++) {
    Circle circle = circleList.get(i);
    if(!circle.update(16)){
        circle.reset(); //如果不能更新,则进行重置
    }
}
postInvalidate(); //刷新绘制逻辑

以上就是整体核心逻辑

效果调节

我们开头的两种效果其实是同一个View实现的,这其中一个重要的点就是速度调整,文章开头是调整出的两种效果,当然染还可以调整出其他效果 第一种

ini 复制代码
this.rotate = 0.2f;
this.speed = 0.2f; //外扩效果

第二种

java 复制代码
 this.rotate = 0.35f;  //触角效果
 this.speed = 0.2f;

第三种

java 复制代码
this.rotate = 0.8f;
this.speed = 0.1f;

当然,还有更多,篇幅原因就不深入了。

总结

本篇到这里就结束了,其实我们的核心代码并不多,但是简单的逻辑就能衍生出很多动画效果。其实,学习粒子动画是非常有意思的事,很多时候,你在实现某些效果的途中,就能突然开发出一种新的动画效果。

本篇代码

下面是本篇内容的完整逻辑,基本就在100行左右。

java 复制代码
public class CircleParticleView extends View {
    private TextPaint mPaint;
    private DisplayMetrics mDM;

    public CircleParticleView(Context context) {
        this(context, null);
    }
    public CircleParticleView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mDM = getResources().getDisplayMetrics();
        initPaint();
    }

    private void initPaint() {
        //否则提供给外部纹理绘制
        mPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setAntiAlias(true);
        mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        mPaint.setStrokeCap(Paint.Cap.ROUND);
        PaintCompat.setBlendMode(mPaint, BlendModeCompat.PLUS);

    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);

        if (widthMode != MeasureSpec.EXACTLY) {
            widthSize = mDM.widthPixels / 2;
        }
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        if (heightMode != MeasureSpec.EXACTLY) {
            heightSize = widthSize / 2;
        }
        setMeasuredDimension(widthSize, heightSize);

    }

    int maxCircleRadius = 20;
    List<Circle> circleList = new ArrayList<>();
    int maxCircleNum = 300;
    long time = 0;

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        int width = getWidth();
        int height = getHeight();
        float maxRadius = Math.min(width, height) / 2f;


        int save = canvas.save();
        canvas.translate(width / 2f, height / 2f);

        final float rotateDegree = (float) Math.toRadians(120f);
        if (circleList.size() < maxCircleNum) {
            circleList.add(new Circle(Color.RED, (int) maxRadius, maxCircleRadius, 0 * rotateDegree));
            circleList.add(new Circle(Color.GREEN, (int) maxRadius, maxCircleRadius, 1 * rotateDegree));
            circleList.add(new Circle(Color.CYAN, (int) maxRadius, maxCircleRadius, 2 * rotateDegree));
        }

        mPaint.setStyle(Paint.Style.FILL);
        for (int i = 0; i < circleList.size(); i++) {
            Circle circle = circleList.get(i);
            circle.draw(canvas, mPaint);
        }
        canvas.restoreToCount(save);

        for (int i = 0; i < circleList.size(); i++) {
            Circle circle = circleList.get(i);
            if (!circle.update(16)) {
                circle.reset();
            }
        }

        postInvalidate();
        time += 16;
    }

    static class Circle {
        int maxLength;  //最大运行距离
        float speed; //外扩速度
        float rotate; // 角速度
        private float degree; //起始角度
        private int y; //y坐标
        private int x; //x坐标
        private int color; //颜色
        private float radius; //小圆半径
        private float drawRadius; //绘制时的小圆半径

        public Circle(int color, int maxLength, float radius, float degree) {
            this.color = color;
            this.radius = radius;
            this.maxLength = maxLength;
            this.degree = degree;
            this.x = (int) (radius * Math.cos(degree));
            this.y = (int) (radius * Math.sin(degree));
            this.rotate = 0.35f;  //触角效果
            this.speed = 0.2f;
        }


        public boolean update(long timeline) {
            float length = (float) Math.hypot(x, y);
            float center = length + this.speed * timeline; //距离增加
            float ratio = center / maxLength;

            this.drawRadius = (1f - ratio) * radius;

            double degree = Math.atan2(y, x) + rotate;  //角度增加

            this.x = (int) (center * Math.cos(degree));
            this.y = (int) (center * Math.sin(degree));

            if (drawRadius <= 0) {
                return false;
            }
            return true;
        }

        public void draw(Canvas canvas, TextPaint paint) {
            paint.setColor(color);
            canvas.drawCircle(x, y, drawRadius, paint);
        }
        public void reset() {
            this.x = (int) (radius * Math.cos(degree));
            this.y = (int) (radius * Math.sin(degree));
        }
    }

}

后续

本篇延续实用性风格,后续我们继续实现一些粒子动画效果。

相关推荐
centor12 小时前
国际版 UnitySetup-Android-Support 安装 Mac 设备
android·macos
23级二本计科12 小时前
前端 HTML + CSS + JavaScript
前端·css·html
踩着两条虫12 小时前
VTJ.PRO「AI + 低代码」应用开发平台的后端模块系统
前端·人工智能·低代码
pany12 小时前
程序员近十年新年愿望,都有哪些变化?
前端·后端·程序员
朱昆鹏12 小时前
IDEA Claude Code or Codex GUI 插件【开源自荐】
前端·后端·github
城东米粉儿12 小时前
compose 中的附带效应笔记一
android
HashTang12 小时前
买了专业屏只当普通屏用?解锁 BenQ RD280U 的“隐藏”开发者模式
前端·javascript·后端
双向3312 小时前
Agent智能体:2026年AI开发者必须掌握的自主系统革命
前端
布列瑟农的星空12 小时前
通用语法校验器tree-sitter——C++语法校验实践
前端
用户812748281512012 小时前
libgui中的BufferQueueProducer加入堆栈CallStack编译报错问题-大厂企业实战项目难题
前端