天气渐热,来片雪花降降温——Android自定义SurfaceView实现雪花效果

 

 

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

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

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

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

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

先上图:

(1)原图:

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

------------

b.背景(snow_bg0.png)

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

 

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

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

 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 version="1.0" encoding="utf-8"?>
 2 
 3 <resources>
 4     <attr name="flakeCount" format="integer"/>
 5     <attr name="minSize" format="integer"/>
 6     <attr name="maxSize" format="integer"/>
 7     <attr name="flakeSrc" format="reference|integer"/>
 8     <attr name="speedX" format="integer"/>
 9     <attr name="speedY" format="integer"/>
10 
11     <declare-styleable name="Snow">
12         <attr name="flakeCount"/>
13         <attr name="minSize"/>
14         <attr name="maxSize"/>
15         <attr name="flakeSrc"/>
16         <attr name="speedX"/>
17         <attr name="speedY"/>
18     </declare-styleable>
19 </resources>

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

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

 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 }

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

 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     <com.haoye.snow.Snow
2         android:layout_width="match_parent"
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);

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

 源码下载:https://github.com/laishenghao/Snow/

 转载请注明:http://www.cnblogs.com/laishenghao/p/5396185.html

 

 

 

原文地址:https://www.cnblogs.com/laishenghao/p/5396185.html