[Android] SurfaceView使用实例

转载声明:本文由Sodino所有,转载请注明出处:http://blog.csdn.net/sodino/article/details/7704084

 

同样,先上效果图如下:

效果图中,抛物线的动画即是由SurfaceView实现的。底部栏中的文字翻转详情相关帖子:
[Android] 文字翻转动画的实现

需求:
1.实现抛物线动画
   1.1 设计物理模型,能够根据时间变量计算出某个时刻图片的X/Y坐标。
   1.2 将图片高频率(相比于UI线程的缓慢而言)刷新到界面中。这儿需要实现将脏界面清屏及刷新操作。
2.文字翻转动画(已解决,见上面的帖子链接)

下面来逐一解决所提出的问题。

-----------------------------------------------------------------------------
分隔线内容与Android无关,请慎读,勿拍砖。谢啦

 

 

1.1 设计物理模型,如果大家还记得初中物理时,这并不难。自己写的草稿图见下:

可以有:图片要从高度为H的位置下落,并且第一次与X轴碰撞时会出现能量损失,至原来的N%。并且我们需要图片的最终落点离起始位置在X轴上的位移为L,默认存在重力加速度g。
详细的物理分析见上图啦,下面只说代码中如何实现,相关代码在PhysicalTool.java。
第一次下落过程所耗时t1与高度height会有如下关系:

 

1 t1 = Math.sqrt(2 * height * 1.0d / GRAVITY);

 

第一次与X轴碰撞后上升至最高点的耗时t2与高度 N%*height会有:

 

1 t2 = Math.sqrt((1 - WASTAGE) * 2 * height * 1.0d / GRAVITY);

 

那么总的动画时间为(t1 + t2 + t2),则水平位移速度有(width为X轴总位移):

 

1 velocity = width * 1.0d / (t1 + 2 * t2);

 

则根据时间计算图片的实时坐标有:
PhysicalTool.comput()

 

 1                 double used = (System.currentTimeMillis() - startTime) * 1.0d / 1000;
 2                 x = velocity * used;
 3                 if (0 <= used && used < t1) {
 4                         y = height - 0.5d * GRAVITY * used * used;
 5                 } else if (t1 <= used && used < (t1 + t2)) {
 6                         double tmp = t1 + t2 - used;
 7                         y = (1 - WASTAGE) * height - 0.5d * GRAVITY * tmp * tmp;
 8                 } else if ((t1 + t2) <= used && used < (t1 + 2 * t2)) {
 9                         double tmp = used - t1 - t2;
10                         y = (1 - WASTAGE) * height - 0.5d * GRAVITY * tmp * tmp;
11                 }

Android无关内容结束了。
----------------------------------------------------------------------------------------

1.2 SurfaceView刷新界面
        SurfaceView是一个特殊的UI组件,特殊在于它能够使用非UI线程刷新界面。至于为何具有此特殊性,将在另一个帖子"SurfaceView 相关知识笔记"中讨论,该帖子将讲述SurfaceView、Surface、ViewRoot、Window Manager/Window、Canvas等之间的关系。
       
        使用SurfaceView需要自定义组件继承该类,并实现SurfaceHolder.Callback,该回调提供了三个方法:

1 surfaceCreated()//通知Surface已被创建,可以在此处启动动画线程
2 surfaceChanged()//通知Surface已改变
3 surfaceDestroyed()//通知Surface已被销毁,可以在此处终止动画线程

SurfaceView使用有一个原则,即该界面操作必须在surfaceCreated之后及surfaceDestroyed之前。该回调的监听通过SurfaceHolder设置。代码如下:

 

1 //于SurfaceView类中,该类实现SurfaceHolder.Callback接口,如本例中的ParabolaView
2 SurfaceHolder holder = getHolder();
3 holder.addCallback(this);

 

示例代码中,通过启动DrawThread调用handleThread()实现对SurfaceView的刷新。
        刷新界面首先需要执行holder.lockCanvas()锁定Canvas并获得Canvas实例,然后进行界面更新操作,最后结束锁定Canvas,提交界面更改,至Surface最终显示在屏幕上。
        代码如下:

 

 

1                                 canvas = holder.lockCanvas();
2                                 … … … … 
3                                 … … … … 
4                                 canvas.drawBitmap(bitmap, x, y, paint);
5                                 holder.unlockCanvasAndPost(canvas);

 

 

 

本例中,需要清除屏幕脏区域,出于简便的做法,是将整个SurfaceView背景重复地设置为透明,代码为:

 

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

 

对于SurfaceView的操作,下面这个链接讲述得更详细,更易理解,推荐去看下:
Android开发之SurfaceView

 

惯例,Java代码如下,XML请自行实现

 

 1 ActSurfaceView.java
 2 
 3 package lab.sodino.surfaceview;
 4 
 5 import lab.sodino.surfaceview.RotateAnimation.InterpolatedTimeListener;
 6 import android.app.Activity;
 7 import android.graphics.BitmapFactory;
 8 import android.os.Bundle;
 9 import android.os.Handler;
10 import android.os.Handler.Callback;
11 import android.os.Message;
12 import android.view.View;
13 import android.view.View.OnClickListener;
14 import android.view.ViewGroup;
15 import android.widget.Button;
16 import android.widget.TextView;
17 
18 public class ActSurfaceView extends Activity implements OnClickListener, ParabolaView.ParabolaListener, Callback,
19                 InterpolatedTimeListener {
20         public static final int REFRESH_TEXTVIEW = 1;
21         private Button btnStartAnimation;
22         /** 动画界面。 */
23         private ParabolaView parabolaView;
24         /** 购物车处显示购物数量的TextView。 */
25         private TextView txtNumber;
26         /** 购物车中的数量。 */
27         private int number;
28         private Handler handler;
29         /** TextNumber是否允许显示最新的数字。 */
30         private boolean enableRefresh;
31 
32         public void onCreate(Bundle savedInstanceState) {
33                 super.onCreate(savedInstanceState);
34                 setContentView(R.layout.main);
35 
36                 handler = new Handler(this);
37 
38                 number = 0;
39 
40                 btnStartAnimation = (Button) findViewById(R.id.btnStartAnim);
41                 btnStartAnimation.setOnClickListener(this);
42 
43                 parabolaView = (ParabolaView) findViewById(R.id.surfaceView);
44                 parabolaView.setParabolaListener(this);
45 
46                 txtNumber = (TextView) findViewById(R.id.txtNumber);
47         }
48 
49         public void onClick(View v) {
50                 if (v == btnStartAnimation) {
51                         LogOut.out(this, "isShowMovie:" + parabolaView.isShowMovie());
52                         if (parabolaView.isShowMovie() == false) {
53                                 number++;
54                                 enableRefresh = true;
55                                 parabolaView.setIcon(BitmapFactory.decodeResource(getResources(), R.drawable.icon));
56                                 // 设置起始Y轴高度和终止X轴位移
57                                 parabolaView.setParams(200, ((ViewGroup) txtNumber.getParent()).getLeft());
58                                 parabolaView.showMovie();
59                         }
60                 }
61         }
62 
63         public void onParabolaStart(ParabolaView view) {
64 
65         }
66 
67         public void onParabolaEnd(ParabolaView view) {
68                 handler.sendEmptyMessage(REFRESH_TEXTVIEW);
69         }
70 
71         public boolean handleMessage(Message msg) {
72                 switch (msg.what) {
73                 case REFRESH_TEXTVIEW:
74 
75                         if (txtNumber.getVisibility() != View.VISIBLE) {
76                                 txtNumber.setVisibility(View.VISIBLE);
77                         }
78                         RotateAnimation anim = new RotateAnimation(txtNumber.getWidth() >> 1, txtNumber.getHeight() >> 1,
79                                         RotateAnimation.ROTATE_INCREASE);
80                         anim.setInterpolatedTimeListener(this);
81                         txtNumber.startAnimation(anim);
82                         break;
83                 }
84                 return false;
85         }
86 
87         @Override
88         public void interpolatedTime(float interpolatedTime) {
89                 // 监听到翻转进度过半时,更新txtNumber显示内容。
90                 if (enableRefresh && interpolatedTime > 0.5f) {
91                         txtNumber.setText(Integer.toString(number));
92                         // Log.d("ANDROID_LAB", "setNumber:" + number);
93                         enableRefresh = false;
94                 }
95         }
96 }

 

 1 DrawThread.java
 2 
 3 package lab.sodino.surfaceview;
 4 
 5 import android.view.SurfaceView;
 6 
 7 /**
 8  * @author Sodino E-mail:sodinoopen@hotmail.com
 9  * @version Time:2012-6-18 上午03:14:31
10  */
11 public class DrawThread extends Thread {
12         private SurfaceView surfaceView;
13         private boolean running;
14 
15         public DrawThread(SurfaceView surfaceView) {
16                 this.surfaceView = surfaceView;
17         }
18 
19         public void run() {
20                 if (surfaceView == null) {
21                         return;
22                 }
23                 if (surfaceView instanceof ParabolaView) {
24                         ((ParabolaView) surfaceView).handleThread();
25                 }
26         }
27 
28         public void setRunning(boolean b) {
29                 running = b;
30         }
31 
32         public boolean isRunning() {
33                 return running;
34         }
35 }
  1 ParabolaView.java
  2 package lab.sodino.surfaceview;
  3 
  4 import android.content.Context;
  5 import android.graphics.Bitmap;
  6 import android.graphics.Canvas;
  7 import android.graphics.Color;
  8 import android.graphics.Paint;
  9 import android.graphics.PixelFormat;
 10 import android.util.AttributeSet;
 11 import android.view.SurfaceHolder;
 12 import android.view.SurfaceView;
 13 
 14 /**
 15  * @author Sodino E-mail:sodinoopen@hotmail.com
 16  * @version Time:2012-6-18 上午02:52:33
 17  */
 18 public class ParabolaView extends SurfaceView implements SurfaceHolder.Callback {
 19         /** 每30ms刷一帧。 */
 20         private static final long SLEEP_DURATION = 10l;
 21         private SurfaceHolder holder;
 22         /** 动画图标。 */
 23         private Bitmap bitmap;
 24         private DrawThread thread;
 25         private PhysicalTool physicalTool;
 26         private ParabolaView.ParabolaListener listener;
 27         /** 默认未创建,相当于Destory。 */
 28         private boolean surfaceDestoryed = true;
 29 
 30         public ParabolaView(Context context, AttributeSet attrs, int defStyle) {
 31                 super(context, attrs, defStyle);
 32                 init();
 33         }
 34 
 35         public ParabolaView(Context context, AttributeSet attrs) {
 36                 super(context, attrs);
 37                 init();
 38         }
 39 
 40         public ParabolaView(Context context) {
 41                 super(context);
 42                 init();
 43         }
 44 
 45         private void init() {
 46                 holder = getHolder();
 47                 holder.addCallback(this);
 48                 holder.setFormat(PixelFormat.TRANSPARENT);
 49 
 50                 setZOrderOnTop(true);
 51                 // setZOrderOnTop(false);
 52 
 53                 physicalTool = new PhysicalTool();
 54         }
 55 
 56         @Override
 57         public void surfaceCreated(SurfaceHolder holder) {
 58                 surfaceDestoryed = false;
 59         }
 60 
 61         @Override
 62         public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
 63 
 64         }
 65 
 66         @Override
 67         public void surfaceDestroyed(SurfaceHolder holder) {
 68                 LogOut.out(this, "surfaceDestroyed");
 69                 surfaceDestoryed = true;
 70                 physicalTool.cancel();
 71         }
 72 
 73         public void handleThread() {
 74                 Canvas canvas = null;
 75 
 76                 Paint pTmp = new Paint();
 77                 pTmp.setAntiAlias(true);
 78                 pTmp.setColor(Color.RED);
 79 
 80                 Paint paint = new Paint();
 81                 // 设置抗锯齿
 82                 paint.setAntiAlias(true);
 83                 paint.setColor(Color.CYAN);
 84                 physicalTool.start();
 85                 LogOut.out(this, "doing:" + physicalTool.doing());
 86                 if (listener != null) {
 87                         listener.onParabolaStart(this);
 88                 }
 89                 while (physicalTool.doing()) {
 90                         try {
 91                                 physicalTool.compute();
 92                                 canvas = holder.lockCanvas();
 93                                 // 设置画布的背景为透明。
 94                                 canvas.drawColor(Color.TRANSPARENT, android.graphics.PorterDuff.Mode.CLEAR);
 95                                 // 绘上新图区域
 96                                 float x = (float) physicalTool.getX();
 97                                 // float y = (float) physicalTool.getY();
 98                                 float y = (float) physicalTool.getMirrorY(getHeight(), bitmap.getHeight());
 99                                 // LogOut.out(this, "x:" + x + " y:" + y);
100                                 canvas.drawRect(x, y, x + bitmap.getWidth(), y + bitmap.getHeight(), pTmp);
101                                 canvas.drawBitmap(bitmap, x, y, paint);
102                                 holder.unlockCanvasAndPost(canvas);
103                                 Thread.sleep(SLEEP_DURATION);
104                         } catch (Exception e) {
105                                 e.printStackTrace();
106                         }
107                 }
108                 // 清除屏幕内容
109                 // 直接按"Home"回桌面,SurfaceView被销毁了,lockCanvas返回为null。
110                 if (surfaceDestoryed == false) {
111                         canvas = holder.lockCanvas();
112                         canvas.drawColor(Color.TRANSPARENT, android.graphics.PorterDuff.Mode.CLEAR);
113                         holder.unlockCanvasAndPost(canvas);
114                 }
115 
116                 thread.setRunning(false);
117                 if (listener != null) {
118                         listener.onParabolaEnd(this);
119                 }
120         }
121 
122         public void showMovie() {
123                 if (thread == null) {
124                         thread = new DrawThread(this);
125                 } else if (thread.getState() == Thread.State.TERMINATED) {
126                         thread.setRunning(false);
127                         thread = new DrawThread(this);
128                 }
129                 LogOut.out(this, "thread.getState:" + thread.getState());
130                 if (thread.getState() == Thread.State.NEW) {
131                         thread.start();
132                 }
133         }
134 
135         /** 正在播放动画时,返回true;否则返回false。 */
136         public boolean isShowMovie() {
137                 return physicalTool.doing();
138         }
139 
140         public void setIcon(Bitmap bit) {
141                 bitmap = bit;
142         }
143 
144         public void setParams(int height, int width) {
145                 physicalTool.setParams(height, width);
146         }
147 
148         /** 设置抛物线的动画监听器。 */
149         public void setParabolaListener(ParabolaView.ParabolaListener listener) {
150                 this.listener = listener;
151         }
152 
153         static interface ParabolaListener {
154                 public void onParabolaStart(ParabolaView view);
155 
156                 public void onParabolaEnd(ParabolaView view);
157         }
158 }
 1 PhysicalTool.java
 2 package lab.sodino.surfaceview;
 3 
 4 /**
 5  * @author Sodino E-mail:sodinoopen@hotmail.com
 6  * @version Time:2012-6-18 上午06:07:16
 7  */
 8 public class PhysicalTool {
 9         /** 重力加速度值。 */
10         private static final float GRAVITY = 400.78033f;
11         /** 与X轴碰撞后,重力势能损失掉的百分比。 */
12         private static final float WASTAGE = 0.3f;
13         /** 起始下降高度。 */
14         private int height;
15         /** 起始点到终点的X轴位移。 */
16         private int width;
17         /** 水平位移速度。 */
18         private double velocity;
19         /** X Y坐标。 */
20         private double x, y;
21         /** 动画开始时间。 */
22         private long startTime;
23         /** 首阶段下载的时间。 单位:毫秒。 */
24         private double t1;
25         /** 第二阶段上升与下载的时间。 单位:毫秒。 */
26         private double t2;
27         /** 动画正在进行时值为true,反之为false。 */
28         private boolean doing;
29 
30         public void start() {
31                 startTime = System.currentTimeMillis();
32                 doing = true;
33         }
34 
35         /** 设置起始下落的高度及水平初速度;并以此计算小球下落的第一阶段及第二阶段上升耗时。 */
36         public void setParams(int h, int w) {
37                 height = h;
38                 width = w;
39 
40                 t1 = Math.sqrt(2 * height * 1.0d / GRAVITY);
41                 t2 = Math.sqrt((1 - WASTAGE) * 2 * height * 1.0d / GRAVITY);
42                 velocity = width * 1.0d / (t1 + 2 * t2);
43                 LogOut.out(this, "t1=" + t1 + " t2=" + t2);
44         }
45 
46         /** 根据当前时间计算小球的X/Y坐标。 */
47         public void compute() {
48                 double used = (System.currentTimeMillis() - startTime) * 1.0d / 1000;
49                 x = velocity * used;
50                 if (0 <= used && used < t1) {
51                         y = height - 0.5d * GRAVITY * used * used;
52                 } else if (t1 <= used && used < (t1 + t2)) {
53                         double tmp = t1 + t2 - used;
54                         y = (1 - WASTAGE) * height - 0.5d * GRAVITY * tmp * tmp;
55                 } else if ((t1 + t2) <= used && used < (t1 + 2 * t2)) {
56                         double tmp = used - t1 - t2;
57                         y = (1 - WASTAGE) * height - 0.5d * GRAVITY * tmp * tmp;
58                 } else {
59                         LogOut.out(this, "used:" + used + " set doing false");
60                         x = velocity * (t1 + 2 * t2);
61                         y = 0;
62                         doing = false;
63                 }
64         }
65 
66         public double getX() {
67                 return x;
68         }
69 
70         public double getY() {
71                 return y;
72         }
73 
74         /** 反转Y轴正方向。适应手机的真实坐标系。 */
75         public double getMirrorY(int parentHeight, int bitHeight) {
76                 int half = parentHeight >> 1;
77                 double tmp = half + (half - y);
78                 tmp -= bitHeight;
79                 return tmp;
80         }
81 
82         public boolean doing() {
83                 return doing;
84         }
85 
86         public void cancel() {
87                 doing = false;
88         }
89 }
 1 RotateAnimation.java
 2 package lab.sodino.surfaceview;
 3 
 4 import android.graphics.Camera;
 5 import android.graphics.Matrix;
 6 import android.view.animation.Animation;
 7 import android.view.animation.Transformation;
 8 
 9 /**
10  * @author Sodino E-mail:sodinoopen@hotmail.com
11  * @version Time:2012-6-27 上午07:32:00
12  */
13 public class RotateAnimation extends Animation {
14         /** 值为true时可明确查看动画的旋转方向。 */
15         public static final boolean DEBUG = false;
16         /** 沿Y轴正方向看,数值减1时动画逆时针旋转。 */
17         public static final boolean ROTATE_DECREASE = true;
18         /** 沿Y轴正方向看,数值减1时动画顺时针旋转。 */
19         public static final boolean ROTATE_INCREASE = false;
20         /** Z轴上最大深度。 */
21         public static final float DEPTH_Z = 310.0f;
22         /** 动画显示时长。 */
23         public static final long DURATION = 800l;
24         /** 图片翻转类型。 */
25         private final boolean type;
26         private final float centerX;
27         private final float centerY;
28         private Camera camera;
29         /** 用于监听动画进度。当值过半时需更新txtNumber的内容。 */
30         private InterpolatedTimeListener listener;
31 
32         public RotateAnimation(float cX, float cY, boolean type) {
33                 centerX = cX;
34                 centerY = cY;
35                 this.type = type;
36                 setDuration(DURATION);
37         }
38 
39         public void initialize(int width, int height, int parentWidth, int parentHeight) {
40                 // 在构造函数之后、getTransformation()之前调用本方法。
41                 super.initialize(width, height, parentWidth, parentHeight);
42                 camera = new Camera();
43         }
44 
45         public void setInterpolatedTimeListener(InterpolatedTimeListener listener) {
46                 this.listener = listener;
47         }
48 
49         protected void applyTransformation(float interpolatedTime, Transformation transformation) {
50                 // interpolatedTime:动画进度值,范围为[0.0f,10.f]
51                 if (listener != null) {
52                         listener.interpolatedTime(interpolatedTime);
53                 }
54                 float from = 0.0f, to = 0.0f;
55                 if (type == ROTATE_DECREASE) {
56                         from = 0.0f;
57                         to = 180.0f;
58                 } else if (type == ROTATE_INCREASE) {
59                         from = 360.0f;
60                         to = 180.0f;
61                 }
62                 float degree = from + (to - from) * interpolatedTime;
63                 boolean overHalf = (interpolatedTime > 0.5f);
64                 if (overHalf) {
65                         // 翻转过半的情况下,为保证数字仍为可读的文字而非镜面效果的文字,需翻转180度。
66                         degree = degree - 180;
67                 }
68                 // float depth = 0.0f;
69                 float depth = (0.5f - Math.abs(interpolatedTime - 0.5f)) * DEPTH_Z;
70                 final Matrix matrix = transformation.getMatrix();
71                 camera.save();
72                 camera.translate(0.0f, 0.0f, depth);
73                 camera.rotateY(degree);
74                 camera.getMatrix(matrix);
75                 camera.restore();
76                 if (DEBUG) {
77                         if (overHalf) {
78                                 matrix.preTranslate(-centerX * 2, -centerY);
79                                 matrix.postTranslate(centerX * 2, centerY);
80                         }
81                 } else {
82                         matrix.preTranslate(-centerX, -centerY);
83                         matrix.postTranslate(centerX, centerY);
84                 }
85         }
86 
87         /** 动画进度监听器。 */
88         public static interface InterpolatedTimeListener {
89                 public void interpolatedTime(float interpolatedTime);
90         }
91 }

 

 

 

 

 

 

 

 

 

 

 

 

 

 

posted @ 2013-01-19 19:17  王世桢  阅读(419)  评论(0编辑  收藏  举报