Android自定义动态的View,实现飘雪的效果

时间:2022-05-16 12:09:16

已经把代码上传到GitHub,SnowFlyView


前言:

目前手上的项目有一个要实现类似于飘雪的效果,在网上搜了很多,但是发现均没有想要的效果——在飘落一定时间之后就会自动停止!

参考国外snowfall 点击打开链接

以及参考了国内某大神的GiftRainView 点击打开链接

本文的代码已经上传到GitHub 点击打开链接

效果:

Android自定义动态的View,实现飘雪的效果  Android自定义动态的View,实现飘雪的效果


分析:

  1. 对于自定义View来说,相信或多或少已经了解很多,在这里就不再过多叙述,主要是实现onDraw()方法!
  2. 飘雪的效果,无非就是让"雪花"被不断的被重绘!因此,我们需要一个线程来处理不断重绘的操作。

如果说我们不太清楚应该设置多少间隔时间才不会显得有卡顿的情况,这个时候我们就可以考虑使用系统的ValueAnimator,ValueAnimator中有一个addUpdateListener()方法,通过这个方法,我们就基本可以实现比较流畅的动画效果!


实现:

以下是我写的实现ValueAnimator.AnimatorUpdateListener 的方法,在onAnimationUpdate中处理雪花图片下一次的坐标:

private class animatorUpdateListenerImp implements ValueAnimator.AnimatorUpdateListener {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
for (int i = 0; i < snowList.size(); i++) {
Snow snow = snowList.get(i);
snow.x += snow.xSpeed;
snow.y += snow.ySpeed;
if (snow.x < -snow.bpWidth || snow.x > getWidth()) {
/**
* the snow falling to the sides
*/
if (isDelyStop)
snowList.remove(i);
else {
snow.x = randomX(snow.bpWidth);
snow.y = randomY(snow.bpHeight);
}
} else if (snow.y > getHeight()) {
/**
* the snow falling to the bottom
*/
if (isDelyStop)
snowList.remove(i);
else {
snow.x = randomX(snow.bpWidth);
snow.y = 0 - snow.bpHeight;
}
}
}
/**
* to prevent the animator running empty
*/
if (snowList.size() <= 0 && animator.isRunning())
animator.cancel();
invalidate();
}
}

/**
* the x coordinate
*/
private float randomX(int bpWidth) {
return initToLeft + xRandom.nextFloat() * (xWidth - bpWidth);
}


/**
* the y coordinate
*/
private float randomY(int bpHeight) {
return 0 - (initToTop + yRandom.nextFloat() * (yHeight - bpHeight));
}


雪花类:

class Snow {
private float x;
private float y;
private float xSpeed;
private float ySpeed;
private int bpHeight;
private int bpWidth;
private Bitmap snowBitmap;
private float BASESPEED = 100.0f;


Snow(float xSpeed, float ySpeed, Bitmap snowBitmap) {
float tempScale = minScale + (float) (Math.random() * (maxScale - minScale));
this.bpHeight = (int) (snowBitmap.getHeight() * tempScale);
this.bpWidth = (int) (snowBitmap.getWidth() * tempScale);
this.x = randomX(bpWidth);
this.y = randomY(bpHeight);
/**
* xDirection > 0 right falling
* xDirection < 0 left falling
* xDirection = 0 vertical falling
*/
float xDirection = 1.0f - (float) (Math.random() * 2.0f);
this.xSpeed = xSpeed * xDirection / BASESPEED;
this.ySpeed = (ySpeed + ySpeed * (float) Math.random()) / BASESPEED;
this.snowBitmap = Bitmap.createScaledBitmap(snowBitmap, bpWidth, bpHeight, true);
}
}


左边图的效果,点击开始之后,雪花一直飘落知道点击执行取消才会停止!实现这种效果,只需要在layout文件中不设置snow_duration或者将其设为小于等于300,也可以在JAVA代码中执行setSnowDuration()方法,将值设置在300以内。

Layout文件如下:

<com.lesences.SnowFlyView
android:id="@+id/snow_fly"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="@dimen/activity_vertical_margin"
app:snow_bitmap="@mipmap/snowflake"
app:snow_count="40"
app:snow_initToLeft="15dp"
app:snow_initToRight="15dp"
app:snow_maxScale="1.20"
app:snow_minScale="0.45"
app:snow_xSpeed="50.0"
app:snow_ySpeed="300.0" />


右边图的效果,在点击开始之后,雪花会在一定的时间之后渐渐地停止飘落!同理,只需要把上面所说的snow_duration设置为大于300的值即可!

Layout文件如下:雪花将会在5秒之后渐渐停止飘落!

<com.lesences.SnowFlyView
android:id="@+id/snow_fly"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="@dimen/activity_vertical_margin"
app:snow_bitmap="@mipmap/snowflake"
app:snow_count="40"
app:snow_duration="5000"
app:snow_initToLeft="15dp"
app:snow_initToRight="15dp"
app:snow_maxScale="1.20"
app:snow_minScale="0.45"
app:snow_xSpeed="50.0"
app:snow_ySpeed="300.0" />

其中代码中出现的 snow_bitmap 指的是雪花图片; snow_count 指的是在屏幕中会出现多少片雪花同时飘落; snow_duration 指的是雪花飘落的时间 snow_initToLeftsnow_initToRightsnow_initToTopsnow_initToBottomsnow_initTo 等指的是限制雪花产生的区域; snow_maxScalesnow_minScale 指的是雪花图片的最大、最小的缩放, snow_xSpeedsnow_ySpeed 等指的是x轴、y轴的飘落速率!

附上完整代码:

import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.drawable.BitmapDrawable;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.view.View;


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


/**
* @author : lesences.
*/
public class SnowFlyView extends View {
private final int msgWhat = 0x01;
public static final long DEFAULTDURATION = 300L;
/**
* the distance of snow start falling to the view top
*/
private int initToTop;
/**
* the distance of snow start falling to the view left
*/
private int initToLeft;
/**
* the distance of snow start falling to the view bottom
*/
private int initToBottom;
/**
* the distance of snow start falling to the view right
*/
private int initToRight;
private float minScale;
private float maxScale;
private float xSpeed;
private float ySpeed;
private int snowCount;
private long snowDuration;
private List<Snow> snowList;
private BitmapDrawable snowBitmap;
private Matrix mtx = new Matrix();
private ValueAnimator animator;
private Random xRandom = new Random();
private Random yRandom = new Random();
private boolean isDelyStop;
private boolean sendMsgable;
/**
* the range of snow start falling in the x direction
*/
private float xWidth;
/**
* the range of snow start falling in the y direction
*/
private float yHeight;




public SnowFlyView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}


public SnowFlyView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initAttr(context, attrs);
init();
}


private void initAttr(Context context, AttributeSet attrs) {
TypedArray attributes = context.obtainStyledAttributes(attrs, R.styleable.SnowFlyView);
int initTo = attributes.getDimensionPixelSize(R.styleable.SnowFlyView_snow_initTo, 0);
initToTop = attributes.getDimensionPixelSize(R.styleable.SnowFlyView_snow_initToTop, 0);
initToLeft = attributes.getDimensionPixelSize(R.styleable.SnowFlyView_snow_initToLeft, 0);
initToBottom = attributes.getDimensionPixelSize(R.styleable.SnowFlyView_snow_initToBottom, 0);
initToRight = attributes.getDimensionPixelSize(R.styleable.SnowFlyView_snow_initToRight, 0);
minScale = attributes.getFloat(R.styleable.SnowFlyView_snow_minScale, 1.0f);
maxScale = attributes.getFloat(R.styleable.SnowFlyView_snow_maxScale, 1.0f);
xSpeed = attributes.getFloat(R.styleable.SnowFlyView_snow_xSpeed, 0.0f);
ySpeed = attributes.getFloat(R.styleable.SnowFlyView_snow_ySpeed, 100.0f);
snowCount = attributes.getInt(R.styleable.SnowFlyView_snow_count, 20);
snowDuration = attributes.getInt(R.styleable.SnowFlyView_snow_duration, 0);
snowBitmap = (BitmapDrawable) attributes.getDrawable(R.styleable.SnowFlyView_snow_bitmap);


if (0 != initTo)
initToTop = initToLeft = initToBottom = initToRight = initTo;
if (minScale <= 0.0f || minScale > maxScale)
throw new IllegalArgumentException("The minScale is illegal");
sendMsgable = snowDuration > DEFAULTDURATION;
attributes.recycle();
}


private void init() {
/**
* close software/hardware
*/
setLayerType(View.LAYER_TYPE_NONE, null);
snowList = new ArrayList<>(snowCount);
animator = ValueAnimator.ofFloat(0.0f, 1.0f);
animator.setRepeatCount(ValueAnimator.INFINITE);
animator.setDuration(DEFAULTDURATION);
animator.addUpdateListener(new animatorUpdateListenerImp());
}


@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
xWidth = getWidth() - initToLeft - initToRight;
yHeight = getHeight() - initToTop - initToBottom;
}


@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (int i = 0; i < snowList.size(); i++) {
Snow snow = snowList.get(i);
mtx.setTranslate(-snow.bpWidth / 2, -snow.bpHeight / 2);
mtx.postTranslate(snow.bpWidth / 2 + snow.x, snow.bpHeight / 2 + snow.y);
canvas.drawBitmap(snow.snowBitmap, mtx, null);
}
}


/**
* init snowList
*/
private void initSnows() {
if (null == snowBitmap) return;
snowList.clear();
for (int i = 0; i < snowCount; i++) {
Snow snow = new Snow(xSpeed, ySpeed, snowBitmap.getBitmap());
snowList.add(snow);
}
}


/**
* stop animation dely
*/
public void stopAnimationDely() {
removeMessages();
this.isDelyStop = true;
}


/**
* stop animation and clear snowList
*/
public void stopAnimationNow() {
removeMessages();
snowList.clear();
invalidate();
animator.cancel();
}


/**
* start animation
*/
public void startAnimation() {
this.isDelyStop = false;
if (animator.isRunning())
animator.cancel();
if (sendMsgable) {
removeMessages();
handler.sendEmptyMessageDelayed(msgWhat, snowDuration);
}
initSnows();
animator.start();
}


/**
* set the duration of animation
*/
public void setSnowDuration(long snowDuration) {
this.snowDuration = snowDuration;
sendMsgable = snowDuration > DEFAULTDURATION;
}


/**
* back the state of animation
*/
public boolean isRunning() {
return animator.isRunning();
}


@Override
protected void onDetachedFromWindow() {
removeMessages();
if (animator.isRunning())
animator.cancel();
super.onDetachedFromWindow();


}


private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (msg.what == msgWhat)
isDelyStop = true;
}
};


private void removeMessages() {
if (handler.hasMessages(msgWhat))
handler.removeMessages(msgWhat);
}




private class animatorUpdateListenerImp implements ValueAnimator.AnimatorUpdateListener {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
for (int i = 0; i < snowList.size(); i++) {
Snow snow = snowList.get(i);
snow.x += snow.xSpeed;
snow.y += snow.ySpeed;
if (snow.x < -snow.bpWidth || snow.x > getWidth()) {
/**
* the snow falling to the sides
*/
if (isDelyStop)
snowList.remove(i);
else {
snow.x = randomX(snow.bpWidth);
snow.y = randomY(snow.bpHeight);
}
} else if (snow.y > getHeight()) {
/**
* the snow falling to the bottom
*/
if (isDelyStop)
snowList.remove(i);
else {
snow.x = randomX(snow.bpWidth);
snow.y = 0 - snow.bpHeight;
}
}
}
/**
* to prevent the animator running empty
*/
if (snowList.size() <= 0 && animator.isRunning())
animator.cancel();
invalidate();
}
}


class Snow {
private float x;
private float y;
private float xSpeed;
private float ySpeed;
private int bpHeight;
private int bpWidth;
private Bitmap snowBitmap;
private float BASESPEED = 100.0f;


Snow(float xSpeed, float ySpeed, Bitmap snowBitmap) {
float tempScale = minScale + (float) (Math.random() * (maxScale - minScale));
this.bpHeight = (int) (snowBitmap.getHeight() * tempScale);
this.bpWidth = (int) (snowBitmap.getWidth() * tempScale);
this.x = randomX(bpWidth);
this.y = randomY(bpHeight);
/**
* xDirection > 0 right falling
* xDirection < 0 left falling
* xDirection = 0 vertical falling
*/
float xDirection = 1.0f - (float) (Math.random() * 2.0f);
this.xSpeed = xSpeed * xDirection / BASESPEED;
this.ySpeed = (ySpeed + ySpeed * (float) Math.random()) / BASESPEED;
this.snowBitmap = Bitmap.createScaledBitmap(snowBitmap, bpWidth, bpHeight, true);
}
}


/**
* the x coordinate
*/
private float randomX(int bpWidth) {
return initToLeft + xRandom.nextFloat() * (xWidth - bpWidth);
}


/**
* the y coordinate
*/
private float randomY(int bpHeight) {
return 0 - (initToTop + yRandom.nextFloat() * (yHeight - bpHeight));
}
}


attr文件:

<declare-styleable name="SnowFlyView">
<attr name="snow_initTo" format="dimension" />
<attr name="snow_initToTop" format="dimension" />
<attr name="snow_initToLeft" format="dimension" />
<attr name="snow_initToBottom" format="dimension" />
<attr name="snow_initToRight" format="dimension" />
<attr name="snow_minScale" format="float" />
<attr name="snow_maxScale" format="float" />
<attr name="snow_count" format="integer" />
<attr name="snow_duration" format="integer" />
<attr name="snow_xSpeed" format="float" />
<attr name="snow_ySpeed" format="float" />
<attr name="snow_bitmap" format="reference" />
</declare-styleable>