A股上市公司传智教育(股票代码 003032)旗下技术交流社区北京昌平校区

 找回密码
 加入黑马

QQ登录

只需一步,快速开始

本帖最后由 Android_Robot 于 2016-5-10 14:38 编辑

英文原文:https://blog.stylingandroid.com/snowfall/

这篇文章的发布日期是2015年的圣诞节,貌似唯一能与之匹配的就是就是在Styling Android上来点喜庆的东西~

虽然我觉得这张照片足以结束这篇文章了,不过我就再慷慨的来点下雪的效果吧。

我们可以在包含了这张图片的布局里添加一个自定义View来实现:


  1. <font color="#555555"><?xml version="1.0" encoding="utf-8"?>  
  2. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  3.   xmlns:tools="http://schemas.android.com/tools"  
  4.   android:layout_width="match_parent"  
  5.   android:layout_height="match_parent"  
  6.   tools:context="com.stylingandroid.snowfall.MainActivity">  
  7.   
  8.   <ImageView  
  9.     android:id="@+id/image"  
  10.     android:layout_width="match_parent"  
  11.     android:layout_height="match_parent"  
  12.     android:layout_centerInParent="true"  
  13.     android:contentDescription="@null"  
  14.     android:scaleType="fitCenter"  
  15.     android:src="@drawable/tree" />  
  16.   
  17.   <com.stylingandroid.snowfall.SnowView  
  18.     android:layout_width="match_parent"  
  19.     android:layout_height="match_parent"  
  20.     android:layout_alignBottom="@id/image"  
  21.     android:layout_alignEnd="@id/image"  
  22.     android:layout_alignLeft="@id/image"  
  23.     android:layout_alignRight="@id/image"  
  24.     android:layout_alignStart="@id/image"  
  25.     android:layout_alignTop="@id/image" />  
  26. </RelativeLayout>  
  27. </font>
复制代码

本来想在一个自定义的ImageView里面做这件事情,但是还是选择了把它们分开,这样我就不需要每次刷新动画的时候都重新渲染一遍图像。那么就让我们看看我们的自定义View:


  1. <font color="#555555">public class SnowView extends View {  
  2.     private static final int NUM_SNOWFLAKES = 150;  
  3.     private static final int DELAY = 5;  
  4.   
  5.     private SnowFlake[] snowflakes;  
  6.   
  7.     public SnowView(Context context) {  
  8.         super(context);  
  9.     }  
  10.   
  11.     public SnowView(Context context, AttributeSet attrs) {  
  12.         super(context, attrs);  
  13.     }  
  14.   
  15.     public SnowView(Context context, AttributeSet attrs, int defStyleAttr) {  
  16.         super(context, attrs, defStyleAttr);  
  17.     }  
  18.   
  19.     protected void resize(int width, int height) {  
  20.         Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);  
  21.         paint.setColor(Color.WHITE);  
  22.         paint.setStyle(Paint.Style.FILL);  
  23.         snowflakes = new SnowFlake[NUM_SNOWFLAKES];  
  24.         for (int i = 0; i < NUM_SNOWFLAKES; i++) {  
  25.             snowflakes[i] = SnowFlake.create(width, height, paint);  
  26.         }  
  27.     }  
  28.   
  29.     @Override  
  30.     protected void onSizeChanged(int w, int h, int oldw, int oldh) {  
  31.         super.onSizeChanged(w, h, oldw, oldh);  
  32.         if (w != oldw || h != oldh) {  
  33.             resize(w, h);  
  34.         }  
  35.     }  
  36.   
  37.     @Override  
  38.     protected void onDraw(Canvas canvas) {  
  39.         super.onDraw(canvas);  
  40.         for (SnowFlake snowFlake : snowflakes) {  
  41.             snowFlake.draw(canvas);  
  42.         }  
  43.         getHandler().postDelayed(runnable, DELAY);  
  44.     }  
  45.   
  46.     private Runnable runnable = new Runnable() {  
  47.         @Override  
  48.         public void run() {  
  49.             invalidate();  
  50.         }  
  51.     };  
  52. }  
  53. </font>
复制代码

这非常简单。当View被resized的时候,我们初始化150个随即放置的SnowFlake对象。onDraw() 方法绘制所有的SnowFlake对象,然后周期性的执行invalidate()。为了不完全占用UI线程,我们在调用这个的时候稍微延迟了一点点时间。

SnowFlake的代码大致是基于 Samuel Arbesman的snowfall算法:


  1. <font color="#555555">class SnowFlake {  
  2.     private static final float ANGE_RANGE = 0.1f;  
  3.     private static final float HALF_ANGLE_RANGE = ANGE_RANGE / 2f;  
  4.     private static final float HALF_PI = (float) Math.PI / 2f;  
  5.     private static final float ANGLE_SEED = 25f;  
  6.     private static final float ANGLE_DIVISOR = 10000f;  
  7.     private static final float INCREMENT_LOWER = 2f;  
  8.     private static final float INCREMENT_UPPER = 4f;  
  9.     private static final float FLAKE_SIZE_LOWER = 7f;  
  10.     private static final float FLAKE_SIZE_UPPER = 20f;  
  11.   
  12.     private final Random random;  
  13.     private final Point position;  
  14.     private float angle;  
  15.     private final float increment;  
  16.     private final float flakeSize;  
  17.     private final Paint paint;  
  18.   
  19.     public static SnowFlake create(int width, int height, Paint paint) {  
  20.         Random random = new Random();  
  21.         int x = random.getRandom(width);  
  22.         int y = random.getRandom(height);  
  23.         Point position = new Point(x, y);  
  24.         float angle = random.getRandom(ANGLE_SEED) / ANGLE_SEED * ANGE_RANGE + HALF_PI - HALF_ANGLE_RANGE;  
  25.         float increment = random.getRandom(INCREMENT_LOWER, INCREMENT_UPPER);  
  26.         float flakeSize = random.getRandom(FLAKE_SIZE_LOWER, FLAKE_SIZE_UPPER);  
  27.         return new SnowFlake(random, position, angle, increment, flakeSize, paint);  
  28.     }  
  29.   
  30.     SnowFlake(Random random, Point position, float angle, float increment, float flakeSize, Paint paint) {  
  31.         this.random = random;  
  32.         this.position = position;  
  33.         this.angle = angle;  
  34.         this.increment = increment;  
  35.         this.flakeSize = flakeSize;  
  36.         this.paint = paint;  
  37.     }  
  38.   
  39.     private void move(int width, int height) {  
  40.         double x = position.x + (increment * Math.cos(angle));  
  41.         double y = position.y + (increment * Math.sin(angle));  
  42.   
  43.         angle += random.getRandom(-ANGLE_SEED, ANGLE_SEED) / ANGLE_DIVISOR;  
  44.   
  45.         position.set((int) x, (int) y);  
  46.   
  47.         if (!isInside(width, height)) {  
  48.             reset(width);  
  49.         }  
  50.     }  
  51.   
  52.     private boolean isInside(int width, int height) {  
  53.         int x = position.x;  
  54.         int y = position.y;  
  55.         return x >= -flakeSize - 1 && x + flakeSize <= width && y >= -flakeSize - 1 && y - flakeSize < height;  
  56.     }  
  57.   
  58.     private void reset(int width) {  
  59.         position.x = random.getRandom(width);  
  60.         position.y = (int) (-flakeSize - 1);  
  61.         angle = random.getRandom(ANGLE_SEED) / ANGLE_SEED * ANGE_RANGE + HALF_PI - HALF_ANGLE_RANGE;  
  62.     }  
  63.   
  64.     public void draw(Canvas canvas) {  
  65.         int width = canvas.getWidth();  
  66.         int height = canvas.getHeight();  
  67.         move(width, height);  
  68.         canvas.drawCircle(position.x, position.y, flakeSize, paint);  
  69.     }  
  70. }  
  71. </font>
复制代码

当每朵雪花初始化之后它被放在Canvas上的一个随机位置。这是为了确保在首次绘制的时候,雪花看起来像是正在进行中,而如果一开始所有的雪花都是从顶部落下的话,就会觉得雪是刚开始下的。当一片雪花离开了画布的时候,它会被重新放置在顶部横轴的一个随机位置 - 这样我们就能回收雪花,避免不必要的对象创建。

绘制每一帧的时候,我们首先为雪花的移动添加一些随机因素来模拟风吹的效果,让每片雪花稍微改变下自己的方向。然后在实际绘制雪花之前,我们执行边界检查(如果必要,把它移到上方)。


所有的常量都经过调整,知道雪花模拟达到我满意的效果为止。

运行效果如下:

youtube视频地址:https://www.youtube.com/watch?v=pk66ZziTfOw

当然,在Canvas绘制并不是渲染这类东西最高效的方法(比如使用OpenGL渲染),但是我还有礼物要打开,还有火鸡要吃所以这个只能改天再说了。

【github源码下载地址】回帖可见:

游客,如果您要查看本帖隐藏内容请回复


其他资源:
[官方秀] 为什么你要报传智·黑马学Android
【github项目分享】Android实现雪花飞舞幻特效

8 个回复

倒序浏览
精品33333
回复 使用道具 举报
不错.........
回复 使用道具 举报
豆腐干豆腐干撒旦法
回复 使用道具 举报
学习学习
回复 使用道具 举报
很不错嘛
回复 使用道具 举报
学习一下
回复 使用道具 举报
baby14 金牌黑马 2019-7-12 07:38:53
8#
多谢分享
回复 使用道具 举报
谢谢分享
回复 使用道具 举报
您需要登录后才可以回帖 登录 | 加入黑马