文章目录
- 第一步:构造一个雨滴类
- 第二步:构造一个工厂类
- 小结
先来看一下效果:
根据效果图,我们来分析一下需求。
- 1、雨滴由一条线段构成
- 2、一个RainWidget包含许多雨滴
- 3、雨滴的长度、宽度、速度、透明度是随机的
- 4、雨滴向下滴落,当超出屏幕高度,将重新随机在屏幕上边缘生成
- 5、一打开应用,雨滴就随机分布在屏幕,而不是出生在屏幕最上方
接下来,我们就一步一步分析该View是如何实现的
第一步:构造一个雨滴类
//雨滴类 以一根线条作为雨滴效果
public class Drip{
//雨滴出生点
public Point bronPoint;
//长度点 **一条线由两个点构成**
public Point lengthPoint;
//雨滴速度
public int speed;
//水滴长度
public int height;
//雨滴宽度
public int width;
//雨滴透明度
public int alpha;
//屏幕宽度
private int mScreenWidth;
//屏幕高度
private int mScreenHeight;
public Drip(int screenWidth,int screenHeight, int speed,int height,int width,int alpha) {
this.mScreenWidth=screenWidth;
this.mScreenHeight=screenHeight;
this.speed = speed;
this.width = width;
this.height=height;
this.alpha = alpha;
//雨滴一旦被创建,就调用initPoint()方法,
//在手机屏幕随机生成雨滴的两个点
initPoint(screenWidth,screenHeight);
}
//该方法用于设置雨滴两个点的坐标
private void initPoint(int screenWidth,int screenHeight){
//出生点的设置。第一次打开应用,出生点一定是随机生成的
//所以x坐标随机范围是(0,屏幕宽度)
//所以y坐标随机范围是(0,屏幕高度)
bronPoint=new Point((int)(Math.random()*screenWidth),(int)(Math.random()*screenHeight));
//第二个点的坐标就好确定了
//x坐标就是第一个点的坐标
//y坐标可以控制雨滴的长度,也就是线条的长度
//y坐标就是第一个点的y坐标+雨滴长度
lengthPoint=new Point(bronPoint.x,bronPoint.y+height);
}
//rain()方法,是雨滴下落的效果
//雨滴下落是由线条两个点的坐标变化而变化的
//首先,两个点的x轴不会发生变化,而y轴的增减量是相同的
public void rain(int screenHeight){
//通过Point.offset()方法,使得y点增加一个speed值
//这只是每一帧动画的效果
bronPoint.offset(0,speed);
lengthPoint.offset(0,speed);
//当雨滴的y坐标大于屏幕高度,那么就重新生成雨滴的两个点的坐标
//第一个参数是控制生成雨滴x坐标的范围
//第二个参数是控制生成雨滴y坐标的范围
//因为重新生成的雨滴必须是从屏幕最上面落下来,所以第二个参数默认是0
if (bronPoint.y>mScreenHeight){
initPoint(mScreenWidth,0);
}
}
}
第二步:构造一个工厂类
通过工厂模式,我们可以漂亮的构造出大量的雨滴
//雨滴工厂,批量生产雨滴
public class DripFactory{
public static Drip createDrip(int mScreenWidth, int mScreenHeight){
//以下四个属性的值,都是通过随机来生成的
//笔者因为偷懒,并没有将随机的默认值和最大值抽离出来
//如果抽离出来,就可以在xml文件中动态控制各个属性
int speed= (int) (Math.random()*10+5); //默认最小5 最大15
int width=(int) (Math.random()*8+3);
int alpha=(int) (Math.random()*200+20);
int height=(int) (Math.random()*30+20);
Drip drip=new Drip(mScreenWidth,mScreenHeight,speed,height,width,alpha);
return drip;
}
}
通过这个工厂类,我们只需要调用一次DripFactory.createDrip(),就可以创建一个雨滴。
##第三步:创建RainWidget
public class RainWidget extends View{
//画笔
private Paint mPaint;
//雨点集合
private List<Drip> drips=null;
//屏幕宽度
private int mWidth;
//屏幕高度
private int mHeight;
}
我们在构造方法中初始化Paint:
public RainWidget(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mPaint=new Paint();
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(Color.WHITE);
}
创建雨滴,我们需要屏幕的宽度和高度,所以我们在onSizeChanged()方法中初始化高宽:
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth=w;
mHeight=h;
}
创建雨滴的方法:
//方法参数为雨滴的数目
private List<Drip> initDrips(int dripNumber) {
List<Drip> drips=new ArrayList<>();
for (int i=0;i<dripNumber;i++){
drips.add(DripFactory.createDrip(mWidth,mHeight));
}
return drips;
}
在onDraw()方法中,我们将所有的雨滴画出来:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//通过for循环,遍历所有雨滴,并且画出来
for (Drip drip:drips){
mPaint.setAlpha(drip.alpha);
mPaint.setStrokeWidth(drip.width);
canvas.drawLine(drip.bronPoint.x,drip.bronPoint.y,drip.lengthPoint.x,drip.lengthPoint.y,mPaint);
}
}
万事具备,只欠如何让雨滴动起来。
先前,我们创建雨滴类的时候,有个rain()方法,雨滴实例调用这个方法,就会使其位置向下位移speed个单位,这只是一帧的效果。那么如何实现一直动的效果呢?
我们的想法就是通过一个while(true)死循环来包裹这个方法,并且每当雨滴实例调用一次rain()方法,就会停下15ms,这样就能达到每15ms雨滴就会下落一点距离,在1s内就会调用rain()方法超过60帧,达到流畅的效果。
理论上人眼觉得不卡是24帧左右,你们可以随意设置这个暂停的时间大小,流不流畅你们自己看。
所以,我们就要新开一个子线程,来执行任务。这里,我们就直接让RainWidger类实现Runnable接口:
public class RainWidget extends View implements Runnable
run方法:
@Override
public void run() {
while (true){
//遍历所有的雨滴,并执行一帧下雨的动作
for (Drip drip:drips){
drip.rain();
}
try {
//线程sleep 15ms,然后调用postInvalidate()来进行重绘
Thread.sleep(15);
postInvalidate();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
这样,所有的方法都已经完成。那么,我们就开始调用这些方法吧,由于创建雨滴我们需要屏幕高宽,所以,我们必须要在高宽的值被确定后,才能调用initDrips()方法。
所以,我们将这个方法写在onSizeChanged()里面:
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth=w;
mHeight=h;
if (drips!=null){
drips.clear();
}
drips=initDrips(50);
//开启线程
new Thread(this).start();
}
小结
关于优化方案:
- 事实上我们的绘制速度太快了,而且需要绘制的对象非常多,如果使用 SurfaceView 的话就能够缓解主线程的压力。
- 我们创建了很多的 雨滴对象,这些对象实际上会有重复的类型,如果使用 享元模式 的话,会节省一些内存……虽然实现过程会有些费脑子……
- 我们的线程需要在 View 被销毁的时候也能够被取消,可以将线程对象拿出来,然后在 View 的
onDetachFromWindow()
中进行取消的操作。