Android組件View繪製流程原理分析

android視圖構成

這裏寫圖片描述

如上圖,Activity的window組成,Activity內部有個Window成員,它的實例爲PhoneWindow,PhoneWindow有個內部類是DecorView,這個DecorView就是存放佈局文件的,裏面有TitleActionBar和我們setContentView傳入進去的layout佈局文件

  • Window類時一個抽象類,提供繪製窗口的API
  • PhoneWindow是繼承Window的一個具體的類,該類內部包含了一個DecorView對象,該DectorView對象是所有應用窗口(Activity界面)的根View
  • DecorView繼承FrameLayout,裏面id=content的就是我們傳入的佈局視圖

依據面向對象從抽象到具體我們可以類比上面關係就像如下: Window是一塊電子屏,PhoneWindow是一塊手機電子屏,DecorView就是電子屏要顯示的內容,Activity就是手機電子屏安裝位置

setContentView流程

setContentView整個過程主要是如何把Activity的佈局文件或者java的View添加至窗口裏,重點概括爲:

  1. 創建一個DecorView的對象mDecor,該mDecor對象將作爲整個應用窗口的根視圖。
  2. 依據Feature等style theme創建不同的窗口修飾佈局文件,並且通過findViewById獲取Activity佈局文件該存放的地方(窗口修飾佈局文件中id爲content的FrameLayout)。
  3. 將Activity的佈局文件添加至id爲content的FrameLayout內。
  4. 當setContentView設置顯示OK以後會回調Activity的onContentChanged方法。Activity的各種View的findViewById()方法等都可以放到該方法中,系統會幫忙回調。

android的View繪製

view繪製主要包括三個方面:

  • measure 測量組件本身的大小
  • layout 確定組件在視圖中的位置
  • draw 根據位置和大小,將組件畫出來

視圖繪製的起點在ViewRootImpl類的performTraversals()方法,該方法完成的工作主要是: 根據之前的狀態,判定是否重新計算測試視圖大小(measure)、是佛重新放置視圖位置(layout)和是否重新重繪視圖(draw) ,部分源碼如下:

private void performTraversals() {
        ......        //最外層的根視圖的widthMeasureSpec和heightMeasureSpec由來
        //lp.width和lp.height在創建ViewGroup實例時等於MATCH_PARENT
        int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);        int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
        ......
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        ......
        mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
        ......
        mView.draw(canvas);
        ......
    }

measure計算視圖大小

幾乎所有的組件都是繼承View類的,而關於view的測量工作,日常開發用得多的方法就是measure和onMeasure兩個方法,measure不可重寫,當我們自定義時主要重寫onMeasure方法即可,在方法內部我們必須完成組件的mMeasuredWidth和mMeasuredHeight實際尺寸測量,而這個尺寸是需要父視圖和子視圖共同決定的

measure流程從根視圖measure遍歷整個view樹結構,如下:

這裏寫圖片描述

還要注意視圖尺寸MeasureSpec是一個組合尺寸,它是一個32位bit值,高兩位是尺寸模式specMode,低30位是尺寸大小值,我們可以利用提供的原聲庫方法很方便的進行尺寸組合和拆解: specMode有三種: MeasureSpec.EXACTLY表示確定大小, MeasureSpec.AT_MOST表示最大大小, MeasureSpec.UNSPECIFIED不確定

int measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);    //合成int specMode = MeasureSpec.getMode(measureSpec);                                   //拆解int specSize = MeasureSpec.getSize(measureSpec);

而在視圖測量meause中,父組件傳給子組件的一般都是一個組合尺寸,我們可以拿出具體尺寸然後根據其他條件產生一個新的尺寸值,將這個值用setMeasuredDimension設置mMeasuredWidth和mMeasuredHeight具體尺寸,完成測量;

measure原理總結

  • MeasureSpec(View的內部類)測量規格爲int型,值由高2位規格模式specMode和低30位具體尺寸specSize組成。其中specMode只有三種值: MeasureSpec.EXACTLY //確定模式,父View希望子View的大小是確定的,由specSize決定;MeasureSpec.AT_MOST //最多模式,父View希望子View的大小最多是specSize指定的值;MeasureSpec.UNSPECIFIED //未指定模式,父View完全依據子View的設計值來決定;
  • View的measure方法是final的,不允許重載,View子類只能重載onMeasure來完成自己的測量邏輯。
  • 最頂層DecorView測量時的MeasureSpec是由ViewRootImpl中getRootMeasureSpec方法確定的(LayoutParams寬高參數均爲MATCH_PARENT,specMode是EXACTLY,specSize爲物理屏幕大小)。
  • ViewGroup類提供了measureChild,measureChild和measureChildWithMargins方法,簡化了父子View的尺寸計算。
  • 只要是ViewGroup的子類就必須要求LayoutParams繼承子MarginLayoutParams,否則無法使用layout_margin參數。
  • View的佈局大小由父View和子View共同決定。
  • 使用View的getMeasuredWidth()和getMeasuredHeight()方法來獲取View測量的寬高,必須保證這兩個方法在onMeasure流程之後被調用才能返回有效值。

layout視圖位置確定

layout的流程主要也是遍歷整個view樹結構,調用view.layout(int l, int t, int r, int b)確定好view的具體座標位置,流程圖如下

這裏寫圖片描述

當我們自定義一個組件時,通常時重寫onLayout方法,裏面實現好自己的邏輯,最後在調用layout方法完成視圖位置確定,如果自定義組件時一個ViewGroup的話,還需要我們去遍歷每一個child確定尺寸

layout原理總結

  • 整個layout過程比較容易理解,從上面分析可以看出layout也是從頂層父View向子View的遞歸調用view.layout方法的過程,即父View根據上一步measure子View所得到的佈局大小和佈局參數,將子View放在合適的位置上。具體layout核心主要有以下幾點:
  • View.layout方法可被重載,ViewGroup.layout爲final的不可重載,ViewGroup.onLayout爲abstract的,子類必須重載實現自己的位置邏輯。
  • measure操作完成後得到的是對每個View經測量過的measuredWidth和measuredHeight,layout操作完成之後得到的是對每個View進行位置分配後的mLeft、mTop、mRight、mBottom,這些值都是相對於父View來說的。
  • 凡是layout_XXX的佈局屬性基本都針對的是包含子View的ViewGroup的,當對一個沒有父容器的View設置相關layout_XXX屬性是沒有任何意義的(前面《Android應用setContentView與LayoutInflater加載解析機制源碼分析》也有提到過)。
  • 使用View的getWidth()和getHeight()方法來獲取View測量的寬高,必須保證這兩個方法在onLayout流程之後被調用才能返回有效值。

draw繪製

完成measure和Layout後,ViewRootImpl中的代碼會創建一個Canvas對象,然後調用View的draw()方法來執行具體的繪製工。所以又迴歸到了ViewGroup與View的樹狀遞歸draw過程 先來看下View樹的遞歸draw流程圖,如下:

這裏寫圖片描述

draw原理總結

可以看見,繪製過程就是把View對象繪製到屏幕上,整個draw過程需要注意如下細節:

  • 如果該View是一個ViewGroup,則需要遞歸繪製其所包含的所有子View。
  • View默認不會繪製任何內容,真正的繪製都需要自己在子類中實現。
  • View的繪製是藉助onDraw方法傳入的Canvas類來進行的。
  • 區分View動畫和ViewGroup佈局動畫,前者指的是View自身的動畫,可以通過setAnimation添加,後者是專門針對ViewGroup顯示內部子視圖時設置的動畫,可以在xml佈局文件中對ViewGroup設置layoutAnimation屬性(譬如對LinearLayout設置子View在顯示時出現逐行、隨機、下等顯示等不同動畫效果)。
  • 在獲取畫布剪切區(每個View的draw中傳入的Canvas)時會自動處理掉padding,子View獲取Canvas不用關注這些邏輯,只用關心如何繪製即可。
  • 默認情況下子View的ViewGroup.drawChild繪製順序和子View被添加的順序一致,但是你也可以重載ViewGroup.getChildDrawingOrder()方法提供不同順序。

view提供的API控制視圖的方法

invalidate和postInvalidate方法源碼分析

請求重新繪製視圖,調用draw

  • invalidate在主線程調用
  • postInvalidate是在非主線程調用

View的requestLayout方法

requestLayout()方法會調用measure過程和layout過程,不會調用draw過程,也不會重新繪製任何View包括該調用者本身。

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