SurfaceView與View的區別
View通過刷新來重繪視圖,Android系統通過發出VSYNC信號來進行屏幕的重繪,刷新的間隔時間爲16ms。如果在16ms內View完成了所需要執行的所有操作,那麼在用戶的視覺上,就不會產生卡頓的感覺;而如果執行的操作邏輯太多,特別是需要頻繁刷新的界面上,例如遊戲界面,那麼就會不斷阻塞主線程,從而導致畫面卡頓。很多時候,在自定義View的Log中經常會看到如下所示的警告。
“Skipped 47 frames! The application may be doing too much work on its main thread”
這些警告的產生,很多情況下就是因爲在繪製過程中,處理邏輯太多造成的。
爲了避免這一問題的產生,Android系統提供了SurfaceView組件來解決這個問題。SurfaceView可以說是View的孿生兄弟,但它與View還是有所不同,它們的區別主要體現在以下幾點。
- View主要適用於主動更新的情況下,而SurfaceView主要適用於被動更新,例如頻繁地刷新。
- View在主線程中對畫面進行刷新,而SurfaceView通常會通過一個子線程來進行頁面的刷新。
- View在繪圖時沒有使用雙緩衝機制,而SurfaceView在底層實現機制中就已經實現了雙緩衝機制。
總之,如果你的自定義View需要頻繁刷新,或者刷新時數據處理量比較大,那麼你就可以考慮使用SurfaceView來取代View了。
SurfaceView的使用
使用SurfaceView的模板代碼:
/**
* Created by Administrator on 2016/6/1.
* 使用SurfaceView的模板代碼
*/
public class SurfaceViewTemplate extends SurfaceView implements SurfaceHolder.Callback, Runnable {
// SurfaceHolder
private SurfaceHolder mHolder;
// 用於繪圖的Canvas
private Canvas mCanvas;
// 子線程標誌位
private boolean mIsDrawing;
public SurfaceViewTemplate(Context context) {
super(context);
initView();
}
public SurfaceViewTemplate(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
public SurfaceViewTemplate(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView();
}
private void initView() {
//初始化一個SurfaceHolder對象
mHolder = getHolder();
//註冊SurfaceHolder的回調方法
mHolder.addCallback(this);
setFocusable(true);
setFocusableInTouchMode(true);
setKeepScreenOn(true);
//mHolder.setFormat(PixelFormat.OPAQUE);
}
//SurfaceView的創建
@Override
public void surfaceCreated(SurfaceHolder surfaceHolder) {
mIsDrawing = true;
//開啓子線程
new Thread(this).start();
}
//SurfaceView的改變
@Override
public void surfaceChanged(SurfaceHolder surfaceHolder, int format, int width, int height) {
}
//SurfaceView的銷燬
@Override
public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
mIsDrawing = false;
}
@Override
public void run() {
//通過循環來不停地進行繪製
while(mIsDrawing){
draw();
}
}
private void draw() {
try {
/**
* 獲取當前的Canvas繪圖對象。
* 獲取到的Canvas對象還是繼續上次的Canvas對象,而不是一個新的對象。因此,之前的繪圖操作都將保留,
* 如果需要擦除,則可以在繪製前,通過drawColor()方法來進行清屏操作。
*/
mCanvas = mHolder.lockCanvas();
//draw something
} catch (Exception e){
} finally {
if(mCanvas != null)
//將該方法放到finally代碼塊中,來保證每次都能將畫布內容進行提交
mHolder.unlockCanvasAndPost(mCanvas);
}
}
}
SurfaceView實例
正弦曲線
在界面上不斷繪製一個正弦曲線,類似示波器、心電圖、股票走勢圖等。只需要不斷修改橫縱座標的值,並讓它們滿足正弦函數即可。因此,使用一個Path對象來保存正弦函數上的座標點,在子線程的while循環中,不斷改變橫縱座標值,代碼如下:
@Override
public void run() {
while(mIsDrawing){
draw();
x += 1;
y = (int) (100 * Math.sin(x * 2 * Math.PI / 180) + 400);
mPath.lineTo(x, y);
}
}
private void draw() {
try {
mCanvas = mHolder.lockCanvas();
//SurfaceView的背景
mCanvas.drawColor(Color.WHITE);
mCanvas.drawPath(mPath, mPaint);
} catch (Exception e){
} finally {
if(mCanvas != null)
mHolder.unlockCanvasAndPost(mCanvas);
}
}
繪製效果如下圖:
繪圖板
使用SurfaceView來實現一個簡單的繪圖板,繪圖的方法與在View中進行繪圖所使用的方法一樣,也是通過Path對象來記錄手指滑動的路徑來進行繪圖。代碼如下:
@Override
public void run() {
/**
* 在前面的模板代碼中,在線程中不斷地調用draw()方法來進行繪製,擔有的時候繪製也不用這麼頻繁。
* 因此可以在子線程中進行sleep操作,儘可能地節省系統資源。
*/
long start = System.currentTimeMillis();
while(mIsDrawing){
draw();
}
long end = System.currentTimeMillis();
/**
* 通過判斷draw()方法所使用的邏輯時長來確定sleep的時長,這是一個非常通用的解決方案,代碼中的100ms是
* 一個大致的經驗值,這個值的取值一般在50ms到100ms之間。
*/
if(end - start < 100){
try {
Thread.sleep(100 - (end - start));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private void draw() {
try {
mCanvas = mHolder.lockCanvas();
mCanvas.drawColor(Color.WHITE);
mCanvas.drawPath(mPath, mPaint);
} catch (Exception e){
} finally {
if(mCanvas != null)
mHolder.unlockCanvasAndPost(mCanvas);
}
}
//記錄手指滑動的路徑
@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;
}
繪製效果如下圖: