[Android] SurfaceView使用實(shí)例
轉(zhuǎn)載聲明:本文由Sodino所有,轉(zhuǎn)載請(qǐng)注明出處:http://blog.csdn.net/sodino/article/details/7704084
同樣,先上效果圖如下:

效果圖中,拋物線的動(dòng)畫即是由SurfaceView實(shí)現(xiàn)的。底部欄中的文字翻轉(zhuǎn)詳情相關(guān)帖子:
[Android] 文字翻轉(zhuǎn)動(dòng)畫的實(shí)現(xiàn)
需求:
1.實(shí)現(xiàn)拋物線動(dòng)畫
1.1 設(shè)計(jì)物理模型,能夠根據(jù)時(shí)間變量計(jì)算出某個(gè)時(shí)刻圖片的X/Y坐標(biāo)。
1.2 將圖片高頻率(相比于UI線程的緩慢而言)刷新到界面中。這兒需要實(shí)現(xiàn)將臟界面清屏及刷新操作。
2.文字翻轉(zhuǎn)動(dòng)畫(已解決,見上面的帖子鏈接)
下面來逐一解決所提出的問題。
-----------------------------------------------------------------------------
分隔線內(nèi)容與Android無關(guān),請(qǐng)慎讀,勿拍磚。謝啦
1.1 設(shè)計(jì)物理模型,如果大家還記得初中物理時(shí),這并不難。自己寫的草稿圖見下:
可以有:圖片要從高度為H的位置下落,并且第一次與X軸碰撞時(shí)會(huì)出現(xiàn)能量損失,至原來的N%。并且我們需要圖片的最終落點(diǎn)離起始位置在X軸上的位移為L,默認(rèn)存在重力加速度g。
詳細(xì)的物理分析見上圖啦,下面只說代碼中如何實(shí)現(xiàn),相關(guān)代碼在PhysicalTool.java。
第一次下落過程所耗時(shí)t1與高度height會(huì)有如下關(guān)系:
1 t1 = Math.sqrt(2 * height * 1.0d / GRAVITY);
第一次與X軸碰撞后上升至最高點(diǎn)的耗時(shí)t2與高度 N%*height會(huì)有:
1 t2 = Math.sqrt((1 - WASTAGE) * 2 * height * 1.0d / GRAVITY);
那么總的動(dòng)畫時(shí)間為(t1 + t2 + t2),則水平位移速度有(width為X軸總位移):
1 velocity = width * 1.0d / (t1 + 2 * t2);
則根據(jù)時(shí)間計(jì)算圖片的實(shí)時(shí)坐標(biāo)有:
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無關(guān)內(nèi)容結(jié)束了。
----------------------------------------------------------------------------------------
1.2 SurfaceView刷新界面
SurfaceView是一個(gè)特殊的UI組件,特殊在于它能夠使用非UI線程刷新界面。至于為何具有此特殊性,將在另一個(gè)帖子"SurfaceView 相關(guān)知識(shí)筆記"中討論,該帖子將講述SurfaceView、Surface、ViewRoot、Window Manager/Window、Canvas等之間的關(guān)系。
使用SurfaceView需要自定義組件繼承該類,并實(shí)現(xiàn)SurfaceHolder.Callback,該回調(diào)提供了三個(gè)方法:
1 surfaceCreated()//通知Surface已被創(chuàng)建,可以在此處啟動(dòng)動(dòng)畫線程 2 surfaceChanged()//通知Surface已改變 3 surfaceDestroyed()//通知Surface已被銷毀,可以在此處終止動(dòng)畫線程
SurfaceView使用有一個(gè)原則,即該界面操作必須在surfaceCreated之后及surfaceDestroyed之前。該回調(diào)的監(jiān)聽通過SurfaceHolder設(shè)置。代碼如下:
1 //于SurfaceView類中,該類實(shí)現(xiàn)SurfaceHolder.Callback接口,如本例中的ParabolaView 2 SurfaceHolder holder = getHolder(); 3 holder.addCallback(this);
示例代碼中,通過啟動(dòng)DrawThread調(diào)用handleThread()實(shí)現(xiàn)對(duì)SurfaceView的刷新。
刷新界面首先需要執(zhí)行holder.lockCanvas()鎖定Canvas并獲得Canvas實(shí)例,然后進(jìn)行界面更新操作,最后結(jié)束鎖定Canvas,提交界面更改,至Surface最終顯示在屏幕上。
代碼如下:
1 canvas = holder.lockCanvas(); 2 … … … … 3 … … … … 4 canvas.drawBitmap(bitmap, x, y, paint); 5 holder.unlockCanvasAndPost(canvas);
本例中,需要清除屏幕臟區(qū)域,出于簡便的做法,是將整個(gè)SurfaceView背景重復(fù)地設(shè)置為透明,代碼為:
1 canvas.drawColor(Color.TRANSPARENT, android.graphics.PorterDuff.Mode.CLEAR);
對(duì)于SurfaceView的操作,下面這個(gè)鏈接講述得更詳細(xì),更易理解,推薦去看下:
Android開發(fā)之SurfaceView
慣例,Java代碼如下,XML請(qǐng)自行實(shí)現(xiàn)
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 /** 動(dòng)畫界面。 */ 23 private ParabolaView parabolaView; 24 /** 購物車處顯示購物數(shù)量的TextView。 */ 25 private TextView txtNumber; 26 /** 購物車中的數(shù)量。 */ 27 private int number; 28 private Handler handler; 29 /** TextNumber是否允許顯示最新的數(shù)字。 */ 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 // 設(shè)置起始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 // 監(jiān)聽到翻轉(zhuǎn)進(jìn)度過半時(shí),更新txtNumber顯示內(nèi)容。 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 /** 動(dòng)畫圖標(biāo)。 */ 23 private Bitmap bitmap; 24 private DrawThread thread; 25 private PhysicalTool physicalTool; 26 private ParabolaView.ParabolaListener listener; 27 /** 默認(rèn)未創(chuàng)建,相當(dāng)于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 // 設(shè)置抗鋸齒 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 // 設(shè)置畫布的背景為透明。 94 canvas.drawColor(Color.TRANSPARENT, android.graphics.PorterDuff.Mode.CLEAR); 95 // 繪上新圖區(qū)域 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 // 清除屏幕內(nèi)容 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 /** 正在播放動(dòng)畫時(shí),返回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 /** 設(shè)置拋物線的動(dòng)畫監(jiān)聽器。 */ 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軸碰撞后,重力勢(shì)能損失掉的百分比。 */ 12 private static final float WASTAGE = 0.3f; 13 /** 起始下降高度。 */ 14 private int height; 15 /** 起始點(diǎn)到終點(diǎn)的X軸位移。 */ 16 private int width; 17 /** 水平位移速度。 */ 18 private double velocity; 19 /** X Y坐標(biāo)。 */ 20 private double x, y; 21 /** 動(dòng)畫開始時(shí)間。 */ 22 private long startTime; 23 /** 首階段下載的時(shí)間。 單位:毫秒。 */ 24 private double t1; 25 /** 第二階段上升與下載的時(shí)間。 單位:毫秒。 */ 26 private double t2; 27 /** 動(dòng)畫正在進(jìn)行時(shí)值為true,反之為false。 */ 28 private boolean doing; 29 30 public void start() { 31 startTime = System.currentTimeMillis(); 32 doing = true; 33 } 34 35 /** 設(shè)置起始下落的高度及水平初速度;并以此計(jì)算小球下落的第一階段及第二階段上升耗時(shí)。 */ 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 /** 根據(jù)當(dāng)前時(shí)間計(jì)算小球的X/Y坐標(biāo)。 */ 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 /** 反轉(zhuǎn)Y軸正方向。適應(yīng)手機(jī)的真實(shí)坐標(biāo)系。 */ 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時(shí)可明確查看動(dòng)畫的旋轉(zhuǎn)方向。 */ 15 public static final boolean DEBUG = false; 16 /** 沿Y軸正方向看,數(shù)值減1時(shí)動(dòng)畫逆時(shí)針旋轉(zhuǎn)。 */ 17 public static final boolean ROTATE_DECREASE = true; 18 /** 沿Y軸正方向看,數(shù)值減1時(shí)動(dòng)畫順時(shí)針旋轉(zhuǎn)。 */ 19 public static final boolean ROTATE_INCREASE = false; 20 /** Z軸上最大深度。 */ 21 public static final float DEPTH_Z = 310.0f; 22 /** 動(dòng)畫顯示時(shí)長。 */ 23 public static final long DURATION = 800l; 24 /** 圖片翻轉(zhuǎn)類型。 */ 25 private final boolean type; 26 private final float centerX; 27 private final float centerY; 28 private Camera camera; 29 /** 用于監(jiān)聽動(dòng)畫進(jìn)度。當(dāng)值過半時(shí)需更新txtNumber的內(nèi)容。 */ 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 // 在構(gòu)造函數(shù)之后、getTransformation()之前調(diào)用本方法。 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:動(dòng)畫進(jìn)度值,范圍為[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 // 翻轉(zhuǎn)過半的情況下,為保證數(shù)字仍為可讀的文字而非鏡面效果的文字,需翻轉(zhuǎn)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 /** 動(dòng)畫進(jìn)度監(jiān)聽器。 */ 88 public static interface InterpolatedTimeListener { 89 public void interpolatedTime(float interpolatedTime); 90 } 91 }

浙公網(wǎng)安備 33010602011771號(hào)