Android自定義View——從零開始實現雪花飄落效果

版權聲明:本文爲博主原創文章,未經博主容許不得轉載html

系列教程:Android開發之從零開始系列java

源碼:AnliaLee/FallingView,歡迎stargit

你們要是看到有錯誤的地方或者有啥好的建議,歡迎留言評論github

前言:轉眼已經是十一月下旬了,天氣慢慢轉冷,不知道北方是否是已經開始下雪了呢?本期教程咱們就順應季節主題,一塊兒來實現 雪花飄落的效果吧。本篇效果思路參考自國外大神的Android實現雪花飛舞效果,並在此基礎上實現進一步的封裝和功能擴展canvas

本篇只着重於思路和實現步驟,裏面用到的一些知識原理不會很是細地拿來說,若是有不清楚的api或方法能夠在網上搜下相應的資料,確定有大神講得很是清楚的,我這就不獻醜了。本着認真負責的精神我會把相關知識的博文連接也貼出來(其實就是懶不想寫那麼多哈哈),你們能夠自行傳送。爲了照顧第一次閱讀系列博客的小夥伴,本篇會出現一些在以前系列博客就講過的內容,看過的童鞋自行跳過該段便可設計模式

國際慣例,先上效果圖api


繪製一個循環下落的「雪球」

咱們先從最簡單的部分作起,自定義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();
        }
    };
}
複製代碼

效果如圖dom

在上述代碼中View基本的框架咱們已經搭好了,思路其實很簡單,咱們須要作僅僅是在每次重繪以前更新作下落運動的物體的位置便可ide

封裝下落物體對象

相關博文連接

Android開發中無處不在的設計模式——Builder模式

[Android] 獲取View的寬度和高度

要實現大雪紛飛的效果,很明顯只有一個雪球是不夠的,並且雪也不能只有雪球一個形狀,咱們但願能夠自定義雪的樣式,甚至不侷限於下雪,還能夠下雨、下金幣等等,所以咱們要對下落的物體進行封裝。爲了之後物體類對外方法代碼的可讀性,這裏咱們採用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;//當前位置X座標
    public float presentY;//當前位置Y座標
    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);//隨機物體的X座標
        initY = random.nextInt(parentHeight)- parentHeight;//隨機物體的Y座標,並讓物體一開始從屏幕頂部下落
        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;
        }

        /** * 設置物體的初始下落速度 * @param speed * @return */
        public Builder setSpeed(int speed) {
            this.initSpeed = speed;
            return this;
        }

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

    /** * 繪製物體對象 * @param canvas */
    public void drawObject(Canvas canvas){
        moveObject();
        canvas.drawBitmap(bitmap,presentX,presentY,null);
    }

    /** * 移動物體對象 */
    private void moveObject(){
        moveY();
        if(presentY>parentHeight){
            reset();
        }
    }

    /** * Y軸上的移動邏輯 */
    private void moveY(){
        presentY += presentSpeed;
    }

    /** * 重置object位置 */
    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();
        }
    };

    /** * 向View添加下落物體對象 * @param fallObject 下落物體對象 * @param num */
    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添加一些物體看看效果

//繪製雪球bitmap
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
FallObject.Builder builder = new FallObject.Builder(bitmap);
FallObject fallObject = builder
		.setSpeed(10)
		.build();

fallingView = (FallingView) findViewById(R.id.fallingView);
fallingView.addFallObject(fallObject,50);//添加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);
	}
}

/** * 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資源導入的構造方法,確定須要配套改變FallObject圖片樣式大小的接口,依然是在FallObjectBuilder中擴展相應的接口

public static final class Builder {
	//省略部分代碼...
	public Builder setSize(int w, int h){
		this.bitmap = changeBitmapSize(this.bitmap,w,h);
		return this;
	}
}

/** * 改變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;
}
複製代碼

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;
        }

        /** * 設置物體的初始下落速度 * @param speed * @return */
        public Builder setSpeed(int speed) {
            this.initSpeed = speed;
            return this;
        }

        /** * 設置物體的初始下落速度 * @param speed * @param isRandomSpeed 物體初始降低速度比例是否隨機 * @return */
        public Builder setSpeed(int 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;
        }
    }

    /** * 重置object位置 */
    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;//π/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;
        }

        /** * 設置風力等級、方向以及隨機因素 * @param level 風力等級(絕對值爲 5 時效果會比較好),爲正時風從左向右吹(物體向X軸正方向偏移),爲負時則相反 * @param isWindRandom 物體初始風向和風力大小比例是否隨機 * @param isWindChange 在物體下落過程當中風的風向和風力是否會產生隨機變化 * @return */
        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();
        }
    }

    /** * X軸上的移動邏輯 */
    private void moveX(){
        presentX += defaultWindSpeed * Math.sin(angle);
        if(isWindChange){
            angle += (float) (random.nextBoolean()?-1:1) * Math.random() * 0.0025;
        }
    }

    /** * 重置object位置 */
    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;
        }

        //限制angle的最大最小值
        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();
複製代碼

效果如圖

至此本篇教程到此結束,若是你們看了感受還不錯麻煩點個贊,大家的支持是我最大的動力~

相關文章
相關標籤/搜索