实现雪花的效果其实也可以通过自定义View的方式来实现的(SurfaceView也是继承自View的),而且操作上也相对简单一些,当然也有一些不足啦...

相对于View,SurfaceView有如下特点:

(1)SurfaceView可以直接获取Canvas对象,在非UI线程里也可以进行绘制;

(2)SurfaceView支持双缓冲技术,具有更高的绘图效率;

(3)Surface系列产品也火了一阵子了,用Surface准没错.....(好吧,我承认微软给了我很大一笔广告费....想象ing...)

先上图:

(1)原图:

a.雪花(snow_flake.png),由于是白色的所以看不见(虚线之间)

------

iOS 雪花粒子动画 android雪花飘落_iOS 雪花粒子动画

------

b.背景(snow_bg0.png)

iOS 雪花粒子动画 android雪花飘落_android 自定义特效_02

(2) 效果截图(雪花的大小、数量、下落速度等都是可通过xml属性调节的):

iOS 雪花粒子动画 android雪花飘落_ide_03

下面开始实现自定义SurfaceView...

1. 首先确定一片雪花所需要的参数:长、宽、在屏幕上的坐标、下落的水平/垂直速度....恩先这些吧,把它们封装到一个类里面:

iOS 雪花粒子动画 android雪花飘落_android 自定义特效_04

1 public class SnowFlake {
2 private int mWidth;
3 private int mHeight;
4 private int mX;
5 private int mY;
6 private int mSpeedX;
7 private int mSpeedY;
8
9 public int getHeight() {
10 return mHeight;
11 }
12
13 public void setHeight(int height) {
14 this.mHeight = height;
15 }
16
17 public int getSpeedX() {
18 return mSpeedX;
19 }
20
21 public void setSpeedX(int speedX) {
22 this.mSpeedX = mSpeedX;
23 }
24
25 public int getSpeedY() {
26 return mSpeedY;
27 }
28
29 public void setSpeedY(int speedY) {
30 this.mSpeedY = speedY;
31 }
32
33 public int getWidth() {
34 return mWidth;
35 }
36
37 public void setWidth(int width) {
38 this.mWidth = width;
39 }
40
41 public int getX() {
42 return mX;
43 }
44
45 public void setX(int x) {
46 this.mX = x;
47 }
48
49 public int getY() {
50 return mY;
51 }
52
53 public void setY(int y) {
54 this.mY = y;
55 }
56 }
2. 在res/values下新建 attrs.xml 文件,自定义几个属性值:雪花的数量、最大/ 小尺寸、下落速度、资源图片等,更改如下:
1 <?xml versinotallow="1.0" encoding="utf-8"?>
2
3 
4 
5 
6 
7 
8 
9 
10
11 
12 
13 
14 
15 
16 
17 
18 
19

iOS 雪花粒子动画 android雪花飘落_android 自定义特效_04

3. 下面轮到SurfaceView出场...啊不...是SurfaceView的son出场了........

(1)定义名称为Snow的类,扩展SurfaceView,并实现接口 SurfaceHolder.Callback,代码如下:

iOS 雪花粒子动画 android雪花飘落_android 自定义特效_04

1 public class Snow extends SurfaceView implements SurfaceHolder.Callback {
2 public Snow(Context context) {
3 this(context, null);
4 }
5
6 public Snow(Context context, AttributeSet attrs) {
7 this(context, attrs, 0);
8 }
9
10 public Snow(Context context, AttributeSet attrs, int defStyleAttr) {
11 super(context, attrs, defStyleAttr);
12 }
13
14 @Override
15 public void surfaceCreated(SurfaceHolder holder) {
16
17 }
18
19 @Override
20 public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
21
22 }
23
24 @Override
25 public void surfaceDestroyed(SurfaceHolder holder) {
26
27 }
28 }

iOS 雪花粒子动画 android雪花飘落_android 自定义特效_04

(2)添加以下变量,初始化默认值:

iOS 雪花粒子动画 android雪花飘落_android 自定义特效_04

1 private SurfaceHolder mHolder;
2 private SnowFlake[] mFlakes;
3 private int mViewWidth = 200;
4 private int mViewHeight = 100;
5 private int mFlakeCount = 20;
6 private int mMinSize = 50;
7 private int mMaxSize = 70;
8 private int mSpeedX = 10;
9 private int mSpeedY = 20;
10 private Bitmap mSnowBitmap = null;
11 private boolean mStart = false;
(3)在构造函数中获取控件属性值,并初始化 SurfaceHolder (注意我们只需在最后一个构造函数实现即可,前面的两个通过this来调用此构造函数):
1 public Snow(Context context, AttributeSet attrs, int defStyleAttr) {
2 super(context, attrs, defStyleAttr);
3 initHolder();
4 setZOrderOnTop(true);
5
6 TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.Snow, defStyleAttr, 0);
7 int cnt = array.getIndexCount();
8 for (int i = 0; i < cnt; i++) {
9 int attr = array.getIndex(i);
10 switch (attr) {
11 case R.styleable.Snow_flakeCount:
12 mFlakeCount = array.getInteger(attr, 0);
13 break;
14 case R.styleable.Snow_minSize:
15 mMinSize = array.getInteger(attr, 50);
16 break;
17 case R.styleable.Snow_maxSize:
18 mMaxSize = array.getInteger(attr, 70);
19 break;
20 case R.styleable.Snow_flakeSrc:
21 Integer srcId = array.getResourceId(attr, R.drawable.snow_flake);
22 mSnowBitmap = BitmapFactory.decodeResource(getResources(), srcId);
23 break;
24 case R.styleable.Snow_speedX:
25 mSpeedX = array.getInteger(attr, 10);
26 break;
27 case R.styleable.Snow_speedY:
28 mSpeedY = array.getInteger(attr, 10);
29 break;
30 default:
31 break;
32 }
33 }
34 if (mMinSize > mMaxSize) {
35 mMaxSize = mMinSize;
36 }
37 array.recycle();
38 }
初始化 SurfaceHolder 部分:
1 private void initHolder() {
2 mHolder = this.getHolder();
3 mHolder.setFormat(PixelFormat.TRANSLUCENT);
4 mHolder.addCallback(this);
5 }
(4)在Snow类中添加如下变量,并重写 onMeasure() 函数,测量SurfaceView的大小:
1 @Override
2 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
3 //--- measure the view's width
4 int widthMode = MeasureSpec.getMode(widthMeasureSpec);
5 if (widthMode == MeasureSpec.EXACTLY) {
6 mViewWidth = MeasureSpec.getSize(widthMeasureSpec);
7 } else {
8 mViewWidth = (getPaddingStart() + mSnowBitmap.getWidth() + getPaddingEnd());
9 }
10
11 //--- measure the view's height
12 int heightMode = MeasureSpec.getMode(heightMeasureSpec);
13 if (heightMode == MeasureSpec.EXACTLY) {
14 mViewHeight = MeasureSpec.getSize(heightMeasureSpec);
15 } else {
16 mViewHeight = (getPaddingTop() + mSnowBitmap.getHeight() + getPaddingBottom());
17 }
18
19 setMeasuredDimension(mViewWidth, mViewHeight);
20 }
(5)初始化snow flakes的函数:通过随机数生成一定范围内的坐标值和snow flake 的大小值,一开始时雪花是在屏幕上方的:
1 private void initSnowFlakes() {
2 mFlakes = new SnowFlake[mFlakeCount];
3 boolean isRightDir = new Random().nextBoolean();
4 for (int i = 0; i < mFlakes.length; i++) {
5 mFlakes[i] = new SnowFlake();
6 mFlakes[i].setWidth(new Random().nextInt(mMaxSize-mMinSize) + mMinSize);
7 mFlakes[i].setHeight(mFlakes[i].getWidth());
8 mFlakes[i].setX(new Random().nextInt(mViewWidth));
9 mFlakes[i].setY(-(new Random().nextInt(mViewHeight)));
10 mFlakes[i].setSpeedY(new Random().nextInt(4) + mSpeedY);
11 if (isRightDir) {
12 mFlakes[i].setSpeedX(new Random().nextInt(4) + mSpeedX);
13 }
14 else {
15 mFlakes[i].setSpeedX(-(new Random().nextInt(4) + mSpeedX));
16 }
17 }
18 }
(6)绘制snow flakes 的函数:通过SurfaceHolder 的lockCanvas()函数获取到画布,绘制完后再调用 unlockCanvasAndPost() 函数释放canvas并将缓冲区绘制的内容一次性绘制到canvas上:
1 private void drawView() {
2 if (mHolder == null) {
3 return;
4 }
5 Canvas canvas = mHolder.lockCanvas();
6 if (canvas == null) {
7 return;
8 }
9 canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
10 drawSnow(canvas);
11 mHolder.unlockCanvasAndPost(canvas);
12 }
13
14 private void drawSnow(Canvas canvas) {
15 Rect rect = new Rect();
16 Paint paint = new Paint();
17 for (SnowFlake flake : mFlakes) {
18 rect.left = flake.getX();
19 rect.top = flake.getY();
20 rect.right = rect.left + flake.getWidth();
21 rect.bottom = rect.top + flake.getHeight();
22 canvas.drawBitmap(mSnowBitmap, null, rect, paint);
23 }
24 }
(7)更新snow flakes的参数的函数:
1 private void updatePara() {
2 int x;
3 int y;
4 for (SnowFlake flake : mFlakes) {
5 if (flake == null) {
6 break;
7 }
8 x = flake.getX() + flake.getSpeedX();
9 y = flake.getY() + flake.getSpeedY();
10 if ((x > mViewWidth + 20 || x < 0)
11 || (y > mViewHeight + 20)) {
12 x = new Random().nextInt(mViewWidth);
13 y = 0;
14 }
15 flake.setX(x);
16 flake.setY(y);
17 }
18 }
(8)开启绘画线程的 start 函数:
1 public void start() {
2 new Thread(){
3 @Override
4 public void run() {
5 while (true) {
6 try {
7 if (mStart) {
8 updatePara();
9 drawView();
10 }
11 Thread.sleep(20);
12 }
13 catch (Exception ex) {
14 ex.printStackTrace();
15 }
16 }
17 }
18 }.start();
19 }
(9)修改 surfaceCreated(SurfaceHolder holder) 函数,即在SurfaceView创建完成后初始化snow flakes,并开启动画线程:
1 @Override
2 public void surfaceCreated(SurfaceHolder holder) {
3 initSnowFlakes();
4 start();
5 }
(10)重写 onVisibilityChanged() 函数,在控件不可见时停止更新和绘制控件,避免CPU资源浪费:
1 @Override
2 protected void onVisibilityChanged(View changedView, int visibility) {
3 super.onVisibilityChanged(changedView, visibility);
4 mStart = (visibility == VISIBLE);
5 }
4. 控件的使用:
由于我们做了很多封装工作,所以控件使用是很简单的, 在布局文件中添加并设置对应属性即可:
1 
3 android:layout_height="match_parent"
4 myview:flakeCount="30"
5 myview:minSize="30"
6 myview:maxSize="70"
7 myview:speedX="5"
8 myview:speedY="10"
9 myview:flakeSrc="@drawable/snow_flake"/>
--------------------------

至此,自定义SurfaceView控件就完成了,当然我们还可以添加一些其他的效果,比如让随机生成的雪花小片的多一些,适当调节雪花的亮度等,这样可以更好地模拟远处下雪的情景,使景色具有深度。

另外在上面的代码实现中,其实通过

mHolder.setFormat(PixelFormat.TRANSLUCENT);
setZOrderOnTop(true);

这两行代码,我们已经将SurfaceView设置为背景透明的模式,在每次绘制的时候,通过

canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);

这行代码清理屏幕后再重新绘制;这使得我们可以在控件外添加背景图片,而不需要每次都在控件中重绘。