開發藝術探索--View的工作原理

第四章,View的工作原理

本章主要介紹兩方面的內容
1. View的工作原理
2. 自定義View的實現方式

需要掌握:View的三大流程;View的常見回調方法;View滑動(上一章中的滑動衝突處理)

大綱

ViewRoot 和 DecorView
MeasureSpec
View工作流程
自定義View

初識ViewRoot 和 DecorView

ViewRoot的實現類是ViewRootImpl,是鏈接WindowManagerDecorView的紐帶,View的三大流程都是通過ViewRoot來完成的

一. ViewRoot

View的繪製流程是從ViewRootperformTraversals開始的.

//這裏面的三大流程中,前面的方法會調用後面的方法,ps:performMeasure會調用measure,measure會調用onMeasure
ViewRoot.performTraversals ->
(performMeasure)measure(onMeasure) ->
(performLayout)layout(onLayout)-> 
(performDraw)draw(onDraw)
  1. 其中measure用來測量View的寬和高,measure後即可獲取到View測量的寬高.
  2. layout用來確定View在父容器中的放置位置
  3. draw則負責將View繪製在屏幕上,draw會調用dispatchDraw來對子View進行draw,只有draw方法完成後View才能顯示在屏幕上.

二. DecorView

  1. DecorView是一個FrameLayout
  2. DecorView是一個頂級View,一般裏面包含一個LinearLayout,上面的是標題欄,下面的是內容欄.
  3. setContentView就是將佈局文件設置到內容區(id爲android.R.id.contentFrameLayout中);

理解MeasureSpec

MeasureSpec 有點像測量規格或者測量說明書,View的尺寸和規格受MeasureSpec父容器 影響.

系統會將ViewLayoutParams根據父容器所施加的規則轉換成對應的MeasureSpec

MeasureSpec

  1. 一個32位的int值,SpecMode(高2位),SpecSize(低30位).
  2. 有三類SpecMode,UNSPECIFIED(沒有限制),EXACTLY(精確性),AT_MOST(不能大於這個值)

MeasureSpec與LayoutParams的關係

對於DecorView,其MeasureSpec窗口尺寸自身的LayoutParams共同決定.

  1. DecorViewMeasureSpec創建過程,可以查看ViewRootImpl#measureHierarchy,其中會調用ViewRootImpl#getRootMeasureSpec,
  2. EXACTLY模式下,DecorView大小就是窗口大小
  3. AT_MOST模式下,DecorView大小不定,不超過窗口大小
  4. 固定大小(EXACTLY),大小爲LayoutParams中指定的大小.

對於普通View,其MeasureSpec父容器的MeasureSpec自身的LayoutParams共同決定.

  1. 查看ViewGroup#measureChildWithMargins,其中會調用ViewGroup#getChildMeasureSpec得到子元素的MeasureSpec.
  2. 子元素的MeasureSpec與父容器的MeasureSpec和本身的LayoutParamsView的Margin與Padding有關.

getChildMeasureSpec

View工作流程

一. measure 過程

View的measure過程

//View#onMeasure
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
  • 一般情況下getDefaultSize得到的就是View測量後的大小.
  • setMeasuredDimension()方法調用之後,我們才能使用getMeasuredWidth()getMeasuredHeight()來獲取視圖測量出的寬高,以此之前調用這兩個方法得到的值都會是0.

  • View的最終大小在onLayout中確定,而測量大小onMeasure中確定,大多數情況下他們是相等的

  • getDefaultSize一般情況下,返回的是測量後的大小,在UNSPECIFIED模式下才返回getSuggestedMinimumWidth()getSuggestedMinimumHeight()

    public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }
  • getSuggestMinimumWidth對應如下情況:

    1. view沒背景則對應android:minWidth,如果此屬性沒指定,則爲0
    2. view有背景則是minWidth(上面屬性對應的值)和 background的minimumWidth的最大值,
    3. 通過Drawable#getMinimumWidth看出background的minimumWidth返回的是Drawable的原始寬高.
  • 直接繼承View的自定義控件需要重寫onMeasure方法並設置wrap_content時的自身大小,否則在佈局中使用wrap_content時就相當於使用match_parent

    解決方法: 在wrap_content時,給View設置一個默認的內部寬/高

//解決示例
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        if (widthSpecMode == MeasureSpec.AT_MOST
                && heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(200, 200);
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(200, heightSpecSize);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSpecSize, 200);
        }
    }

ViewGroup的measure過程

  • ViewGroup是一個抽象類,沒有重寫onMeasure,提供了一個measurechild的方法(先拿到子View的LayoutParams,根據LayoutParams確定其MeasureSpec,接着將MeasureSpec傳遞給子view進行測量)
    因爲不同的ViewGroup的佈局特性不一樣,導致其測量細節各不相同.
protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

在某些極端情況下,系統可能需要多次measure才能確定view的大小.
比較好的習慣是在onLayout獲取view的寬高

四種方法獲取View的寬高

  • 通過如下四種方法獲取View的寬高

    1. Activity/View#onWindowFocusChanged
    2. view.post(runnable)runnable將消息投遞到隊列的尾部
    3. ViewTreeObserver
    4. 手動調用View#measure方法;
  • 第四種方式需要根據LayoutParams分情況處理

//1. MATCH_PARENT: 無法測量
//2. 具體值 dp/px
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100,EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100,EXACTLY);
//3. wrap_content
int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,AT_MOST);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,AT_MOST);//理論上能支持的最大值來構造
view.measure(widthMeasureSpec,heightMeasureSpec);

二. layout 過程

layout用於ViewGroup確定子元素的位置.

  1. layout方法確定view本身的位置,但onLayout確定所有子元素的位置.
  2. layout中首先通過setFrame來確定l,r,t,b四個位置,接着調用onLayout.
  3. View 的onLayout是空方法,ViewGroup的onLayout是一個抽象方法,不同ViewGroup的佈局特性不一致.
  4. 自定義ViewGroup中的onLayout中會遍歷調用子view的layout過程
  5. getMeasuredWidth和getWidth只是賦值時機不同(測量寬高的賦值時機要稍微早一些),值一般相等
//在自定義view中將r增大,則導致`最終寬高`和`測量寬高`不一致
 @Override public void layout(int l, int t, int r, int b) {
    super.layout(l, t, r+100, b);
  }

三. draw 過程

background.draw (繪製背景)-> 
onDraw (繪製自己)->
dispatchDraw(draw child)(繪製children) -> 
onDrawScrollBars.(繪製裝飾)
  1. dispatchDraw用於繪製傳遞
  2. onDraw一般是一個空方法,不同的子View/子ViewGroup繪製過程是不同的.
  3. View的dispatchDraw是空方法,ViewGroup的dispatchDraw有代碼.
  4. setWillNotDraw:如果一個View不需要繪製任何內容,設置這個標記位true後,系統會進行優化.
  5. 默認情況下View沒有開啓,ViewGroup開啓了setWillNotDraw.
  6. 當明確知道一個ViewGroup需要通過onDraw來繪製內容時,需要顯示地關閉WILL_NOT_DRAW標記.

自定義View

  1. 繼承View,實現onDraw,需要支持wrap_content和padding處理
  2. 繼承ViewGroup,需要處理自己和子元素的測量和佈局過程.
  3. 繼承特定的View(TextView),不需要處理wrap_content和padding
  4. 繼承特定ViewGroup(LinearLayout),和2差不多

需要注意

  1. 支持wrap_content,否則wrap_content就和match_parent效果相同.
  2. 處理好padding,(需要在draw中處理padding)否則padding屬性是無法起作用的
  3. 直接繼承自ViewGroup 的控件需要在onMeasureonLayout中處理 paddingmargin.
  4. 不需要使用handler,因爲View有post方法
  5. 線程和動畫及時停止View#onDetachedFromWindow,否則會導致內存泄漏
  6. 嵌套滑動,需要衝突處理,參考第3章

延伸閱讀:
Android視圖繪製流程完全解析,帶你一步步深入瞭解View(二)

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