SurfaceView與View區別
前面我們所有的講解基本都是自定義View來實現各種Android的自定義控件,但編寫過相機的Android程序員,肯定對SurfaceView不陌生,那什麼時候該用SurfaceView呢?
我們先來看一個概念,在Android中屏幕的刷新時間爲16ms,如果View能夠在16ms內完成所有的執行的繪圖操作,那麼在視覺上,界面是流暢的;否則APP就會卡頓,我們經常會看到如果View的邏輯非常複雜,Android Studio都會提示以下日誌:
Skipped 60 frames! The application maybe doing too much work on its main thread
之所以會提示這個警告,是因爲我們在自定義View的繪圖操作中,執行了非常複雜的邏輯運算,導致16s內並沒有完成繪製,所以當出現在自定義View中非常複雜的耗時的邏輯運算時,就需要使用SurfaceView。
SurfaceView在兩個方面改進了View的繪圖操作:
1.使用了雙緩衝技術
2.自帶畫布,支持在子線程中更新畫布內容
這裏說的雙緩衝技術,就是多加了一塊緩衝畫布,當需要執行繪圖操作的時候,先在緩衝畫布上繪製,繪製好後直接將緩衝畫布的內部更新到主畫布之中。這樣,在屏幕更新的時候,只需要把緩衝畫布上的內容照搬過來就可以了,就不會存在耗時的邏輯問題,也解決了超時繪製。
使用緩衝的Canvas繪圖
前面我們已經介紹了,SurfaceView時自帶畫布的,具有雙緩衝技術,那麼問題來了,我們怎麼才能拿到這塊畫布呢?直接先上代碼:
SurfaceHolder surfaceHolder=getHolder();
Canvas canvas=surfaceHodler.lockCanvas();
//中間執行繪圖操作
surfaceHolder.unlockCanvasAndPost(canvas);
我們這裏直接通過surfaceHolder.lockCanvas()獲取到了緩衝畫布,並且將畫布上鎖,防止被其他線程篡改,當繪圖完成之後釋放鎖,通過surfaceHolder.unlockCanvasAndPost(canvas)進行釋放,這段代碼不僅釋放鎖,還將緩衝畫布的內容更新到主線程的畫布上,從而顯示到屏幕中。
這裏上鎖是防止其他線程同時更新緩衝畫布,造成緩衝畫布亂七八糟,所以我們需要加鎖,至於什麼是線程鎖,死鎖,釋放鎖等知識,這是Java多線程的知識,詳情參考Java多線程書籍或者操作系統,這屬於基礎,篇幅有限,這裏就不贅述了。
SurfaceView生命週期
在講解SurfaceView生命週期之前,我們先要理解三個概念:Surface,SurfaceView,SurfaceHolder。有過MVC開發經驗的小夥伴應該會非常熟悉,SurfaceView就是視圖V,Surface中保存了緩衝畫布和繪製內容相關的各種數據,也就是模型M,SurfaceHolder很明顯就是MVC中的C控制器。
所以,當我們需要操作SurfaceView的時候,必然需要Surface存在,所以Android專門提供了監聽Surface生命週期的函數:
public class DemoSurfaceView extends SurfaceView {
private SurfaceHolder surfaceHolder;
public DemoSurfaceView(Context context) {
super(context);
}
public DemoSurfaceView(Context context, AttributeSet attrs) {
super(context, attrs);
this.surfaceHolder=getHolder();
this.surfaceHolder.addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
});
}
public DemoSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
}
上面也是每個自定義SurfaceView的基本使用方式,下面小編解釋以下Surface的生命週期。
1.surfaceCreated:當Surface對象被創建後,該函數就會調用。
2.surfaceChanged:當surface發生任何結構性變化時,可以時格式,或者大小變化,該函數就會被立即調用。
3.surfaceDestroyed:當surface將要被銷燬時調用。
一般來說,我們需要在類初始化時就立即繪圖,那麼一般放在surfaceCreated中來開啓子線程的繪圖操作,以防止沒被創建時,緩衝畫布時空的,在surfaceDestroyed中觀察線程是否執行完成,如果沒有執行完成,但surface將要被銷燬,必須強制取消線程執行。
實現天氣APP背景自動左右循環移動效果
爲了實現常用的天氣APP自動移動背景效果,我們來看看我們首先需要定義哪些成員變量,根據剛纔講的我們需要觀察線程在銷燬時,線程是否在執行,所以必須定義個線程是否執行的布爾變量,surfaceHolder控制器當然也需要,左右移動只需要X座標變化,所以也需要定義變化的X座標值,代碼如下:
private SurfaceHolder surfaceHolder;//控制器
private boolean flag=false;//線程是否能執行
private Bitmap bgBitmap;//背景圖片
private float screenWidht,screenHeight;//屏幕寬高
private int mBgX;//繪製的X座標
private Canvas canvas;//畫布
private Thread thread;//線程
//定義一個枚舉類型,判斷移動的方向
private enum State{
LEFT,RIGHT
}
private State state=State.LEFT;//開始向左運動
private final int MOVE_SIZE=1;//每次移動的距離
因爲時左右循環啊移動,送所以我們還定義了枚舉類型判斷現在時向左還是向右,同時定義畫布,屏幕寬高,以及當前運動方向,線程。
其次,我們需要監控Surface的生民週期,所以在其構造函數中調用如下方法進行監控:
public BgAnimSurfaceView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
this.surfaceHolder=getHolder();
this.surfaceHolder.addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
flag=true;//設置線程可以執行繪圖操作
startAnim();//執行動畫
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
flag=false;//設置線程不可以執行繪圖操作
}
});
}
接着,我們需要將背景圖片寬度放大到屏幕的3/2,高度爲屏幕高度,所以,我們首先必須將圖片定義到指定的大小,用到前面的Bitmap知識,代碼如下:
/***
* 執行動畫
*/
private void startAnim(){
this.screenWidht=getWidth();//獲取屏幕寬度
this.screenHeight=getHeight();//獲取屏幕高度
int enlargeWidht=(int) getWidth()*3/2;//放大的倍數
Bitmap bitmap= BitmapFactory.decodeResource(getResources(),R.drawable.background);//獲取源圖片
this.bgBitmap=Bitmap.createScaledBitmap(bitmap,enlargeWidht,(int)this.screenHeight,true);//將源圖片寬度放大3/2倍,生成新的圖片
this.thread=new Thread(new Runnable() {
@Override
public void run() {
while (flag){//如果線程可以執行
canvas=surfaceHolder.lockCanvas();
drawView();//繪製
surfaceHolder.unlockCanvasAndPost(canvas);
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
this.thread.start();
}
這段代碼就是放大圖片,然後執行左右移動,這裏使用到了本文第二個知識點,如何使用緩衝畫布,而我們將繪製的操作放在了drawView()函數中,這裏我們50ms執行一次繪圖操作,不設置間隔時間,移動可能很快,達不到慢慢移動的效果,接着我們看看drawView()代碼實現:
/***
* 開始繪製
*/
private void drawView(){
this.canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);//先清空屏幕
this.canvas.drawBitmap(this.bgBitmap,this.mBgX,0,null);//繪製圖片
switch (this.state){//判斷現在是向左還是向右移動
case LEFT:
this.mBgX-=this.MOVE_SIZE;//向左移動
break;
case RIGHT:
this.mBgX+=this.MOVE_SIZE;//向右移動
break;
default:
break;
}
//如果向左移動了1/2,那麼更改爲向右移動,本身圖片寬度只有3/2都移動了1/2顯然已經移動完了
if(this.mBgX<=-this.screenWidht/2){
this.state=State.RIGHT;
}
//如果X座標大於0,向左移動
if(this.mBgX>=0){
this.state=State.LEFT;
}
}
這樣我們就實現了天氣APP背景自動移動的效果,代碼中的註釋已經夠詳細了,這裏就不再贅述了,本文Github下載地址:點擊下載