[Android] SurfaceView使用實例

同樣,先上效果圖如下:

 

效果圖中,拋物線的動畫即是由SurfaceView實現的。底部欄中的文字翻轉詳情相關帖子:
[Android] 文字翻轉動畫的實現
需求:
1.實現拋物線動畫
   1.1 設計物理模型,能夠根據時間變量計算出某個時刻圖片的X/Y坐標。
   1.2 將圖片高頻率(相比於UI線程的緩慢而言)刷新到界面中。這兒需要實現將臟界面清屏及刷新操作。
2.文字翻轉動畫(已解決,見上面的帖子鏈接)

下面來逐一解決所提出的問題。

—————————————————————————–
分隔線內容與Android無關,請慎讀,勿拍磚。謝啦

 

1.1 設計物理模型,如果大傢還記得初中物理時,這並不難。自己寫的草稿圖見下:

 

可以有:圖片要從高度為H的位置下落,並且第一次與X軸碰撞時會出現能量損失,至原來的N%。並且我們需要圖片的最終落點離起始位置在X軸上的位移為L,默認存在重力加速度g。
詳細的物理分析見上圖啦,下面隻說代碼中如何實現,相關代碼在PhysicalTool.java。
第一次下落過程所耗時t1與高度height會有如下關系:

[java]
t1 = Math.sqrt(2 * height * 1.0d / GRAVITY); 

第一次與X軸碰撞後上升至最高點的耗時t2與高度 N%*height會有:

[java] 
t2 = Math.sqrt((1 – WASTAGE) * 2 * height * 1.0d / GRAVITY); 

那麼總的動畫時間為(t1 + t2 + t2),則水平位移速度有(width為X軸總位移):

[java] 
velocity = width * 1.0d / (t1 + 2 * t2); 

則根據時間計算圖片的實時坐標有:
PhysicalTool.comput()

[java] 
double used = (System.currentTimeMillis() – startTime) * 1.0d / 1000; 
x = velocity * used; 
if (0 <= used && used < t1) { 
        y = height – 0.5d * GRAVITY * used * used; 
} else if (t1 <= used && used < (t1 + t2)) { 
        double tmp = t1 + t2 – used; 
        y = (1 – WASTAGE) * height – 0.5d * GRAVITY * tmp * tmp; 
} else if ((t1 + t2) <= used && used < (t1 + 2 * t2)) { 
        double tmp = used – t1 – t2; 
        y = (1 – WASTAGE) * height – 0.5d * GRAVITY * tmp * tmp; 

Android無關內容結束瞭。
—————————————————————————————-

1.2 SurfaceView刷新界面
        SurfaceView是一個特殊的UI組件,特殊在於它能夠使用非UI線程刷新界面。至於為何具有此特殊性,將在另一個帖子"SurfaceView 相關知識筆記"中討論,該帖子將講述SurfaceView、Surface、ViewRoot、Window Manager/Window、Canvas等之間的關系。
       
        使用SurfaceView需要自定義組件繼承該類,並實現SurfaceHolder.Callback,該回調提供瞭三個方法:

[java] 
surfaceCreated()//通知Surface已被創建,可以在此處啟動動畫線程 
surfaceChanged()//通知Surface已改變 
surfaceDestroyed()//通知Surface已被銷毀,可以在此處終止動畫線程 

SurfaceView使用有一個原則,即該界面操作必須在surfaceCreated之後及surfaceDestroyed之前。該回調的監聽通過SurfaceHolder設置。代碼如下:
[java] 
//於SurfaceView類中,該類實現SurfaceHolder.Callback接口,如本例中的ParabolaView 
SurfaceHolder holder = getHolder(); 
holder.addCallback(this); 

示例代碼中,通過啟動DrawThread調用handleThread()實現對SurfaceView的刷新。
        刷新界面首先需要執行holder.lockCanvas()鎖定Canvas並獲得Canvas實例,然後進行界面更新操作,最後結束鎖定Canvas,提交界面更改,至Surface最終顯示在屏幕上。
        代碼如下:
[java] 
canvas = holder.lockCanvas(); 
… … … …  
… … … …  
canvas.drawBitmap(bitmap, x, y, paint); 
holder.unlockCanvasAndPost(canvas); 

本例中,需要清除屏幕臟區域,出於簡便的做法,是將整個SurfaceView背景重復地設置為透明,代碼為:

[java] 
canvas.drawColor(Color.TRANSPARENT, android.graphics.PorterDuff.Mode.CLEAR); 

對於SurfaceView的操作,下面這個鏈接講述得更詳細,更易理解,推薦去看下:
Android開發之SurfaceView
慣例,Java代碼如下,XML請自行實現
本文由Sodino所有,轉載請註明出處:http://blog.csdn.net/sodino/article/details/7704084

[java] 
ActSurfaceView.java 
 
package lab.sodino.surfaceview; 
 
import lab.sodino.surfaceview.RotateAnimation.InterpolatedTimeListener; 
import android.app.Activity; 
import android.graphics.BitmapFactory; 
import android.os.Bundle; 
import android.os.Handler; 
import android.os.Handler.Callback; 
import android.os.Message; 
import android.view.View; 
import android.view.View.OnClickListener; 
import android.view.ViewGroup; 
import android.widget.Button; 
import android.widget.TextView; 
 
public class ActSurfaceView extends Activity implements OnClickListener, ParabolaView.ParabolaListener, Callback, 
                InterpolatedTimeListener { 
        public static final int REFRESH_TEXTVIEW = 1; 
        private Button btnStartAnimation; 
        /** 動畫界面。 */ 
        private ParabolaView parabolaView; 
        /** 購物車處顯示購物數量的TextView。 */ 
        private TextView txtNumber; 
        /** 購物車中的數量。 */ 
        private int number; 
        private Handler handler; 
        /** TextNumber是否允許顯示最新的數字。 */ 
        private boolean enableRefresh; 
 
        public void onCreate(Bundle savedInstanceState) { 
                super.onCreate(savedInstanceState); 
                setContentView(R.layout.main); 
 
                handler = new Handler(this); 
 
                number = 0; 
 
                btnStartAnimation = (Button) findViewById(R.id.btnStartAnim); 
                btnStartAnimation.setOnClickListener(this); 
 
                parabolaView = (ParabolaView) findViewById(R.id.surfaceView); 
                parabolaView.setParabolaListener(this); 
 
                txtNumber = (TextView) findViewById(R.id.txtNumber); 
        } 
 
        public void onClick(View v) { 
                if (v == btnStartAnimation) { 
                        LogOut.out(this, "isShowMovie:" + parabolaView.isShowMovie()); 
                        if (parabolaView.isShowMovie() == false) { 
                                number++; 
                                enableRefresh = true; 
                                parabolaView.setIcon(BitmapFactory.decodeResource(getResources(), R.drawable.icon)); 
                                // 設置起始Y軸高度和終止X軸位移 
                                parabolaView.setParams(200, ((ViewGroup) txtNumber.getParent()).getLeft()); 
                                parabolaView.showMovie(); 
                        } 
                } 
        } 
 
        public void onParabolaStart(ParabolaView view) { 
 
        } 
 
        public void onParabolaEnd(ParabolaView view) { 
                handler.sendEmptyMessage(REFRESH_TEXTVIEW); 
        } 
 
        public boolean handleMessage(Message msg) { 
                switch (msg.what) { 
                case REFRESH_TEXTVIEW: 
 
                        if (txtNumber.getVisibility() != View.VISIBLE) { 
                                txtNumber.setVisibility(View.VISIBLE); 
                        } 
                        RotateAnimation anim = new RotateAnimation(txtNumber.getWidth() >> 1, txtNumber.getHeight() >> 1, 
                                        RotateAnimation.ROTATE_INCREASE); 
                        anim.setInterpolatedTimeListener(this); 
                        txtNumber.startAnimation(anim); 
                        break; 
                } 
                return false; 
        } 
 
        @Override 
        public void interpolatedTime(float interpolatedTime) { 
                // 監聽到翻轉進度過半時,更新txtNumber顯示內容。 
                if (enableRefresh && interpolatedTime > 0.5f) { 
                        txtNumber.setText(Integer.toString(number)); 
                        // Log.d("ANDROID_LAB", "setNumber:" + number); 
                        enableRefresh = false; 
                } 
        } 

[java] 
DrawThread.java 
 
package lab.sodino.surfaceview; 
 
import android.view.SurfaceView; 
 
/**
 * @author Sodino E-mail:sodinoopen@hotmail.com
 * @version Time:2012-6-18 上午03:14:31
 */ 
public class DrawThread extends Thread { 
        private SurfaceView surfaceView; 
        private boolean running; 
 
        public DrawThread(SurfaceView surfaceView) { 
                this.surfaceView = surfaceView; 
        } 
 
        public void run() { 
                if (surfaceView == null) { 
                        return; 
                } 
                if (surfaceView instanceof ParabolaView) { 
                        ((ParabolaView) surfaceView).handleThread(); 
                } 
        } 
 
        public void setRunning(boolean b) { 
                running = b; 
        } 
 
        public boolean isRunning() { 
                return running; 
        } 

[java] 
ParabolaView.java 
package lab.sodino.surfaceview; 
 
import android.content.Context; 
import android.graphics.Bitmap; 
import android.graphics.Canvas; 
import android.graphics.Color; 
import android.graphics.Paint; 
import android.graphics.PixelFormat; 
import android.util.AttributeSet; 
import android.view.SurfaceHolder; 
import android.view.SurfaceView; 
 
/**
 * @author Sodino E-mail:sodinoopen@hotmail.com
 * @version Time:2012-6-18 上午02:52:33
 */ 
public class ParabolaView extends SurfaceView implements SurfaceHolder.Callback { 
        /** 每30ms刷一幀。 */ 
        private static final long SLEEP_DURATION = 10l; 
        private SurfaceHolder holder; 
        /** 動畫圖標。 */ 
        private Bitmap bitmap; 
        private DrawThread thread; 
        private PhysicalTool physicalTool; 
        private ParabolaView.ParabolaListener listener; 
        /** 默認未創建,相當於Destory。 */ 
        private boolean surfaceDestoryed = true; 
 
        public ParabolaView(Context context, AttributeSet attrs, int defStyle) { 
                super(context, attrs, defStyle); 
                init(); 
        } 
 
        public ParabolaView(Context context, AttributeSet attrs) { 
                super(context, attrs); 
                init(); 
        } 
 
        public ParabolaView(Context context) { 
                super(context); 
                init(); 
        } 
 
        private void init() { 
                holder = getHolder(); 
                holder.addCallback(this); 
                holder.setFormat(PixelFormat.TRANSPARENT); 
 
                setZOrderOnTop(true); 
                // setZOrderOnTop(false); 
 
                physicalTool = new PhysicalTool(); 
        } 
 
        @Override 
        public void surfaceCreated(SurfaceHolder holder) { 
                surfaceDestoryed = false; 
        } 
 
        @Override 
        public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { 
 
        } 
 
        @Override 
        public void surfaceDestroyed(SurfaceHolder holder) { 
                LogOut.out(this, "surfaceDestroyed"); 
                surfaceDestoryed = true; 
                physicalTool.cancel(); 
        } 
 
        public void handleThread() { 
                Canvas canvas = null; 
 
                Paint pTmp = new Paint(); 
                pTmp.setAntiAlias(true); 
                pTmp.setColor(Color.RED); 
 
                Paint paint = new Paint(); 
                // 設置抗鋸齒 
                paint.setAntiAlias(true); 
                paint.setColor(Color.CYAN); 
                physicalTool.start(); 
                LogOut.out(this, "doing:" + physicalTool.doing()); 
                if (listener != null) { 
                        listener.onParabolaStart(this); 
                } 
                while (physicalTool.doing()) { 
                        try { 
                                physicalTool.compute(); 
                                canvas = holder.lockCanvas(); 
                                // 設置畫佈的背景為透明。 
                                canvas.drawColor(Color.TRANSPARENT, android.graphics.PorterDuff.Mode.CLEAR); 
                                // 繪上新圖區域 
                                float x = (float) physicalTool.getX(); 
                                // float y = (float) physicalTool.getY(); 
                                float y = (float) physicalTool.getMirrorY(getHeight(), bitmap.getHeight()); 
                                // LogOut.out(this, "x:" + x + " y:" + y); 
                                canvas.drawRect(x, y, x + bitmap.getWidth(), y + bitmap.getHeight(), pTmp); 
                                canvas.drawBitmap(bitmap, x, y, paint); 
                                holder.unlockCanvasAndPost(canvas); 
                                Thread.sleep(SLEEP_DURATION); 
                        } catch (Exception e) { 
                                e.printStackTrace(); 
                        } 
                } 
                // 清除屏幕內容 
                // 直接按"Home"回桌面,SurfaceView被銷毀瞭,lockCanvas返回為null。 
                if (surfaceDestoryed == false) { 
                        canvas = holder.lockCanvas(); 
                        canvas.drawColor(Color.TRANSPARENT, android.graphics.PorterDuff.Mode.CLEAR); 
                        holder.unlockCanvasAndPost(canvas); 
                } 
 
                thread.setRunning(false); 
                if (listener != null) { 
                        listener.onParabolaEnd(this); 
                } 
        } 
 
        public void showMovie() { 
                if (thread == null) { 
                        thread = new DrawThread(this); 
                } else if (thread.getState() == Thread.State.TERMINATED) { 
                        thread.setRunning(false); 
                        thread = new DrawThread(this); 
                } 
                LogOut.out(this, "thread.getState:" + thread.getState()); 
                if (thread.getState() == Thread.State.NEW) { 
                        thread.start(); 
                } 
        } 
 
        /** 正在播放動畫時,返回true;否則返回false。 */ 
        public boolean isShowMovie() { 
                return physicalTool.doing(); 
        } 
 
        public void setIcon(Bitmap bit) { 
                bitmap = bit; 
        } 
 
        public void setParams(int height, int width) { 
                physicalTool.setParams(height, width); 
        } 
 
        /** 設置拋物線的動畫監聽器。 */ 
        public void setParabolaListener(ParabolaView.ParabolaListener listener) { 
                this.listener = listener; 
        } 
 
        static interface ParabolaListener { 
                public void onParabolaStart(ParabolaView view); 
 
                public void onParabolaEnd(ParabolaView view); 
        } 

 

[java] 
PhysicalTool.java 
package lab.sodino.surfaceview; 
 
/**
 * @author Sodino E-mail:sodinoopen@hotmail.com
 * @version Time:2012-6-18 上午06:07:16
 */ 
public class PhysicalTool { 
        /** 重力加速度值。 */ 
        private static final float GRAVITY = 400.78033f; 
        /** 與X軸碰撞後,重力勢能損失掉的百分比。 */ 
        private static final float WASTAGE = 0.3f; 
        /** 起始下降高度。 */ 
        private int height; 
        /** 起始點到終點的X軸位移。 */ 
        private int width; 
        /** 水平位移速度。 */ 
        private double velocity; 
        /** X Y坐標。 */ 
        private double x, y; 
        /** 動畫開始時間。 */ 
        private long startTime; 
        /** 首階段下載的時間。 單位:毫秒。 */ 
        private double t1; 
        /** 第二階段上升與下載的時間。 單位:毫秒。 */ 
        private double t2; 
        /** 動畫正在進行時值為true,反之為false。 */ 
        private boolean doing; 
 
        public void start() { 
                startTime = System.currentTimeMillis(); 
                doing = true; 
        } 
 
        /** 設置起始下落的高度及水平初速度;並以此計算小球下落的第一階段及第二階段上升耗時。 */ 
        public void setParams(int h, int w) { 
                height = h; 
                width = w; 
 
                t1 = Math.sqrt(2 * height * 1.0d / GRAVITY); 
                t2 = Math.sqrt((1 – WASTAGE) * 2 * height * 1.0d / GRAVITY); 
                velocity = width * 1.0d / (t1 + 2 * t2); 
                LogOut.out(this, "t1=" + t1 + " t2=" + t2); 
        } 
 
        /** 根據當前時間計算小球的X/Y坐標。 */ 
        public void compute() { 
                double used = (System.currentTimeMillis() – startTime) * 1.0d / 1000; 
                x = velocity * used; 
                if (0 <= used && used < t1) { 
                        y = height – 0.5d * GRAVITY * used * used; 
                } else if (t1 <= used && used < (t1 + t2)) { 
                        double tmp = t1 + t2 – used; 
                        y = (1 – WASTAGE) * height – 0.5d * GRAVITY * tmp * tmp; 
                } else if ((t1 + t2) <= used && used < (t1 + 2 * t2)) { 
                        double tmp = used – t1 – t2; 
                        y = (1 – WASTAGE) * height – 0.5d * GRAVITY * tmp * tmp; 
                } else { 
                        LogOut.out(this, "used:" + used + " set doing false"); 
                        x = velocity * (t1 + 2 * t2); 
                        y = 0; 
                        doing = false; 
                } 
        } 
 
        public double getX() { 
                return x; 
        } 
 
        public double getY() { 
                return y; 
        } 
 
        /** 反轉Y軸正方向。適應手機的真實坐標系。 */ 
        public double getMirrorY(int parentHeight, int bitHeight) { 
                int half = parentHeight >> 1; 
                double tmp = half + (half – y); 
                tmp -= bitHeight; 
                return tmp; 
        } 
 
        public boolean doing() { 
                return doing; 
        } 
 
        public void cancel() { 
                doing = false; 
        } 

[java] 
RotateAnimation.java 
package lab.sodino.surfaceview; 
 
import android.graphics.Camera; 
import android.graphics.Matrix; 
import android.view.animation.Animation; 
import android.view.animation.Transformation; 
 
/**
 * @author Sodino E-mail:sodinoopen@hotmail.com
 * @version Time:2012-6-27 上午07:32:00
 */ 
public class RotateAnimation extends Animation { 
        /** 值為true時可明確查看動畫的旋轉方向。 */ 
        public static final boolean DEBUG = false; 
        /** 沿Y軸正方向看,數值減1時動畫逆時針旋轉。 */ 
        public static final boolean ROTATE_DECREASE = true; 
        /** 沿Y軸正方向看,數值減1時動畫順時針旋轉。 */ 
        public static final boolean ROTATE_INCREASE = false; 
        /** Z軸上最大深度。 */ 
        public static final float DEPTH_Z = 310.0f; 
        /** 動畫顯示時長。 */ 
        public static final long DURATION = 800l; 
        /** 圖片翻轉類型。 */ 
        private final boolean type; 
        private final float centerX; 
        private final float centerY; 
        private Camera camera; 
        /** 用於監聽動畫進度。當值過半時需更新txtNumber的內容。 */ 
        private InterpolatedTimeListener listener; 
 
        public RotateAnimation(float cX, float cY, boolean type) { 
                centerX = cX; 
                centerY = cY; 
                this.type = type; 
                setDuration(DURATION); 
        } 
 
        public void initialize(int width, int height, int parentWidth, int parentHeight) { 
                // 在構造函數之後、getTransformation()之前調用本方法。 
                super.initialize(width, height, parentWidth, parentHeight); 
                camera = new Camera(); 
        } 
 
        public void setInterpolatedTimeListener(InterpolatedTimeListener listener) { 
                this.listener = listener; 
        } 
 
        protected void applyTransformation(float interpolatedTime, Transformation transformation) { 
                // interpolatedTime:動畫進度值,范圍為[0.0f,10.f] 
                if (listener != null) { 
                        listener.interpolatedTime(interpolatedTime); 
                } 
                float from = 0.0f, to = 0.0f; 
                if (type == ROTATE_DECREASE) { 
                        from = 0.0f; 
                        to = 180.0f; 
                } else if (type == ROTATE_INCREASE) { 
                        from = 360.0f; 
                        to = 180.0f; 
                } 
                float degree = from + (to – from) * interpolatedTime; 
                boolean overHalf = (interpolatedTime > 0.5f); 
                if (overHalf) { 
                        // 翻轉過半的情況下,為保證數字仍為可讀的文字而非鏡面效果的文字,需翻轉180度。 
                        degree = degree – 180; 
                } 
                // float depth = 0.0f; 
                float depth = (0.5f – Math.abs(interpolatedTime – 0.5f)) * DEPTH_Z; 
                final Matrix matrix = transformation.getMatrix(); 
                camera.save(); 
                camera.translate(0.0f, 0.0f, depth); 
                camera.rotateY(degree); 
                camera.getMatrix(matrix); 
                camera.restore(); 
                if (DEBUG) { 
                        if (overHalf) { 
                                matrix.preTranslate(-centerX * 2, -centerY); 
                                matrix.postTranslate(centerX * 2, centerY); 
                        } 
                } else { 
                        matrix.preTranslate(-centerX, -centerY); 
                        matrix.postTranslate(centerX, centerY); 
                } 
        } 
 
        /** 動畫進度監聽器。 */ 
        public static interface InterpolatedTimeListener { 
                public void interpolatedTime(float interpolatedTime); 
        } 

作者:sodino

發佈留言