[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 }