前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Android自定义View——从零开始实现雪花飘落效果

Android自定义View——从零开始实现雪花飘落效果

作者头像
用户2802329
发布2018-08-07 14:40:44
1.7K0
发布2018-08-07 14:40:44
举报
文章被收录于专栏:Android先生Android先生

作者:Anlia 地址:http://www.jianshu.com/p/ce704b03f3f5 声明:本文是Anlia原创,已获其授权发布,未经原作者允许请勿转载 大家要是看到有错误的地方或者有啥好的建议,欢迎留言评论

前言:转眼已是十一月下旬了,天气慢慢转冷,不知道北方是不是已经开始下雪了呢?本期教程我们就顺应季节主题,一起来实现 雪花飘落的效果吧。本篇效果思路参考自国外大神的Android实现雪花飞舞效果,并在此基础上实现进一步的封装和功能扩展

本篇只着重于思路和实现步骤,里面用到的一些知识原理不会非常细地拿来讲,如果有不清楚的api或方法可以在网上搜下相应的资料,肯定有大神讲得非常清楚的,我这就不献丑了。本着认真负责的精神我会把相关知识的博文链接也贴出来(其实就是懒不想写那么多哈哈),大家可以自行传送。为了照顾第一次阅读系列博客的小伙伴,本篇会出现一些在之前系列博客就讲过的内容,看过的童鞋自行跳过该段即可

国际惯例,先上效果图:

目录

  • 绘制一个循环下落的“雪球”
  • 封装下落物体对象
  • 扩展一:增加导入Drawable资源的构造方法和设置物体大小的接口
  • 扩展一:扩展二:实现雪花“大小不一”、“快慢有别”的效果
  • 扩展三:引入“风”的概念

绘制一个循环下落的“雪球”

我们先从最简单的部分做起,自定义View中实现循环动画的方法有很多,最简单直接的当然是用Animation类去实现,但考虑到无论是雪花、雪球亦或是雨滴什么的,每个独立的个体都有自己的起点、速度和方向等等,其下落的过程会出现很多随机的因素,实现这种非规律的动画Animation类就不怎么适用了,因此我们这次要利用线程通信实现一个简单的定时器,达到周期性绘制View的效果。这里我们简单绘制一个“雪球”(其实就是个白色背景的圆形哈哈)来看看定时器的效果,新建一个FallingView

public class FallingView extends View {

    private Context mContext;
    private AttributeSet mAttrs;

    private int viewWidth;
    private int viewHeight;

    private static final int defaultWidth = 600;
    private static final int defaultHeight = 1000;
    private static final int intervalTime = 5;

    private Paint testPaint;
    private int snowY;

    public FallingView(Context context) {
        super(context);
        mContext = context;
        init();
    }

    public FallingView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        mContext = context;
        mAttrs = attrs;
        init();
    }

    private void init(){
        testPaint = new Paint();
        testPaint.setColor(Color.WHITE);
        testPaint.setStyle(Paint.Style.FILL);
        snowY = 0;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int height = measureSize(defaultHeight, heightMeasureSpec);
        int width = measureSize(defaultWidth, widthMeasureSpec);
        setMeasuredDimension(width, height);

        viewWidth = width;
        viewHeight = height;
    }

    private int measureSize(int defaultSize,int measureSpec) {
        int result = defaultSize;
        int specMode = View.MeasureSpec.getMode(measureSpec);
        int specSize = View.MeasureSpec.getSize(measureSpec);

        if (specMode == View.MeasureSpec.EXACTLY) {
            result = specSize;
        } else if (specMode == View.MeasureSpec.AT_MOST) {
            result = Math.min(result, specSize);
        }
        return result;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawCircle(100,snowY,25,testPaint);
        getHandler().postDelayed(runnable, intervalTime);
    }

    
    private Runnable runnable = new Runnable() {
        @Override
        public void run() {
            snowY += 15;
            if(snowY>viewHeight){
                snowY = 0;
            }
            invalidate();
        }
    };
}

效果如图:

在上述代码中View基本的框架我们已经搭好了,思路其实很简单,我们需要做仅仅是在每次重绘之前更新做下落运动的物体的位置即可

封装下落物体对象

相关博文链接 Android开发中无处不在的设计模式——Builder模式:http://blog.csdn.net/sbsujjbcy/article/details/49208969 [Android] 获取View的宽度和高度:http://www.jianshu.com/p/d18f0c96acb8

要实现大雪纷飞的效果,很明显只有一个雪球是不够的,而且雪也不能只有雪球一个形状,我们希望可以自定义雪的样式,甚至不局限于下雪,还可以下雨、下金币等等,因此我们要对下落的物体进行封装。为了以后物体类对外方法代码的可读性,这里我们采用Builder设计模式来构建物体对象类,新建FallObject

public class FallObject {
    private int initX;
    private int initY;
    private Random random;
    private int parentWidth;
    private int parentHeight;
    private float objectWidth;
    private float objectHeight;

    public int initSpeed;

    public float presentX;
    public float presentY;
    public float presentSpeed;

    private Bitmap bitmap;
    public Builder builder;

    private static final int defaultSpeed = 10;

    public FallObject(Builder builder, int parentWidth, int parentHeight){
        random = new Random();
        this.parentWidth = parentWidth;
        this.parentHeight = parentHeight;
        initX = random.nextInt(parentWidth);
        initY = random.nextInt(parentHeight)- parentHeight;
        presentX = initX;
        presentY = initY;

        initSpeed = builder.initSpeed;

        presentSpeed = initSpeed;
        bitmap = builder.bitmap;
        objectWidth = bitmap.getWidth();
        objectHeight = bitmap.getHeight();
    }

    private FallObject(Builder builder) {
        this.builder = builder;
        initSpeed = builder.initSpeed;
        bitmap = builder.bitmap;
    }

    public static final class Builder {
        private int initSpeed;
        private Bitmap bitmap;

        public Builder(Bitmap bitmap) {
            this.initSpeed = defaultSpeed;
            this.bitmap = bitmap;
        }

        
        public Builder setSpeed(int speed) {
            this.initSpeed = speed;
            return this;
        }

        public FallObject build() {
            return new FallObject(this);
        }
    }

    
    public void drawObject(Canvas canvas){
        moveObject();
        canvas.drawBitmap(bitmap,presentX,presentY,null);
    }

    
    private void moveObject(){
        moveY();
        if(presentY>parentHeight){
            reset();
        }
    }

    
    private void moveY(){
        presentY += presentSpeed;
    }

    
    private void reset(){
        presentY = -objectHeight;
        presentSpeed = initSpeed;
    }
}

FallingView中相应地设置添加物体的方法

public class FallingView extends View {
    
    private List<FallObject> fallObjects;

    private void init(){
        fallObjects = new ArrayList<>();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if(fallObjects.size()>0){
            for (int i=0;i<fallObjects.size();i++) {
                
                fallObjects.get(i).drawObject(canvas);
            }
            
            getHandler().postDelayed(runnable, intervalTime);
        }
    }

    
    private Runnable runnable = new Runnable() {
        @Override
        public void run() {
            invalidate();
        }
    };

    
    public void addFallObject(final FallObject fallObject, final int num) {
        getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
            @Override
            public boolean onPreDraw() {
                getViewTreeObserver().removeOnPreDrawListener(this);
                for (int i = 0; i < num; i++) {
                    FallObject newFallObject = new FallObject(fallObject.builder,viewWidth,viewHeight);
                    fallObjects.add(newFallObject);
                }
                invalidate();
                return true;
            }
        });
    }
}

Activity中向FallingView添加一些物体看看效果

snowPaint = new Paint();
snowPaint.setColor(Color.WHITE);
snowPaint.setStyle(Paint.Style.FILL);
bitmap = Bitmap.createBitmap(50, 50, Bitmap.Config.ARGB_8888);
bitmapCanvas = new Canvas(bitmap);
bitmapCanvas.drawCircle(25,25,25,snowPaint);


FallObject.Builder builder = new FallObject.Builder(bitmap);
FallObject fallObject = builder
        .setSpeed(10)
        .build();

fallingView = (FallingView) findViewById(R.id.fallingView);
fallingView.addFallObject(fallObject,50);

效果如图:

到这里我们完成了一个最基础的下落物体类,下面开始扩展功能和效果

扩展一:增加导入Drawable资源的构造方法和设置物体大小的接口

我们之前的FallObject类中Builder只支持bitmap的导入,很多时候我们的图片样式都是从drawable资源文件夹中获取的,每次都要将drawable转成bitmap是件很麻烦的事,因此我们要在FallObject类中封装drawable资源导入的构造方法,修改FallObject

public static final class Builder {
    
    public Builder(Bitmap bitmap) {
        this.initSpeed = defaultSpeed;
        this.bitmap = bitmap;
    }

    public Builder(Drawable drawable) {
        this.initSpeed = defaultSpeed;
        this.bitmap = drawableToBitmap(drawable);
    }
}public static Bitmap drawableToBitmap(Drawable drawable) {
    Bitmap bitmap = Bitmap.createBitmap(
            drawable.getIntrinsicWidth(),
            drawable.getIntrinsicHeight(),
            drawable.getOpacity() != PixelFormat.OPAQUE ? Bitmap.Config.ARGB_8888
                    : Bitmap.Config.RGB_565);
    Canvas canvas = new Canvas(bitmap);
    drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
    drawable.draw(canvas);
    return bitmap;
}

有了drawable资源导入的构造方法,肯定需要配套改变FallObject图片样式大小的接口,依然是在FallObjectBuilder中扩展相应的接口

public static final class Builder {
    
    public Builder setSize(int w, int h){
        this.bitmap = changeBitmapSize(this.bitmap,w,h);
        return this;
    }
}public static Bitmap changeBitmapSize(Bitmap bitmap, int newW, int newH) {
    int oldW = bitmap.getWidth();
    int oldH = bitmap.getHeight();
    
    float scaleWidth = ((float) newW) / oldW;
    float scaleHeight = ((float) newH) / oldH;
    
    Matrix matrix = new Matrix();
    matrix.postScale(scaleWidth, scaleHeight);
    
    bitmap = Bitmap.createBitmap(bitmap, 0, 0, oldW, oldH, matrix, true);
    return bitmap;
}

Activity中初始化下落物体样式时我们就可以导入drawable资源和设置物体大小了(图片资源我是在阿里图标库下载的)

FallObject.Builder builder = new FallObject.Builder(getResources().getDrawable(R.drawable.ic_snow));
FallObject fallObject = builder
        .setSpeed(10)
        .setSize(50,50)
        .build();

来看下效果:

扩展二:实现雪花“大小不一”、“快慢有别”的效果

之前我们通过导入drawable资源的方法让屏幕“下起了雪花”,但雪花个个都一样大小,下落速度也都完全一致,这显得十分的单调,看起来一点也不像现实中的下雪场景。因此我们需要利用随机数实现雪花“大小不一”“快慢有别”的效果,修改FallObject

public class FallObject {
    
    private boolean isSpeedRandom;
    private boolean isSizeRandom;

    public FallObject(Builder builder, int parentWidth, int parentHeight){
        
        this.builder = builder;
        isSpeedRandom = builder.isSpeedRandom;
        isSizeRandom = builder.isSizeRandom;

        initSpeed = builder.initSpeed;
        randomSpeed();
        randomSize();
    }

    private FallObject(Builder builder) {
        
        isSpeedRandom = builder.isSpeedRandom;
        isSizeRandom = builder.isSizeRandom;
    }

    public static final class Builder {
        
        private boolean isSpeedRandom;
        private boolean isSizeRandom;

        public Builder(Bitmap bitmap) {
            
            this.isSpeedRandom = false;
            this.isSizeRandom = false;
        }

        public Builder(Drawable drawable) {
            
            this.isSpeedRandom = false;
            this.isSizeRandom = false;
        }

        
        public Builder setSpeed(int speed) {
            this.initSpeed = speed;
            return this;
        }

        
        public Builder setSpeed(int speed,boolean isRandomSpeed) {
            this.initSpeed = speed;
            this.isSpeedRandom = isRandomSpeed;
            return this;
        }

        
        public Builder setSize(int w, int h){
            this.bitmap = changeBitmapSize(this.bitmap,w,h);
            return this;
        }

        
        public Builder setSize(int w, int h, boolean isRandomSize){
            this.bitmap = changeBitmapSize(this.bitmap,w,h);
            this.isSizeRandom = isRandomSize;
            return this;
        }
    }

    
    private void reset(){
        presentY = -objectHeight;
        randomSpeed();
    }

    
    private void randomSpeed(){
        if(isSpeedRandom){
            presentSpeed = (float)((random.nextInt(3)+1)*0.1+1)* initSpeed;
        }else {
            presentSpeed = initSpeed;
        }
    }

    
    private void randomSize(){
        if(isSizeRandom){
            float r = (random.nextInt(10)+1)*0.1f;
            float rW = r * builder.bitmap.getWidth();
            float rH = r * builder.bitmap.getHeight();
            bitmap = changeBitmapSize(builder.bitmap,(int)rW,(int)rH);
        }else {
            bitmap = builder.bitmap;
        }
        objectWidth = bitmap.getWidth();
        objectHeight = bitmap.getHeight();
    }
}

Activity中设置相应参数即可

FallObject.Builder builder = new FallObject.Builder(getResources().getDrawable(R.drawable.ic_snow));
FallObject fallObject = builder
        .setSpeed(10,true)
        .setSize(50,50,true)
        .build();

效果如图,是不是看起来感觉好多了๑乛◡乛๑

扩展三:引入“风”的概念

“风”其实是一种比喻,实际上要做的是让雪花除了做下落运动外,还会横向移动,也就是说我们要模拟出雪花在风中乱舞的效果。为了让雪花在X轴上的位移不显得鬼畜(大家可以直接随机增减x坐标值就知道为什么是鬼畜了哈哈),我们采用正弦函数来获取X轴上的位移距离,如图所示:

正弦函数曲线见下图:

我们选取-π到π这段曲线,可以看出角的弧度在为π/2时正弦值最大(-π/2时最小),因此我们在计算角度时还需要考虑其极限值。同时,因为我们添加了横向的移动,所以判断边界时要记得判定最左和最右的边界,修改FallObject

public class FallObject {
    
    public int initSpeed;
    public int initWindLevel;
    
    private float angle;
    
    private boolean isWindRandom;
    private boolean isWindChange;

    private static final int defaultWindLevel = 0;
    private static final int defaultWindSpeed = 10;
    private static final float HALF_PI = (float) Math.PI / 2;

    public FallObject(Builder builder, int parentWidth, int parentHeight){
        
        isWindRandom = builder.isWindRandom;
        isWindChange = builder.isWindChange;

        initSpeed = builder.initSpeed;
        randomSpeed();
        randomSize();
        randomWind();
    }

    private FallObject(Builder builder) {
        
        isWindRandom = builder.isWindRandom;
        isWindChange = builder.isWindChange;
    }

    public static final class Builder {
        
        private boolean isWindRandom;
        private boolean isWindChange;

        public Builder(Bitmap bitmap) {
            
            this.isWindRandom = false;
            this.isWindChange = false;
        }

        public Builder(Drawable drawable) {
            
            this.isWindRandom = false;
            this.isWindChange = false;
        }

        
        public Builder setWind(int level,boolean isWindRandom,boolean isWindChange){
            this.initWindLevel = level;
            this.isWindRandom = isWindRandom;
            this.isWindChange = isWindChange;
            return this;
        }
    }

    
    private void moveObject(){
        moveX();
        moveY();
        if(presentY>parentHeight || presentX<-bitmap.getWidth() || presentX>parentWidth+bitmap.getWidth()){
            reset();
        }
    }

    
    private void moveX(){
        presentX += defaultWindSpeed * Math.sin(angle);
        if(isWindChange){
            angle += (float) (random.nextBoolean()?-1:1) * Math.random() * 0.0025;
        }
    }

    
    private void reset(){
        presentY = -objectHeight;
        randomSpeed();
        randomWind();
    }

    
    private void randomWind(){
        if(isWindRandom){
            angle = (float) ((random.nextBoolean()?-1:1) * Math.random() * initWindLevel /50);
        }else {
            angle = (float) initWindLevel /50;
        }

        
        if(angle>HALF_PI){
            angle = HALF_PI;
        }else if(angle<-HALF_PI){
            angle = -HALF_PI;
        }
    }
}

Activity中调用新增加的接口

FallObject.Builder builder = new FallObject.Builder(getResources().getDrawable(R.drawable.ic_snow));
FallObject fallObject = builder
        .setSpeed(7,true)
        .setSize(50,50,true)
        .setWind(5,true,true)
        .build();

效果如图:

至此本篇教程到此结束,如果大家看了感觉还不错麻烦点个赞,你们的支持是我最大的动力~

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2017-11-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Android先生 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 扩展一:增加导入Drawable资源的构造方法和设置物体大小的接口
  • 扩展二:实现雪花“大小不一”、“快慢有别”的效果
  • 扩展三:引入“风”的概念
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档