android 下雪动画,自定义下雪动画(上)

本章目录

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:自定义属性

下面开始正式画了,如果不太清楚如何下手的话,可以先把问题简单化,跑通了,再给它复杂化。比如说本例,一群雪花不会,那就先处理一朵雪花的情况。

假设我们需要绘制一朵大小随机,出现的位置随机的雪花,要处理的属性有什么:

int minSize:雪花大小随机值下限

int maxSize:雪花大小随机值上限

Bitmap snowSrc:雪花的图案

int moveX:雪花每次移动的横向距离,也就是横向移动速度

int moveY:雪花每次移动的纵向距离,也就是纵向移动速度

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

d028ed6d7ed9

屏幕坐标.png

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

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

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

如果我们雪花从屏幕顶端出现,想要实现一个移动的效果, 就是在一个固定时间内(比如20 - 50毫秒),改变图片的X轴和Y轴的值,重绘。如此反复循环就造成雪花的移动效果了。

好了,意义明白了,接下来就是把这些属性初始化了。

attrs.xml中:

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中:

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">

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"/>

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方法即可,它有四个参数:

Bitmap bitmap:需要绘制的位图,就是我们自定义的snowSrc。

Rect src:就是位图的原始区域,比如说想动态改变原图的大小会调用,本例用null就可以了,即不对原图做任何改变。

RectF dst:位图要放置在屏幕的区域,比如正中央或者屏幕顶端之类的。

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

}

}

运行下,看下结果:

d028ed6d7ed9

单雪花不动.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;//暂时互相取反,后面再随机移动方向

}

}

}

效果为:

d028ed6d7ed9

单雪花移动.gif

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值