Android自定义View——下雨效果


相关链接:Android自定义View——滚动条

先来看一下效果:
这里写图片描述
根据效果图,我们来分析一下需求。

  • 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();
    }

小结

关于优化方案:

  1. 事实上我们的绘制速度太快了,而且需要绘制的对象非常多,如果使用 SurfaceView 的话就能够缓解主线程的压力。
  2. 我们创建了很多的 雨滴对象,这些对象实际上会有重复的类型,如果使用 享元模式 的话,会节省一些内存……虽然实现过程会有些费脑子……
  3. 我们的线程需要在 View 被销毁的时候也能够被取消,可以将线程对象拿出来,然后在 ViewonDetachFromWindow() 中进行取消的操作。
  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值