版权声明:本文为博主原创文章,未经博主容许不得转载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
相关博文连接
要实现大雪纷飞的效果,很明显只有一个雪球是不够的,并且雪也不能只有雪球一个形状,咱们但愿能够自定义雪的样式,甚至不局限于下雪,还能够下雨、下金币等等,所以咱们要对下落的物体进行封装。为了之后物体类对外方法代码的可读性,这里咱们采用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个雪球对象
复制代码
效果如图
到这里咱们完成了一个最基础的下落物体类,下面开始扩展功能和效果
咱们以前的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图片样式大小的接口,依然是在FallObject的Builder中扩展相应的接口
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();
复制代码
效果如图
至此本篇教程到此结束,若是你们看了感受还不错麻烦点个赞,大家的支持是我最大的动力~