首先声明的是,粒子效果不一定是用surfaceview来实现的,只要可以绘制和更新绘制既可以做到很多精彩的画面。
简单的说一下自定义view吧,其实就是继承View,然后生成几个构造方法,这样就是一个简单的自定义view。
public class MyView extends View{
public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyView(Context context) {
super(context);
}
}
但是它什么也做不了,什么也不显示,很多人都知道,覆盖onDraw(Canvas)方法,没错
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//做自己想做的事情(绘制)
}
这样,一个可以显示自己绘制的view已经完成一半了,为什么说一半,因为还不会动,但是这会有很多逻辑,而这些逻辑不应该出现在主线程中,所以用Handler来做这部分事情,开启一个处理逻辑的线程通过Handler来更新绘制。
private Runnable run = new Runnable() {
public void run() {
long curTime = 0;
while (true) {
curTime = System.currentTimeMillis();
//此处加入逻辑
mHandler.sendEmptyMessage(0);
curTime = System.currentTimeMillis() - curTime;
try {
if(curTime < 30){
Thread.sleep(30 - curTime);
}
} catch (InterruptedException e) {
break;
}
}
}
};
private void logic(){
//此处加入逻辑处理
}
private Handler mHandler = new Handler() {
public void handleMessage(android.os.Message msg) {
invalidate();
};
};
好吧,这样基本上一个自定义view的框架就出来了。
铺垫好了后面就会好理解了,绘制和动画基本上都是在onDraw里面和logic里面完成,其他的粒子效果也是。
粒子效果在我看来,其实就是具有相同属性的对象集合运动,说白了,就是一个列表里面有多个对象,每个对象自己都在动,放在一起,就出现了粒子效果。再简单点说,只要定义一个类,这个类有绘制和运动就可以了,然后实例化多个,放在列表里面,绘制的时候遍历每个元素就可以了。
所以要在自定义view中加入一个ArrayList就可以了,然后在onDraw和logic里面遍历list里面的每个元素。说到这里了,其实核心也出来了,粒子效果最终追述到的根源,还是单个元素的定制。
那么粒子效果有大家想象的那么难吗,其实没有,起码我写的几个都是在100行代码左右(单个元素),那么,来看看下雨的效果怎么实现的吧。
如果在手机上看,会很清楚和流畅,因为我用视频录制转换成gif,这样分辨率就小了很多,而且帧率也变了。
单个雨点实际上就是一条线,线的大小可以根据自己的喜好定制随机范围。那么需要定义一些基本的变量。显示的宽,高,随机数。
/**
* 显示区域的宽度
*/
protected int width;
/**
* 显示区域的高度
*/
protected int height;
/**
* 效果元素的随机对象
*/
protected Random rand;
上面的宽高是通过View的getWidth()和getHeight()来传递的。rand也是在构造的时候就已经实例化了。
public EffectItem(int width, int height){
this.width = width;
this.height = height;
rand =new Random();
}
上面的EffectItem其实就是Rain,只不过我把它抽离出来了,方便以后其他粒子复用,但是不影响阅读,把它看成Rain就可以了。绘制的区域出来了,那么绘制的线呢?这就通过rand来生成,先看看系统绘制直线的方法:
public void drawLine(float startX, float startY, float stopX, float stopY, Paint paint) {
native_drawLine(mNativeCanvas, startX, startY, stopX, stopY, paint.mNativePaint);
}
需要起始x,y和终点x,y。系统有个Rect的类,它有left, top, right, bottom。所以可以使用这个类来辅助。
<span style="white-space:pre"> </span>int x = rand.nextInt(width);
int y = rand.nextInt(height);
int w = rand.nextInt(size / 2);
int h = rand.nextInt(size);
w = w > h ? h : w;
point.left = x;
point.top = y;
point.right = x - w;
point.bottom = y + h;
size的值可以自己定,我定的是50像素,这样一条线段的坐标就出来了,只要draw里面使用就可以了
public void draw(Canvas canvas){
canvas.drawLine(point.left, point.top, point.right, point.bottom, paint);
}
还记得我前面说的吗,每个元素都会在自定义的View里面的onDraw(Canvas)里面遍历绘制,所以不用途担心这里面的draw不起作用。ok,绘制线出来了,但是,你会发现这个线不动,因为没有逻辑让它动起来。
那么该怎么动呢,对,用速度,要让它往下落,那么y的坐标就一直在变,所有y方向有有个速度,但是你又会发现,一个斜杠在往下掉,不符合自然规律,应该让它按照它的角度和方向落下,听的好难啊啊,其实就2行代码,上面代码你会很奇怪有个w,h,这次有用了,稍微在纸上画画,你会发现这条线段是按照一定规律,即一个系数(是几不重要),只要x方向乘以w和y方向乘以h就可以了,所以如果定义速度的话就好办多了。
private void reset(){
int x = rand.nextInt(width);
int y = rand.nextInt(height);
int w = rand.nextInt(size / 2);
int h = rand.nextInt(size);
w = w > h ? h : w;
point.left = x;
point.top = y;
point.right = x - w;
point.bottom = y + h;
int speedX = w;
int speedY = h;
speedX = speedX == 0 ? 1 : speedX;
speedY = speedY == 0 ? 1 : speedY;
speedX = speedX > speedY ? speedY : speedX;
speed.x = -speedX;
speed.y = speedY;
}
上面就是生成一个雨点的方法,speed是个Point的对象,这样可以表示x和y方向上的速度。
生成和绘制都有了,就差运动了
public void move(){
point.left += speed.x;
point.top += speed.y;
point.right = point.right + speed.x;
point.bottom = point.bottom + speed.y;
if(point.left < 0 || point.left > width || point.bottom > height){
reset();
}
speed.y += rand.nextBoolean() ? 1 : 0;
}
上面就是运动的方法,就是简单的把位置变换了下,但是当出绘制区域的时候,会重新生成一个雨点,所以,屏幕上实际上就是固定数目的雨点的不停的变换位置。最后的y方向的速度是不定时的将y方向加入加速度,这样就会有重力似的效果。每个元素move方法其实就是在自定义View中的logic中被遍历的。
到此位置,一个雨点的生成,绘制,运动都完成了,只要加入到list中然后在View里面遍历就基本完成了。一个雨点完整的代码如下:
public class RainPoint extends EffectItem{
private Paint paint = new Paint();
private final int size = 50; //长度在0-50像素
private Rect point; //雨点
private Point speed; //雨点x,y方向速度
public RainPoint(int width, int height){
super(width, height);
point = new Rect();
speed = new Point();
paint.setColor(0xffffffff);
reset();
}
public void draw(Canvas canvas){
canvas.drawLine(point.left, point.top, point.right, point.bottom, paint);
}
public void move(){
point.left += speed.x;
point.top += speed.y;
point.right = point.right + speed.x;
point.bottom = point.bottom + speed.y;
if(point.left < 0 || point.left > width || point.bottom > height){
reset();
}
speed.y += rand.nextBoolean() ? 1 : 0;
}
private void reset(){
int x = rand.nextInt(width);
int y = rand.nextInt(height);
int w = rand.nextInt(size / 2);
int h = rand.nextInt(size);
w = w > h ? h : w;
point.left = x;
point.top = y;
point.right = x - w;
point.bottom = y + h;
int speedX = w;
int speedY = h;
speedX = speedX == 0 ? 1 : speedX;
speedY = speedY == 0 ? 1 : speedY;
speedX = speedX > speedY ? speedY : speedX;
speed.x = -speedX;
speed.y = speedY;
}
}
而EffectItem代码如下:
<pre name="code" class="java">public abstract class EffectItem implements EffectBase{
/**
* 显示区域的宽度
*/
protected int width;
/**
* 显示区域的高度
*/
protected int height;
/**
* 效果元素的随机对象
*/
protected Random rand;
public EffectItem(int width, int height){
this.width = width;
this.height = height;
rand =new Random();
}
}
而
EffectBase接口定义了两个方法:
public interface EffectBase {
/**
* 绘制效果
* @param canvas
*/
public void draw(Canvas canvas);
/**
* 效果元素变化
*/
public void move();
}
这样,一个粒子的完整过程就出来了,而后面其它的粒子也是按照这个模型就行设计的。剩下的就是在list里面加入多个实例了,然后按部就班的在onDraw里面遍历每个元素的draw方法,在logic里面遍历move方法。
<span style="white-space:pre"> </span><span style="color:#ff0000;">最重要的是,因为这是自定义View,可以像使用其他View一样,放到布局里面,所以丰富它,可以有无限的遐想展示空间。</span>
变化雨点颜色(在xml中定义属性就可以了)
每个雨点都随机颜色(在xml中定义属性就可以了)
还可以进行区域clip哦
好了,如果谁有好的想法也可以共享一下哦,看看如果能实现,看看效果。详细代码:https://github.com/xianfeng99/Particle
github上面的代码可能没有及时更新。