前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >手撸一个物体下落的控件,实现雪花飘落效果

手撸一个物体下落的控件,实现雪花飘落效果

作者头像
饮水思源为名
发布2018-12-28 14:54:11
1.3K0
发布2018-12-28 14:54:11
举报
文章被收录于专栏:Android小菜鸡Android小菜鸡

效果图:

圣诞登录页.gif

参考文章:

Android自定义View——从零开始实现雪花飘落效果 感谢原文作者,不仅实现了效果,并且写得非常详细,还做了优化。笔者参考原文作者的源码,做了一点修改,实现了效果并加入了项目中。不过都大同小异,下面笔者会将学习和制作中的难点和注意点分享给大家。

提炼与分享:

1. 如何实现简单的物体下落:

代码语言:javascript
复制
    @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 drawObject(Canvas canvas){
        moveObject();
        canvas.drawBitmap(bitmap,presentX,presentY,null);
    }
    private void moveObject(){
        moveX();
        moveY();
        if(presentY>parentHeight || presentX<-bitmap.getWidth() || presentX>parentWidth+bitmap.getWidth()){
            reset();
        }
    }
    private void moveY(){
        presentY += presentSpeed;
    }
    private void moveX(){
        presentX += defaultWindSpeed * Math.sin(angle);
        if(isAngleChange){
            angle += (float) (random.nextBoolean()?-1:1) * Math.random() * 0.0025;
        }
    }
    private void reset(){
        presentY = -objectHeight;
        randomSpeed();
        randomWind();//记得重置一下初始角度,不然雪花会越下越少(因为角度累加会让雪花越下越偏)
    }

  首先是Y轴控制竖直下落,初始的Y轴坐标是通过屏幕高度取随机值-屏幕高度来确定的。这样物体会从不同的位置下落,在相同速度的情况下,也能在不同的时间进入屏幕。   然后是X轴,正常的雪花肯定不是竖直下落,也不是折线下落,而是弧形,View中采用的sin函数的-Pi到Pi之间的值绘制弧形。x轴的初始位置通过对屏幕宽度做随机值确定。   最后在物体到底屏幕底部,或者超过屏幕左右边界时,重置物体(reset方法)。需要重置的是y轴的点,以及物体的速度,当然还有我们模拟的风力,后面会单独说。

2. 为什么要使用Builder建造者模式

  其实原文已经讲得很仔细了,我们物体会有大量的参数和对应的行为方法,为了提高代码的可读性,我们将物体提取出来,作为一个单独的类。而大量的参数采用普通的构造方法去构造,实在是不知道,传入的参数究竟代表什么。而建造者模式能够解决这个问题。

代码语言:javascript
复制
        FallObject.Builder builder = new FallObject.Builder(getResources().getDrawable(R.drawable.snowflake));
        FallObject fallObject = builder
                .setSpeed(8,true)
                .setSize(80,80,true)
                .setWind(5,true)
                .build();
        //初始化一个雪球样式的fallObject
        ((FallingView)findViewById(R.id.fallingView)).addFallObject(fallObject,60);//添加60个雪球对象

  在这个项目中,我们将所有与下落物体相关的方法和属性全部封装在FallObject中,并且提供Builder内部类实例化。而我们的View则仅仅需要作为一个画布,提供添加下落对象的方法,重复的绘制物体即可。至于绘制的对象是要下落还是要旋转,都与View没有关系了。

3. 绘制图片并且控制其大小

  绘制图片在View中是有提供方法的:canvas.drawBitmap(bitmap,presentX,presentY,null);从方法中可以看到,我们需要的是bitmap的图片,那么,我们在修改图片大小之前,还需要先将drawable转化为bitmap。

代码语言:javascript
复制
    /**
     * drawable图片资源转bitmap
     * @param drawable
     * @return
     */
    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转化为bitmap的代码,简单来讲就是Bitmap的采用的是工厂模式创建一个bitmap空对象,然后通过drawable将图片图像画在bitmap对象中。

代码语言:javascript
复制
    /**
     * 改变bitmap的大小
     * @param bitmap 目标bitmap
     * @param newW 目标宽度-
     * @param newH 目标高度
     * @return
     */
    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 matrix = new Matrix();
        matrix.postScale(scaleWidth, scaleHeight);
        // 得到新的图片
        bitmap = Bitmap.createBitmap(bitmap, 0, 0, oldW, oldH, matrix, true);
        return bitmap;
    }

  上面是改变图片显示大小的方法。就是直接对bitmap的缩放操作返回新的bitmao对象。需要注意的是为了保证不失帧,新的宽高度需要按原大小等比例缩放。

4. 如何引入风的概念

代码语言:javascript
复制
    /**
     * 随机风的风向和风力大小比例,即随机物体初始下落角度
     */
    private void randomWind(){
        if(isAngleChange){
            angle = (float) ((random.nextBoolean()?-1:1) * Math.random() * initWindLevel /50);
        }else {
            angle = (float) initWindLevel /50;
        }
        //限制angle的最大最小值
        if(angle>HALF_PI){
            angle = HALF_PI;
        }else if(angle<-HALF_PI){
            angle = -HALF_PI;
        }
    }

  正常情况下,我们的雪花不会是直线下落的,而是有轻微的弧度飘落,我们通过改变X轴的方式来实现水平位移,但是为了保证位移的平滑,我们采用了sin正弦函数计算x轴的值,采用-π/2到π/2的弧线值作为函数的角度。这个曲线值是[-1,1],可以实现雪花自由的左右弧线移动。initWindLevel是我们模拟的风力,风力值越大,雪花飘落的弧度就越大。

源码:

画布View:

代码语言:javascript
复制
package com.wusy.wusylibrary.view.FallingView;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewTreeObserver;

import java.util.ArrayList;
import java.util.List;

/**
 * Created by XIAO RONG on 2018/12/24.
 */

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 = 10;//重绘间隔时间

    private List<FallObject> fallObjects;

    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(){
        fallObjects = new ArrayList<>();
    }

    @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 = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

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

    @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();
        }
    };
    /**
     * 向View添加下落物体对象
     * @param fallObject 下落物体对象
     * @param num
     * view.getViewTreeObserver().addOnPreDrawListener(opdl)
     * 此方法在视图绘制前会被调用,测量结束,客户获取到一些数据。再计算一些动态宽高时可以使用。
     * 调用一次后需要注销这个监听,否则会阻塞ui线程。
     */
    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;
            }
        });
    }
}

下落物体类:

代码语言:javascript
复制
package com.wusy.wusylibrary.view.FallingView;

import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.PixelFormat;
import android.graphics.drawable.Drawable;

import java.util.Random;

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 initWindLevel;//初始风力等级
    private float angle;//下落物体角度
    private boolean isAngleChange;//下落物体角度是否改变
    private static final int defaultWindLevel = 0;//默认风力等级
    private static final int defaultWindSpeed = 10;//默认单位风速
    private static final float HALF_PI = (float) Math.PI / 2;//π/2

    public float initSpeed;//初始下降速度
    public float presentSpeed;//当前下降速度


    public float presentX;//当前位置X坐标
    public float presentY;//当前位置Y坐标

    private boolean isSpeedRandom;//物体初始下降速度比例是否随机
    private boolean isSizeRandom;//物体初始大小比例是否随机

    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);//随机物体的X坐标
        initY = random.nextInt(parentHeight)- parentHeight;//随机物体的Y坐标,并让物体一开始从屏幕顶部下落
        presentX = initX;
        presentY = initY;
        this.builder = builder;
        isSpeedRandom = builder.isSpeedRandom;
        isSizeRandom = builder.isSizeRandom;
        isAngleChange=builder.isAngleChange;
        initWindLevel=builder.initWindLevel;
        randomSpeed();
        randomSize();
    }

    private FallObject(Builder builder) {
        this.builder = builder;
        initSpeed = builder.initSpeed;
        bitmap = builder.bitmap;
        isSpeedRandom = builder.isSpeedRandom;
        isSizeRandom = builder.isSizeRandom;
        isAngleChange=builder.isAngleChange;
        initWindLevel=builder.initWindLevel;
    }
    /**
     * 绘制物体对象
     * @param canvas
     */
    public void drawObject(Canvas canvas){
        moveObject();
        canvas.drawBitmap(bitmap,presentX,presentY,null);
    }

    /**
     * 移动物体对象
     */
    private void moveObject(){
        moveX();
        moveY();
        if(presentY>parentHeight || presentX<-bitmap.getWidth() || presentX>parentWidth+bitmap.getWidth()){
            reset();
        }
    }

    /**
     * Y轴上的移动逻辑
     */
    private void moveY(){
        presentY += presentSpeed;
    }
    private void moveX(){
        presentX += defaultWindSpeed * Math.sin(angle);
        if(isAngleChange){
            angle += (float) (random.nextBoolean()?-1:1) * Math.random() * 0.0025;
        }
    }
    /**
     * 重置object位置
     */
    private void reset(){
        presentY = -objectHeight;
        randomSpeed();
        randomWind();//记得重置一下初始角度,不然雪花会越下越少(因为角度累加会让雪花越下越偏)
    }
    /**
     * 随机风的风向和风力大小比例,即随机物体初始下落角度
     */
    private void randomWind(){
        if(isAngleChange){
            angle = (float) ((random.nextBoolean()?-1:1) * Math.random() * initWindLevel /50);
        }else {
            angle = (float) initWindLevel /50;
        }
        //限制angle的最大最小值
        if(angle>HALF_PI){
            angle = HALF_PI;
        }else if(angle<-HALF_PI){
            angle = -HALF_PI;
        }
    }
    /**
     * 随机物体初始下落速度
     */
    private void randomSpeed(){
        if(isSpeedRandom){
            initSpeed = (float)((random.nextInt(3)+1)*0.1+1)* builder.initSpeed;
        }else {
            initSpeed = builder.initSpeed;
        }
        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();
    }
    /**
     * 改变bitmap的大小
     * @param bitmap 目标bitmap
     * @param newW 目标宽度-
     * @param newH 目标高度
     * @return
     */
    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 matrix = new Matrix();
        matrix.postScale(scaleWidth, scaleHeight);
        // 得到新的图片
        bitmap = Bitmap.createBitmap(bitmap, 0, 0, oldW, oldH, matrix, true);
        return bitmap;
    }
    /**
     * drawable图片资源转bitmap
     * @param drawable
     * @return
     */
    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;
    }

    public static final class Builder {
        private float initSpeed;
        private Bitmap bitmap;
        private boolean isSpeedRandom;
        private boolean isSizeRandom;
        private int initWindLevel;//下落物体角度
        private boolean isAngleChange;//下落物体角度是否改变
        public Builder(Bitmap bitmap) {
            this.initSpeed = defaultSpeed;
            this.bitmap = bitmap;
        }
        public Builder(Drawable drawable) {
            this.initSpeed = defaultSpeed;
            this.bitmap = drawableToBitmap(drawable);
        }

        /**
         * 设置物体的初始下落速度
         * @param level
         * @param isAngleChange 物体初始下降速度比例是否随机
         * @return
         */
        public Builder setWind(int level,boolean isAngleChange) {
            this.initWindLevel  = level;
            this.isAngleChange = isAngleChange;
            return this;
        }
        /**
         * 设置物体的初始下落速度
         * @param speed
         * @return
         */
        public Builder setSpeed(float speed) {
            this.initSpeed = speed;
            return this;
        }
        /**
         * 设置物体的初始下落速度
         * @param speed
         * @param isRandomSpeed 物体初始下降速度比例是否随机
         * @return
         */
        public Builder setSpeed(float speed,boolean isRandomSpeed) {
            this.initSpeed = speed;
            this.isSpeedRandom = isRandomSpeed;
            return this;
        }
        /**
         * 设置下落物体的大小
         * @param w
         * @param h
         * @return
         */
        public Builder setSize(int w, int h){
            this.bitmap = changeBitmapSize(this.bitmap,w,h);
            return this;
        }
        /**
         * 设置物体大小
         * @param w
         * @param h
         * @param isRandomSize 物体初始大小比例是否随机
         * @return
         */
        public Builder setSize(int w, int h, boolean isRandomSize){
            this.bitmap = changeBitmapSize(this.bitmap,w,h);
            this.isSizeRandom = isRandomSize;
            return this;
        }

        /**
         * 构建FallObject
         * @return
         */
        public FallObject build() {
            return new FallObject(this);
        }

    }
}

在用户在xml中使用,Activity中实例化:

代码语言:javascript
复制
  FallObject.Builder builder = new FallObject.Builder(getResources().getDrawable(R.drawable.snowflake));
        FallObject fallObject = builder
                .setSpeed(8,true)
                .setSize(80,80,true)
                .setWind(5,true)
                .build();
        //初始化一个雪球样式的fallObject
        ((FallingView)findViewById(R.id.fallingView)).addFallObject(fallObject,60);//添加50个雪球对象
代码语言:javascript
复制
 <com.wusy.wusylibrary.view.FallingView.FallingView
            android:id="@+id/fallingView"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2018.12.26 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 效果图:
  • 参考文章:
  • 提炼与分享:
    • 1. 如何实现简单的物体下落:
      • 2. 为什么要使用Builder建造者模式
        • 3. 绘制图片并且控制其大小
          • 4. 如何引入风的概念
          • 源码:
          相关产品与服务
          容器服务
          腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档