注册

Android 粒子漩涡动画

前言


粒子动画经常用于大画幅的渲染效果,实际上难度并不高,但是在使用粒子动画时,必须要遵循的一些要素,主要是:



  • 起点
  • 矢量速度
  • 符合运动学公式

在之前的文章中,如《烟花效果》和《粒子喷泉》是典型的粒子动画,起点之所以重要是因为其实位置决定粒子出现的位置,矢量速度则决定了快慢和方向,运动学公式属于粒子动画的一部分,当然不是物理性的,毕竟平面尺寸也就那么长,这里的物理学公式使得画面更加丝滑而无跳动感觉。


本篇将实现下面的效果


fire_90.gif
注意:gif图有些卡,实际上流畅很多


本篇效果实现


本篇效果是无数圆随机产生然后渐渐变大并外旋,另外也有雨滴,这里的雨滴相对简单一些。


首先定义粒子对象


定义粒子对象是非常重要的,绝大部分倾下粒子本身就是需要单独控制的,因为每个粒子的轨迹都是有所差别的。


定义圆圈粒子


private static class Circle {
float x;
float y;
int color;

float radius;

Circle(float x, float y, float radius) {
reset(x, y, radius);
}

private void reset(float x, float y, float radius) {
this.x = x;
this.y = y;
this.radius = radius;
this.color = Color.rgb((int) (Math.random() * 256), (int) (Math.random() * 256), (int) (Math.random() * 256));
}
}

定义雨滴


private static class RainDrop {
float x;
float y;

RainDrop(float x, float y) {
this.x = x;
this.y = y;
}
}

定义粒子管理集合


private ArrayList mParticles;
private ArrayList mRainDrops;
private long mLastUpdateTime; //记录执行时间

生成粒子对象



  • 生成雨滴是从顶部屏幕意外开始,而y = -50f值是雨滴的高度决定。
  • 圆圈是随机产生,在中心位置圆圈内。

// 创建新的雨滴
if (mRainDrops.size() < 80) {
int num = getWidth() / padding;
double nth = num * Math.random() * padding;
double x = nth + padding / 2f * Math.random();
RainDrop drop = new RainDrop((float) x, -50f);
mRainDrops.add(drop);
}

// 创建新的粒子
if (mParticles.size() < 100) {
float x = (float) (getWidth() / 2f - radius + 2*radius * Math.random());
float y = (float) (getHeight()/2f - radius + 2*radius * Math.random() );

Circle particle = new Circle(x, y,5);
mParticles.add(particle);
}

绘制雨滴


雨滴的绘制非常简单,调用相应的canvas方法即可


// 绘制雨滴
mPaint.setColor(Color.WHITE);
for (RainDrop drop : mRainDrops) {
canvas.drawLine(drop.x, drop.y, drop.x, drop.y + 20, mPaint);
}

// 绘制粒子
for (Circle particle : mParticles) {
mPaint.setColor(particle.color);
canvas.drawCircle(particle.x, particle.y, particle.radius, mPaint);
}

更新粒子位置


雨滴的更新相对简单,但是圆圈的旋转是一个难点,一个重要的问题是如何旋转粒子的,其实有很多方法,其中最笨的方法是旋转Canvas坐标系,底层有很多矩阵计算,但是这个似乎使用Math.atan2(y,x)显然更加方便,我们只需要在当前角度加上偏移量就能旋转。


float angle = (float) Math.atan2(dy, dx) + deltaTime * 0.65f;

下面是完整的更新逻辑


// 更新雨滴位置
Iterator rainIterator = mRainDrops.iterator();
while (rainIterator.hasNext()) {
RainDrop drop = rainIterator.next();
if (drop.y > getHeight() + 50) {
int num = getWidth() / padding;
double nth = num * Math.random() * padding;
double x = nth + padding * Math.random();

drop.x = (float) (x);
drop.y = -50;
} else {
drop.y += 20;
}

}

// 更新粒子位置
long currentTime = System.currentTimeMillis();
float deltaTime = (currentTime - mLastUpdateTime) / 1000f;
mLastUpdateTime = currentTime;

float centerX = getWidth() / 2f;
float centerY = getHeight() / 2f;

Iterator iterator = mParticles.iterator();
while (iterator.hasNext()) {
Circle particle = iterator.next();
float dx = particle.x - centerX;
float dy = particle.y - centerY;
float distance = (float) Math.sqrt(dx * dx + dy * dy) + 4.5f;// 增加偏移
float angle = (float) Math.atan2(dy, dx) + deltaTime * 0.5f;
particle.radius += 1f;

particle.x = centerX + (float) Math.cos(angle) * distance;
particle.y = centerY + (float) Math.sin(angle) * distance;

if (particle.radius > 10) {
int maxRadius = 100;
float fraction = (particle.radius - 10) / (maxRadius - 10);
if (fraction >= 1) {
fraction = 1;
}
particle.color = argb((int) (255 * (1 - fraction)), Color.red(particle.color), Color.green(particle.color), Color.blue(particle.color));
}
if (Color.alpha(particle.color) == 0) {

float x = (float) (getWidth() / 2f - radius + 2* radius * Math.random());
float y = (float) (getHeight()/2f - radius + 2*radius * Math.random() );
particle.reset(x,y, 5);
}

}

粒子刷新


其实刷新机制我们以前经常使用,调用postInvalidate即可,本身就是View自身的方法。


总结


本篇主要内容总体上就是这些,下面是全部代码逻辑


public class VortexView extends View {

private Paint mPaint;
private ArrayList mParticles;
private ArrayList mRainDrops;
private long mLastUpdateTime;
private int padding = 20;

public VortexView(Context context) {
super(context);
mPaint = new Paint();
mParticles = new ArrayList<>();
mRainDrops = new ArrayList<>();
mLastUpdateTime = System.currentTimeMillis();
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float radius = Math.min(getWidth(), getHeight()) / 3f;

// 创建新的雨滴
if (mRainDrops.size() < 80) {
int num = getWidth() / padding;
double nth = num * Math.random() * padding;
double x = nth + padding / 2f * Math.random();
RainDrop drop = new RainDrop((float) x, -50f);
mRainDrops.add(drop);
}

// 创建新的粒子
if (mParticles.size() < 100) {
float x = (float) (getWidth() / 2f - radius + 2*radius * Math.random());
float y = (float) (getHeight()/2f - radius + 2*radius * Math.random() );

Circle particle = new Circle(x, y,5);
mParticles.add(particle);
}

// 绘制雨滴
mPaint.setColor(Color.WHITE);
for (RainDrop drop : mRainDrops) {
canvas.drawLine(drop.x, drop.y, drop.x, drop.y + 20, mPaint);
}

// 绘制粒子
for (Circle particle : mParticles) {
mPaint.setColor(particle.color);
canvas.drawCircle(particle.x, particle.y, particle.radius, mPaint);
}

// 更新雨滴位置
Iterator rainIterator = mRainDrops.iterator();
while (rainIterator.hasNext()) {
RainDrop drop = rainIterator.next();
if (drop.y > getHeight() + 50) {
int num = getWidth() / padding;
double nth = num * Math.random() * padding;
double x = nth + padding * Math.random();

drop.x = (float) (x);
drop.y = -50;
} else {
drop.y += 20;
}

}

// 更新粒子位置
long currentTime = System.currentTimeMillis();
float deltaTime = (currentTime - mLastUpdateTime) / 1000f;
mLastUpdateTime = currentTime;

float centerX = getWidth() / 2f;
float centerY = getHeight() / 2f;

Iterator iterator = mParticles.iterator();
while (iterator.hasNext()) {
Circle particle = iterator.next();
float dx = particle.x - centerX;
float dy = particle.y - centerY;
float distance = (float) Math.sqrt(dx * dx + dy * dy) + 3.5f;// 增加偏移
float angle = (float) Math.atan2(dy, dx) + deltaTime * 0.65f;
particle.radius += 1f;

particle.x = centerX + (float) Math.cos(angle) * distance;
particle.y = centerY + (float) Math.sin(angle) * distance;

if (particle.radius > 10) {
int maxRadius = 100;
float fraction = (particle.radius - 10) / (maxRadius - 10);
if (fraction >= 1) {
fraction = 1;
}
particle.color = argb((int) (255 * (1 - fraction)), Color.red(particle.color), Color.green(particle.color), Color.blue(particle.color));
}
if (Color.alpha(particle.color) == 0) {

float x = (float) (getWidth() / 2f - radius + 2* radius * Math.random());
float y = (float) (getHeight()/2f - radius + 2*radius * Math.random() );
particle.reset(x,y, 5);
}

}

Collections.sort(mParticles, comparator);

// 使view无效从而重新绘制,实现动画效果
invalidate();
}
Comparator comparator = new Comparator() {
@Override
public int compare(Circle left, Circle right) {
return (int) (left.radius - right.radius);
}
};

public static int argb(
int alpha,
int red,
int green,
int blue)
{
return (alpha << 24) | (red << 16) | (green << 8) | blue;
}

private static class Circle {
float x;
float y;
int color;

float radius;

Circle(float x, float y, float radius) {
reset(x, y, radius);
}

private void reset(float x, float y, float radius) {
this.x = x;
this.y = y;
this.radius = radius;
this.color = Color.rgb((int) (Math.random() * 256), (int) (Math.random() * 256), (int) (Math.random() * 256));
}
}

private static class RainDrop {
float x;
float y;

RainDrop(float x, float y) {
this.x = x;
this.y = y;
}
}
}

作者:时光少年
来源:juejin.cn/post/7317957339012202496

0 个评论

要回复文章请先登录注册