View的工作原理(一)測量過程

在Android體系中View作為視覺上的呈現(xiàn),扮演著非常重要的角色。盡管Android提供了一套包含很多控件的GUI庫。但是在大多數情況下,因為交互或展現(xiàn)的定制化要求,我們不是不能直接拿來使用的。怎么解決這一問題呢?

為了防止Android應用界面的同類化嚴重,我們需要通過自定義View來實現(xiàn)更友好的用戶體驗和更美觀的呈現(xiàn)效果。

前言

為了更好的自定義View,我們應該掌握View的基本流程,如:View的測量流程、布局流程及繪制流程;

除了三大流程之外,View常見的回調方法也是需要熟練掌握的。

  1. 構造方法:
    基本屬性和自定義屬性的初始化和賦值;
  2. onAttach
  3. onVisibilityChanged
  4. onDetach

View的工作原理

ViewRoot對應于ViewRootImpl類,它是連接WindowManger和DecorView的紐帶,View的三大流程均是通過ViewRoot來完成的。

在ActivityThread中,當Activity對象創(chuàng)建完畢后,會將DecorView添加到Window中,同時創(chuàng)建ViewRootImpl對象,并將ViewRootImpl對象和DecorView建立關聯(lián)。參看下面源碼(位于android-25/android/app/ActivityThread.java
):

                //該代碼位于handleLaunchActivity->handleResumeActivity方法中
                ActivityClientRecord r = mActivities.get(token);
                ...
                r.window = r.activity.getWindow();
                View decor = r.window.getDecorView();
                decor.setVisibility(View.INVISIBLE);
                ViewManager wm = a.getWindowManager();
                WindowManager.LayoutParams l = r.window.getAttributes();
                a.mDecor = decor;
                l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
                l.softInputMode |= forwardBit;
                if (r.mPreserveWindow) {
                    a.mWindowAdded = true;
                    r.mPreserveWindow = false;
                    // Normally the ViewRoot sets up callbacks with the Activity
                    // in addView->ViewRootImpl#setView. If we are instead reusing
                    // the decor view we have to notify the view root that the
                    // callbacks may have changed.
                    ViewRootImpl impl = decor.getViewRootImpl();
                    if (impl != null) {
                        impl.notifyChildRebuilt();
                    }
                }

View的繪制流程是從ViewRoot的performTraversals方法開始的,經過measure、layout和draw三個過程才最終將一個View繪制出來。其中:

  1. measure:用來測量View的寬和高;
  2. layout:用來View在父容器放置的位置;
  3. draw:負責將View繪制在屏幕上;
performTraversals 的工作流程圖

如圖所示,performTraversals會依次調用performMeasure、performLayout和performDraw三個方法,這三個方法分別完成頂級View的measure、layout和draw這三大流程;其中performMeasure中會調用measure方法,measure方法又會調用onMeasure方法,在onMeasure方法則會對所有的子View進行measure過程,這時measure流程就從父容器傳遞到子元素中去。然后子元素重復父容器的measure過程,如此反復就完成了整個View樹的遍歷,這樣就完成了依次measure過程。另外兩個過程是類似的,不贅述。

Measure過程

measure過程決定了View的寬高,measure完成以后可以通過getMeasureWidth和getMeasureHeight方法來獲取View測量后的寬高,幾乎所有情況下這兩個值都等同于View的最終寬高值,特殊情況下除外(onLayout時強制指定寬高)。

Layout過程

layout過程決定了View的四個頂點的坐標和實際的View寬高。方法完成后,可以通過getTop、getBottom、getLeft、getRight來拿到View四個頂點的位置,并可以通過getWidth、getHeight方法來拿到View最終寬高。

Draw過程

draw過程決定了View的顯示內容,只有draw方法完成之后View的內容才能顯示在屏幕上。

DecorView層級組成

DecorView作為頂級的View,一般情況下它內部會包含一個豎直方向的LinearLayout,這個LinearLayout里面有上下兩部分(具體情況和系統(tǒng)版本及主題有關),上面是標題欄,下面是內容區(qū)。

Activity通過setContentView所設置的布局其實是添加到DecorView的內容區(qū)里面了。而內容區(qū)的id是content,因此可以理解制定布局的方式不叫setView而叫setContentView;確切的說,設置的布局是加在id為content的FrameLayout的布局中。

如圖:

頂級View: DecorView 的 結構圖

理解MeasureSpec

MeasureSpec通過將高2位的SpecMode和低30位的SpecSize打包成一個32位的int里面來避免過多的內存分配。為了方便操作,其同時提供了打包和解包方法。

SpecMode

  • UNSPECIFIED(未指明模式)
    父容器不對View做任何限制,要多大給多大,這種情況下一般用于系統(tǒng)內部,表示一種測量的狀態(tài)。

  • EXACTLY(準確模式)
    父容器已經檢測出View所需要的精確大小,這時View的最終大小就是低30位的SpecSize指定的值。這種情況對應于LayoutParams中的match_parent和具體的數值這兩種模式。

  • AT_MOST(最大模式)
    父容器指定了一個可用大小即就是低30位的SpecSize的值,最終View的值不能大于這個值。它對應于LayoutParmas中的wrap_content。

MeasureSpec和LayoutParams的對應關系

Android系統(tǒng)內部是通過MeasureSpec來進行View測量的,但正常情況下我們使用View指定MeasureSpec;同時我們也可以給View設置LayoutParams。在View的測量時,系統(tǒng)會將LayoutParams在父容器的約束下轉換成對應的MeasureSpec,然后在根據MeasureSpec來確定View測量后的寬高。

  • MeasureSpec不是僅由LayoutParams決定的,其需要和父容器一起才能決定View的MeasureSpec,從而進一步決定View的寬高。
  • 頂級View(DecorView)的MeasureSepc由窗口的尺寸和其自身的LayoutParams來共同決定。
  • 對于普通View,它的MeasureSpec由父容器的MeasureSpec和其自身的LayoutParams來共同決定。
  • MeasureSpec一旦確定onMeasure中就可以確定View的測量寬、高。

DecorView中對應關系

  1. LayoutParams.MATCH_PARENT:精確模式大小就是窗口的大小。
  2. LayoutParams.WRAP_CONTENT:最大模式,大小不確定,但不能超過窗口的大小
  3. 固定模式(如,100dp):精確模式,大小為LayoutParams指定的大小。

普通View中的對應關系

  1. 當View采用固定模式寬高的時候,不管父容器的MeasureSpec是什么,View的MeasureSpec是精確模式且其大小為當前View要求的大小。
  2. 當View寬高采用match_parent時,那么View的MeasureSpec是精確模式且其大小為父容器剩余的空間大??;如果父容器為最大模式,那么View的MeasureSpec是最大模式且其大小不超過父容器剩余的空間大小。
  3. 當View的寬高采用wrap_content時,不論父容器的模式是精確模式還是最大模式,View的MeasureSpec是最大模式且其大小不超過父容器剩余的空間大小。
普通 View 的 MeasureSpec的創(chuàng)建規(guī)則
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

View的工作流程

View的工作流程主要是指measure(測量)、layout(布局)、draw(繪制)這三大流程。
其中measure確定了View的測量寬高,layout確定了View的最終寬高和四個頂點的位置,draw則將View繪制到屏幕上。

measure過程

measure過程要分兩種情況來分析。如果是原始的View,那么通過measure方法就完成了View的測量過程;如果是一個ViewGroup除了完成自己的測量過程外,還要遍歷調用所有子元素的measure方法,各個子元素再遞歸的執(zhí)行這個流程。

原始View的measure過程

View的measure過程使用measure方法完成的,measure方法是一個final修飾的方法,意味著子類不能重寫此方法,在measure方法中會調用其onMeasure方法。onMeasure方法源碼如下:

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
    
    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;
    }

onMeasure方法很簡潔,setMeasuredDimension方法會設置View的寬高的測量值。我們同時需要關注getDefaultSize這個方法,這個方法邏輯很簡單在AT_MOST和EXACTLY這兩種情形下,getDefaultSize大小就是MeasureSpec的specSize的值。

而在UNSPECIFIED(位指明模式)的情形下,一般用于系統(tǒng)內部的測量過程,getSuggestedMinimumWidth、getSuggestedMinimumHeight這兩個方法的返回值。源碼如下:

   protected int getSuggestedMinimumHeight() {
        return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
   }

   protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
   }

這種情形下,可以得出如下結論:
如果View沒有設置背景,那么返回android:minWidth這個屬性所指定的值,這個值可以為0;如果View設置了背景,則返回minWidth和背景的最小寬度、高度之間的最大值。

結論

直接繼承View的自定義空間需要重寫onMeasure方法,并設置wrap_content時的自身大小,否則在布局中使用wrap_content就相當于使用了match_parent。

View在布局中使用wrap_content,那么他的specMode為AT_MOST,這種模式下寬高等于specSize,根據之前的表格可以得知,specSize就是parentSize即父容器當前剩余的空間大小;很顯然這種效果等同于布局中使用match_parent。

如何解決這個問題?很簡單,只需要給View指定一個默認的內部寬高,并在wrap_content是設置此寬高即可。代碼如下:

    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);
        heightMeasureSpec);
        if (widthSpecMode == MeasureSpec.AT_ MOST && heightSpecMode == MeasureSpec.AT_ MOST){
            setMeasuredDimension(mWidth, mHeight);
        } else if (widthSpecMode == MeasureSpec.AT_ MOST){
            setMeasuredDimension(mWidth, heightSpecSize);
        } else if (heightSpecMode == MeasureSpec.AT_ MOST){
            setMeasuredDimension(widthSpecSize, mHeight);
        }
    }

查看TextView、ImageView等的源碼可以知道,針對wrap_content的情形,在onMeasure方法中都做了特殊處理,可以自行閱讀查看。

ViewGroup的measure過程

和View不同的是ViewGroup是一個抽象類,因此它沒有重寫onMeasure方法,而是提供了一個叫做measureChildren的方法,如下所示。

    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

    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);
    }

上述代碼可以看出ViewGroup在measure時,會對每一個子元素進行measure,measureChild方法的實現(xiàn)就是取出子元素的LayoutParams,然后再通過getChildMeasureSpec來獲取子元素的MeasureSpec,接著講MeasureSpec傳遞給依次調用子元素的measure方法進行測量。

總結

ViewGroup作為一個抽象類并沒有定義其測量的具體過程,代表測量過程的onMeasure方法需要各個子類進行具體的實現(xiàn),比如LinearLayout、RelativeLayout等。ViewGroup的實現(xiàn)類有不同的布局特性,導致系統(tǒng)無法統(tǒng)一實現(xiàn),因此才決定交給擴展類區(qū)實現(xiàn)。


View的測量過程是三大流程中最復雜的一個,measure完成以后,通過getMeasureWidth/Height方法就可以正確獲取到View的測量寬、高。需要注意的是,極端的情況下,系統(tǒng)可能需要多次調用measure才能最終確定測量寬、高,這種情形下onMeasure拿到的測量寬高可能并不準確。一個比較好的習慣是在onLayout方法中獲取View的測量寬高,或者最終寬高。

參考一:《安卓開發(fā)藝術探究》實體書

參考二:《安卓開發(fā)藝術探究》Kindle電紙書

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容