Android中SurfaceView簡單使用
1.什麼是SurfaceView?
表面意為表層,表面,顧名思義SurfaceView就是指一個在表層的檢視物件。為什麼說是在表層呢,這是因為它有點特殊跟其他搜尋不一樣,其他檢視是繪製在“表層”的上面,而它就是充當“表層”本身.SDK的文件說到:SurfaceView就是在視窗上挖一個洞,它就是顯示在這個洞裡,所以的檢視是顯示在視窗上,所以檢視可以顯式在SurfaceView之上,你也可以新增一些層在SurfaceView之上。從API中可以看出SurfaceView屬於View的子類它是專門為製作遊戲而產生的,它的功能非常強大,最重要的是它支援OpenGL ES庫,2D和建立SurfaceView的時候需要實現SurfaceHolder.Callback介面,它可以用來監聽SurfaceView的狀態,比如:SurfaceView的改變,SurfaceView的建立,SurfaceView銷燬等,我們可以在相應的方法中做一些比如初始化的操作或者清空的操作等等。
Android的系統提供了檢視進行繪圖處理,我們通過自定義的檢視可以滿足大部分的繪圖需求,但是這有個問題就是我們通常自定義的檢視是用於主動更新情況的,使用者無法控制其繪製的速度,由於檢視是通過無效方法通知系統去呼叫view.onDraw方法進行重繪,而安卓系統是通過發出VSYNC訊號來進行螢幕的重繪,重新整理的時間是16毫秒,如果在16毫秒內搜尋完成不了執行的操作,使用者就會看著卡頓,比如當繪製方法裡執行的邏輯過多,需要頻繁重新整理的介面上,例如遊戲介面,那麼就會不斷的阻塞主執行緒,從而導致畫面卡頓。而SurfaceView相當於是另一個繪圖執行緒,它是不會阻礙主執行緒,並且它在底層實現機制中實現了雙緩衝機制。
2.如何使用SurfaceView? 首先SurfaceView也是一個View,它也有自己的生命週期。因為它需要另外一個執行緒來執行繪製操作,所以我們可以在它生命週期的初始化階段開闢一個新執行緒,然後開始執行繪製,當生命週期的結束階段我們插入結束繪製執行緒的操作。這些是由其內部一個SurfaceHolder物件完成的。
SurfaceView它的繪製原理是繪製前先鎖定畫布(獲取畫布),然後等都繪製結束以後在對畫布進行解鎖,最後在把畫布內容顯示到螢幕上。
通常情況下,使用以下步驟來建立一個SurfaceView的模板:
(1)建立SurfaceView
建立自定義的SurfaceView繼承自SurfaceView,並實現兩個介面:SurfaceHolder.Callback和Runnable。程式碼如下:
public class MySurfaceView extends SurfaceView implements SurfaceHolder.Callback,Runnable
通過實現這兩個介面,就需要在自定義的SurfaceView中實現介面的方法,對於SurfaceHolder.Callback方法,需要實現如下方法,其實就是SurfaceView的生命週期:
@Override
public void surfaceCreated(SurfaceHolder holder) {
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
對於Runnable介面,需要實現run()方法,.
@Override
public void run() {
}
(2)初始化SurfaceView
在自定義的MySurfaceView的構造方法中,需要對SurfaceView進行初始化,包括SurfaceHolder的初始化、畫筆的初始化等。在自定義的SurfaceView中,通常需要定義以下三個成員變數:
private SurfaceHolder mHolder;
private Canvas mCanvas;//繪圖的畫布
private boolean mIsDrawing;//控制繪畫執行緒的標誌位
SurfaceHolder,顧名思義,它裡面儲存了一個對Surface物件的引用,而我們執行繪製方法本質上就是操控Surface。SurfaceHolder因為儲存了對Surface的引用,所以使用它來處理Surface的生命週期。(說到底 SurfaceView的生命週期其實就是Surface的生命週期)例如使用 SurfaceHolder來處理生命週期的初始化。
(3)使用SurfaceView 通過SurfaceHolder物件的lockCanvans()方法,我們可以獲取當前的Canvas繪圖物件。接下來的操作就和自定義View中的繪圖操作一樣了。需要注意的是這裡獲取到的Canvas物件還是繼續上次的Canvas物件,而不是一個新的物件。因此,之前的繪圖操作都會被保留,如果需要擦除,則可以在繪製前,通過drawColor()方法來進行清屏操作。
繪製的時候,在surfaceCreated()方法中開啟子執行緒進行繪製,而子執行緒使用一個while(mIsDrawing)的迴圈來不停的進行繪製,在繪製的邏輯中通過lockCanvas()方法獲取Canvas物件進行繪製,通過unlockCanvasAndPost(mCanvas)方法對畫布內容進行提交。整體程式碼模板如下:
public class MyView extends SurfaceView implements SurfaceHolder.Callback {
private Paint paint;//宣告一個畫筆
public MyView(Context context) {
super(context);
paint=new Paint();//例項化畫筆物件
paint.setColor(Color.RED);
// 為SurfaceHolder新增一個SurfaceHolder.Callback回撥介面
getHolder().addCallback(this);
}
public void draw() {
Canvas canvas=getHolder().lockCanvas();//圖形繪製之前鎖定畫布
canvas.drawColor(Color.WHITE);//畫布為白色
canvas.save();//儲存畫布的狀態
canvas.drawRect(getWidth()/2,getHeight()/2,100,100,paint);
//getWidth():獲取view的寬度 drawRect畫矩形
canvas.restore();//重新使用畫布
canvas.rotate(90,getWidth()/2,getHeight()/2);//將線繞view中心旋轉90度
canvas.drawLine(0,getHeight()/2,getWidth(),getHeight(),paint);//畫線
getHolder().unlockCanvasAndPost(canvas);//圖形繪製完之後解鎖畫布
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
draw();
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
}
這裡說一個優化的地方,這就是在run方法中。 在我們的draw()方法每一次更新所耗費的時間是不確定的。舉個例子 比如第一次迴圈draw() 耗費了1000毫秒 ,第二次迴圈draw() 耗時2000毫秒。很明顯這樣就會造成執行重新整理時間時快時慢,可能出現卡頓現象。為此最好保證每次重新整理的時間是相同的,這樣可以保證整體畫面過渡流暢。
/**每30幀重新整理一次螢幕**/
public static final int TIME_IN_FRAME = 30;
@Override
public void run() {
while (mIsRunning) {
/**取得更新之前的時間**/
long startTime = System.currentTimeMillis();
/**在這裡加上執行緒安全鎖**/
synchronized (mSurfaceHolder) {
/**拿到當前畫布 然後鎖定**/
mCanvas =mSurfaceHolder.lockCanvas();
draw();
/**繪製結束後解鎖顯示在螢幕上**/
mSurfaceHolder.unlockCanvasAndPost(mCanvas);
}
/**取得更新結束的時間**/
long endTime = System.currentTimeMillis();
/**計算出一次更新的毫秒數**/
int diffTime = (int)(endTime - startTime);
/**確保每次更新時間為30幀**/
while(diffTime <=TIME_IN_FRAME) {
diffTime = (int)(System.currentTimeMillis() - startTime);
/**執行緒等待**/
Thread.yield();
}
}
}
這裡說一下Thread.yield(): 與Thread.sleep(long millis):的區別: Thread.yield(): 是暫停當前正在執行的執行緒物件 ,並去執行其他執行緒。
Thread.sleep(long millis):則是使當前執行緒暫停引數中所指定的毫秒數然後在繼續執行執行緒。
3.SurfaceView的使用例項 (1)正弦曲線
要繪製一個正弦曲線,只需要不斷修改橫縱座標的值,並讓他們滿足正弦函式即可。因此,我們需要一個Path物件來儲存正弦函式上的座標點,在子執行緒的while迴圈中,不斷改變橫縱座標值。程式碼如下:
public static final int TIME_IN_FRAME = 30;
@Override
public void run() {
long startTime = System.currentTimeMillis();
while(mIsDrawing){
draw();
// x+=1;
// y=(int)(100*Math.sin(x*2*Math.PI/180)+400);
// mPath.lineTo(x,y);
}
/**取得更新結束的時間**/
long endTime = System.currentTimeMillis();
/**計算出一次更新的毫秒數**/
int diffTime = (int)(endTime - startTime);
/**確保每次更新時間為30幀**/
while(diffTime <=TIME_IN_FRAME) {
diffTime = (int)(System.currentTimeMillis() - startTime);
/**執行緒等待**/
Thread.yield();
}
}
(2)畫圖板
我們也可以通過使用SurfaceView來實現一個簡單的繪圖板,繪圖的方法與View中進行繪圖所使用的方法一樣,也是通過Path物件記錄手指滑動的路徑來進行繪圖。在SurfaceView的onTouchEvent()方法中記錄Path路徑,程式碼如下所示:
@Override
public boolean onTouchEvent(MotionEvent event) {
int x=(int)event.getX();
int y=(int)event.getY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
mPath.moveTo(x,y);
break;
case MotionEvent.ACTION_MOVE:
mPath.lineTo(x,y);
break;
case MotionEvent.ACTION_UP:
break;
}
return true;//表示此View攔截處理觸控事件
}
並在draw方法中進行繪製:
private void draw() {
try{
mCanvas=mHolder.lockCanvas();//獲取Canvas物件進行繪製
//SurfaceView背景
mCanvas.drawColor(Color.WHITE);
mCanvas.drawPath(mPath,mPaint);
}catch (Exception e){
e.printStackTrace();
}finally {
if (mCanvas!=null){
mHolder.unlockCanvasAndPost(mCanvas);//保證繪製的畫布內容提交
}
}
}
4.SurfaceView和View的區別 總的歸納起來SurfaceView和View不同之處有:
1. SurfaceView允許其他執行緒更新檢視物件(執行繪製方法)而View不允許這麼做,它只允許UI執行緒更新檢視物件。
2. SurfaceView是放在其他最底層的檢視層次中,所有其他檢視層都在它上面,所以在它之上可以新增一些層,而且它不能是透明的。
3. 它執行動畫的效率比View高,而且你可以控制幀數。
4. SurfaceView在繪圖時使用l了雙緩衝機制,而View沒有。