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

作者: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();

效果如图:

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

原文发布于微信公众号 - Android先生(cyg_24kshign)

原文发表时间:2017-11-24

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Android点滴积累

Android XML shape 标签使用详解(apk瘦身,减少内存好帮手)

  一个android开发者肯定懂得使用 xml 定义一个 Drawable,比如定义一个 rect 或者 circle 作为一个 View 的背景。但是,也肯...

1210
来自专栏郭霖

Android中轴旋转特效实现,制作别样的图片浏览器

Android API Demos中有很多非常Nice的例子,这些例子的代码都写的很出色,如果大家把API Demos中的每个例子研究透了,那么恭喜你已经成为一...

2566
来自专栏知识分享

3-系统方案A(Activity界面跳转,携带数据,显示曲线界面)

https://www.cnblogs.com/yangfengwu/p/9970387.html

972
来自专栏Android点滴积累

Android XML shape 标签使用详解(apk瘦身,减少内存好帮手)

Android XML shape 标签使用详解   一个android开发者肯定懂得使用 xml 定义一个 Drawable,比如定义一个 rect 或者 c...

3497
来自专栏郭霖

Android PowerImageView实现,可以播放动画的强大ImageView

我个人是比较喜欢逛贴吧的,贴吧里总是会有很多搞笑的动态图片,经常看一看就会感觉欢乐很多,可以释放掉不少平时的压力。确实,比起一张单调的图片,动态图片明显更加的有...

2925
来自专栏Android小菜鸡

沉浸式状态栏的封装使用

  随着用户要求的不断提高,Android版本的不断升级,沉浸式状态栏似乎已经成为了每个App的必备功能。   首先要实现它我们得先理解他,状态栏不同于标题栏...

2591
来自专栏androidBlog

使用CoordinatorLayout打造各种炫酷的效果

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/gdutxiaoxu/article/details...

8241
来自专栏向治洪

仿uc下部弹出菜单

先说说我怎么会无聊到这种地步去弄这个代码呢,在今年2月份的时候公司本来要做个这种弹出的菜单的,有5个按钮每个都有一个菜单,记得网上有仿UC菜单的源码,就下下来看...

2198
来自专栏Android知识点总结

2-VIII--ViewPager滑动监听与自定义滑动特效

1221
来自专栏三流程序员的挣扎

Android 动画总结(9) - 过渡动画

前面已经介绍过一部分 Activity 之间的过渡动画。现在讲的不是 Activity 转场,而是同一个页面的 View 之间的过渡。

4171

扫码关注云+社区

领取腾讯云代金券