Android自定義控件(十)——SurfaceView實戰實現天氣APP背景移動效果


實現效果

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下載地址:點擊下載

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章