自定义下雪动画(上)

本章目录

  • Part One:构造方法
  • Part Two:自定义属性
  • Part Three:布局测量
  • Part Four:绘制
  • Part Five:重绘

在了解了自定义View的基本绘制流程后,还需要大量的练习去巩固这方面的知识,所以这一节我们再练习个下雪案例。

Part One:构造方法

构造方法的写法还是老样子,没有啥改变的:

public class SnowView extends View{
    public SnowView(Context context) {
        this(context, null);
    }

    public SnowView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SnowView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public SnowView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        initAttrs(context, attrs);
    }

    private void initAttrs(Context context, AttributeSet attrs) {
    }
}

需要注意的是,这里需要把app的gradle中的minSDK改为21(android5.0)。如果想适配更低版本的手机,也就是说想要在android5.0以下的手机上运行,需要把4个参数的构造方法删除,在3个参数的构造方法里使用super和初始化属性。

Part Two:自定义属性

下面开始正式画了,如果不太清楚如何下手的话,可以先把问题简单化,跑通了,再给它复杂化。比如说本例,一群雪花不会,那就先处理一朵雪花的情况。
假设我们需要绘制一朵大小随机,出现的位置随机的雪花,要处理的属性有什么:

  1. int minSize:雪花大小随机值下限
  2. int maxSize:雪花大小随机值上限
  3. Bitmap snowSrc:雪花的图案
  4. int moveX:雪花每次移动的横向距离,也就是横向移动速度
  5. int moveY:雪花每次移动的纵向距离,也就是纵向移动速度

前三个属性都好理解,后两个移动属性可能有的人会有点疑惑,先来看看屏幕坐标。


屏幕坐标.png

屏幕的左上角是起点(0,0),

  • X轴坐标从起点位置向右是正数,向左是负数
  • Y轴坐标从起点位置向下是正数,向上是负数

如果我们雪花从屏幕顶端出现,想要实现一个移动的效果, 就是在一个固定时间内(比如20 - 50毫秒),改变图片的X轴和Y轴的值,重绘。如此反复循环就造成雪花的移动效果了。
好了,意义明白了,接下来就是把这些属性初始化了。
attrs.xml中:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="SnowView">
        <attr name="minSize" format="integer"/>
        <attr name="maxSize" format="integer"/>
        <attr name="snowSrc" format="reference|integer"/>
        <attr name="moveX" format="integer"/>
        <attr name="moveY" format="integer"/>
    </declare-styleable>
</resources>

SnowView中:

public class SnowView extends View{
    private int minSize;    //雪花大小随机值下限
    private int maxSize;    //雪花大小随机值上限
    private Bitmap snowSrc; //雪花的图案
    private int moveX;      //雪花每次移动的横向距离,也就是横向移动速度
    private int moveY;      //雪花每次移动的纵向距离,也就是纵向移动速度

    public SnowView(Context context) {
        this(context, null);
    }

    public SnowView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SnowView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public SnowView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        initAttrs(context, attrs);
    }

    private void initAttrs(Context context, AttributeSet attrs) {
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.SnowView, 0, 0);
        minSize = typedArray.getInt(R.styleable.SnowView_minSize, 48);//获取最小值,默认48
        maxSize = typedArray.getInt(R.styleable.SnowView_maxSize, 72);//获取最大值,默认72
        int srcId = typedArray.getResourceId(R.styleable.SnowView_snowSrc, R.drawable.snow_flake);//获取默认图片资源ID
        snowSrc = BitmapFactory.decodeResource(getResources(), srcId);//根据资源ID生成Bitmap对象
        moveX = typedArray.getInt(R.styleable.SnowView_moveX, 10);//获取X轴移动速度
        moveY = typedArray.getInt(R.styleable.SnowView_moveY, 10);//获取Y轴移动速度
        if (minSize > maxSize){
            maxSize = minSize;
        }        
        typedArray.recycle();//TypedArray共享资源,资源回收
    }
}

activity_main.xml中:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.terana.mycustomview.MainActivity">

    <com.terana.mycustomview.cutstomview.SnowView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:minSize="16"
        app:maxSize="48"
        app:snowSrc="@drawable/snow_ball"
        app:moveX="10"
        app:moveY="10"/>

</RelativeLayout>

Part Three:布局测量

测量我们之前说过,除了MeasureSpec.AT_MOST这种,也就是包裹内容需要根据情况设定个默认值,其它的写法完全可以一样,可以照搬。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width = getDefaultMeasureSizes(getSuggestedMinimumWidth(), widthMeasureSpec, true);
        int height = getDefaultMeasureSizes(getSuggestedMinimumHeight(), heightMeasureSpec, false);
        setMeasuredDimension(width, height);
    }

    private int getDefaultMeasureSizes(int suggestedMinimumSize, int defaultMeasureSpec, boolean flag) {
        int result = suggestedMinimumSize;
        int specMode = MeasureSpec.getMode(defaultMeasureSpec);
        int specSize = MeasureSpec.getSize(defaultMeasureSpec);
        switch (specMode){
            case MeasureSpec.UNSPECIFIED:
                result = suggestedMinimumSize;
                break;
            case MeasureSpec.AT_MOST:
                if (flag){
                    result = snowSrc.getWidth() + getPaddingLeft() +getPaddingRight();
                }else {
                    result = snowSrc.getHeight() + getPaddingTop() +getPaddingBottom();
                }
                break;
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
        }
        return result;
    }

其实从实际情况来说,本例只需要让SnowView全屏才比较适合,即便不写测量布局,默认就是全屏显示,只不过写上更规范一些。

Part Four:绘制

好了,准备工作都做好了,可以正式开始画图了。
绘制工作很简单,就是在onDraw方法里调用drawBitmap方法即可,它有四个参数:

  1. Bitmap bitmap:需要绘制的位图,就是我们自定义的snowSrc。
  2. Rect src:就是位图的原始区域,比如说想动态改变原图的大小会调用,本例用null就可以了,即不对原图做任何改变。
  3. RectF dst:位图要放置在屏幕的区域,比如正中央或者屏幕顶端之类的。
  4. Paint paint:画笔,不多说了,本例没有啥特性绘制的东西,直接new一个默认即可。

暂时先把雪花画一个固定位置,比如屏幕的中心顶部:

public class SnowView extends View{
    private int minSize;    //雪花大小随机值下限
    private int maxSize;    //雪花大小随机值上限
    private Bitmap snowSrc; //雪花的图案
    private int moveX;      //雪花每次移动的横向距离,也就是横向移动速度
    private int moveY;      //雪花每次移动的纵向距离,也就是纵向移动速度
    private Paint snowPaint;

    public SnowView(Context context) {
        this(context, null);
    }

    public SnowView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SnowView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public SnowView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        initAttrs(context, attrs);
        initVariables();
    }

    private void initVariables() {
        snowPaint = new Paint();
    }

    private void initAttrs(Context context, AttributeSet attrs) {
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.SnowView, 0, 0);
        minSize = typedArray.getInt(R.styleable.SnowView_minSize, 48);//获取最小值,默认48
        maxSize = typedArray.getInt(R.styleable.SnowView_maxSize, 72);//获取最大值,默认72
        int srcId = typedArray.getResourceId(R.styleable.SnowView_snowSrc, R.drawable.snow_flake);//获取默认图片资源ID
        snowSrc = BitmapFactory.decodeResource(getResources(), srcId);//根据资源ID生成Bitmap对象
        moveX = typedArray.getInt(R.styleable.SnowView_moveX, 10);//获取X轴移动速度
        moveY = typedArray.getInt(R.styleable.SnowView_moveY, 10);//获取Y轴移动速度
        if (minSize > maxSize){
            maxSize = minSize;
        }
        typedArray.recycle();//TypedArray共享资源,资源回收
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width = getDefaultMeasureSizes(getSuggestedMinimumWidth(), widthMeasureSpec, true);
        int height = getDefaultMeasureSizes(getSuggestedMinimumHeight(), heightMeasureSpec, false);
        setMeasuredDimension(width, height);
    }

    private int getDefaultMeasureSizes(int suggestedMinimumSize, int defaultMeasureSpec, boolean flag) {
        int result = suggestedMinimumSize;
        int specMode = MeasureSpec.getMode(defaultMeasureSpec);
        int specSize = MeasureSpec.getSize(defaultMeasureSpec);
        switch (specMode){
            case MeasureSpec.UNSPECIFIED:
                result = suggestedMinimumSize;
                break;
            case MeasureSpec.AT_MOST:
                if (flag){
                    result = snowSrc.getWidth() + getPaddingLeft() +getPaddingRight();
                }else {
                    result = snowSrc.getHeight() + getPaddingTop() +getPaddingBottom();
                }
                break;
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
        }
        return result;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        RectF rectF = new RectF();
        //暂时画在屏幕的中心顶部
        rectF.left = getWidth() / 2;
        rectF.top = 0;
        rectF.right = rectF.left +snowSrc.getWidth();
        rectF.bottom = rectF.top +snowSrc.getHeight();
        canvas.drawBitmap(snowSrc, null, rectF, snowPaint);
    }
}

运行下,看下结果:


单雪花不动.png

Part Five:重绘

一个静止的单雪花已经绘制出来了,下面就该让它动起来,并且位置随机了。
先前的自定义View篇,我们是在外部通过handler传递消息并重绘。这次换个方式,在SnowView的内部使用handler。但是,需要注意的是,此处最好不使用new Handler来创建对象了,因为View的内部自带一个Handler。
另外,在Part Four中把初始位置写死了,要想改变此位置,需要定义变量出来,并在onSizeChanged里面完成初始化,代码如下

   @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        drawSnow(canvas);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        currentX = w / 2;
    }
    
    private int currentX;
    private int currentY = 0;
    private void drawSnow(Canvas canvas) {
        //暂时画在屏幕的中心顶部
        rectF.left = currentX;
        rectF.top = currentY;
        rectF.right = rectF.left +snowSrc.getWidth();
        rectF.bottom = rectF.top +snowSrc.getHeight();
        canvas.drawBitmap(snowSrc, null, rectF, snowPaint);
        getHandler().postDelayed(new Runnable() {
            @Override
            public void run() {
                moveSknowFlake();
                invalidate();
            }
        }, 20);
    }

    private void moveSknowFlake() {
        currentX = currentX + moveX;
        currentY = currentY + moveY;
        //判断如果雪花移出屏幕左侧,右侧或者下侧,则回到起始位置重新开始
        if (currentX > getWidth() || currentX < 0 || currentY > getHeight()){
            currentX = getWidth() / 2;
            currentY = 0;
        }
    }

现在的结果是一朵雪花从固定位置开始,以固定的速度到固定位置结束。
最后一步优化就是把这些都随机化。
完整的SnowView代码为:

package com.terana.mycustomview.cutstomview;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;

import com.terana.mycustomview.R;

import java.util.Random;

public class SnowView extends View{
    private int minSize;    //雪花大小随机值下限
    private int maxSize;    //雪花大小随机值上限
    private Bitmap snowSrc; //雪花的图案
    private int moveX;      //雪花每次移动的横向距离,也就是横向移动速度
    private int moveY;      //雪花每次移动的纵向距离,也就是纵向移动速度
    private Paint snowPaint;
    private RectF rectF;

    public SnowView(Context context) {
        this(context, null);
    }

    public SnowView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SnowView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public SnowView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        initAttrs(context, attrs);
        initVariables();
    }

    private void initVariables() {
        snowPaint = new Paint();
        rectF = new RectF();
    }

    private void initAttrs(Context context, AttributeSet attrs) {
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.SnowView, 0, 0);
        minSize = typedArray.getInt(R.styleable.SnowView_minSize, 48);//获取最小值,默认48
        maxSize = typedArray.getInt(R.styleable.SnowView_maxSize, 72);//获取最大值,默认72
        int srcId = typedArray.getResourceId(R.styleable.SnowView_snowSrc, R.drawable.snow_flake);//获取默认图片资源ID
        snowSrc = BitmapFactory.decodeResource(getResources(), srcId);//根据资源ID生成Bitmap对象
        moveX = typedArray.getInt(R.styleable.SnowView_moveX, 10);//获取X轴移动速度
        moveY = typedArray.getInt(R.styleable.SnowView_moveY, 10);//获取Y轴移动速度
        if (minSize > maxSize){
            maxSize = minSize;
        }
        typedArray.recycle();//TypedArray共享资源,资源回收
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width = getDefaultMeasureSizes(getSuggestedMinimumWidth(), widthMeasureSpec, true);
        int height = getDefaultMeasureSizes(getSuggestedMinimumHeight(), heightMeasureSpec, false);
        setMeasuredDimension(width, height);
    }

    private int getDefaultMeasureSizes(int suggestedMinimumSize, int defaultMeasureSpec, boolean flag) {
        int result = suggestedMinimumSize;
        int specMode = MeasureSpec.getMode(defaultMeasureSpec);
        int specSize = MeasureSpec.getSize(defaultMeasureSpec);
        switch (specMode){
            case MeasureSpec.UNSPECIFIED:
                result = suggestedMinimumSize;
                break;
            case MeasureSpec.AT_MOST:
                if (flag){
                    result = snowSrc.getWidth() + getPaddingLeft() +getPaddingRight();
                }else {
                    result = snowSrc.getHeight() + getPaddingTop() +getPaddingBottom();
                }
                break;
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
        }
        return result;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        drawSnow(canvas);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        currentX = new Random().nextInt(w);//初始位置为屏幕宽度中的一个随机值
        currentY = -(new Random().nextInt(h));//初始位置为屏幕的上方的随机值,不可见
    }

    private int currentX;
    private int currentY;
    private void drawSnow(Canvas canvas) {
        //暂时画在屏幕的中心顶部
        rectF.left = currentX;
        rectF.top = currentY;
        rectF.right = rectF.left +snowSrc.getWidth();
        rectF.bottom = rectF.top +snowSrc.getHeight();
        canvas.drawBitmap(snowSrc, null, rectF, snowPaint);
        getHandler().postDelayed(new Runnable() {
            @Override
            public void run() {
                moveSknowFlake();
                invalidate();
            }
        }, 20);
    }

    private boolean moveDirection = true;
    private void moveSknowFlake() {
        if (moveDirection){
            currentX = currentX + (new Random().nextInt(4) + moveX);//速度为一个初始随机值 + 设定横移速度
        }else {
            currentX = currentX - (new Random().nextInt(4) + moveX);//速度为一个初始随机值 + 设定横移速度
        }
        currentY = currentY + (new Random().nextInt(4) + moveY);//速度为一个初始随机值 + 设定竖移速度
        //判断如果雪花移出屏幕左侧,右侧或者下侧,则回到起始位置重新开始
        if (currentX > getWidth() || currentX < 0 || currentY > getHeight()){
            currentX = new Random().nextInt(getWidth());
            currentY = 0;
            moveDirection = !moveDirection;//暂时互相取反,后面再随机移动方向
        }
    }
}

效果为:


单雪花移动.gif

由于没有引入创建雪花对象,很多地方的代码比较生涩,效果也很一般。下一节会在现有基础上完成完整的下雪效果,其实关键的代码都已经实现。剩下的无非就是再创建个对象,用数组去绘制而已。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,902评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,037评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,978评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,867评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,763评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,104评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,565评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,236评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,379评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,313评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,363评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,034评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,637评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,719评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,952评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,371评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,948评论 2 341

推荐阅读更多精彩内容