安卓 View 的工作原理

在Android的知識體系中,View扮演著很重要的角色,簡單來理解,View 是 Android 在視覺上的呈現(xiàn)。在界面上 Android 提供了一套 GUI庫,里面有很多控件,但是很多時(shí)候我們并不滿足于系統(tǒng)提供的控件,因?yàn)檫@樣就意味這應(yīng)用界面的同類化比較嚴(yán)重。那么怎么才能做出與眾不同的效果呢?答案是自定義 View,也可以叫自定義控件,通過自定義 View 我們可以實(shí)現(xiàn)各種五花八門的效果。但是自定義 View 是有一定難度的,尤其是復(fù)雜的自定義View,大部分時(shí)候我們僅僅了解基本控件的使用方法是無法做出復(fù)雜的自定義控件的。為了更好地自定義 View,還需要掌握 View 的底層工作原理,比如View的測量流程、布局流程以及繪制流程,掌握這幾個(gè)基本流程后,我們就對 View 的底層更加了解,這樣我們就可以做出一個(gè)比較完善的自定義 View。

初識 ViewRoot 和 DecorView

在正式介紹 View 的三大流程之前,我們必須先介紹一些基本概念,這樣才能更好地理解View的measure、layout和draw過程,本節(jié)主要介紹 ViewRoot 和 DecorView 的概念。

ViewRoot 對應(yīng)于 ViewRootImpl 類,它是連接Window-Manager和DecorView的紐帶,View的三大流程均是通過ViewRoot來完成的。在ActivityThread中,當(dāng)Activity對象被創(chuàng)建完畢后,會將DecorView添加到Window中,同時(shí)會創(chuàng)建 ViewRootImpl 對象,并將ViewRootImpl對象和DecorView建立關(guān)聯(lián),這個(gè)過程可參看如下源碼:

root = new ViewRootImpl(view.getContext(),display);     
root.setView(view,wparams,panelParentView);

View的繪制流程是從ViewRoot的performTraversals方法開始的,它經(jīng)過measure、layout和draw三個(gè)過程才能最終將一個(gè)View繪制出來,其中measure用來測量View的寬和高,layout用來確定View在父容器中的放置位置,而draw則負(fù)責(zé)將View繪制在屏幕上。針對performTraversals的大致流程,可用流程圖1來表示。

圖1 performTraversals的工作流程圖

如圖1 所示,performTraversals會依次調(diào)用performMea-sure、performLayout和performDraw三個(gè)方法,這三個(gè)方法分別完成頂級View的measure、layout和draw這三大流程,其中在performMeasure中會調(diào)用measure方法,在measure方法中又會調(diào)用onMeasure方法,在onMeasure方法中則會對所有的子元素進(jìn)行measure過程,這個(gè)時(shí)候measure流程就從父容器傳遞到子元素中了,這樣就完成了一次measure過程。接著子元素會重復(fù)父容器的measure過程,如此反復(fù)就完成了整個(gè)View樹的遍歷。同理,performLayout和performDraw的傳遞流程和performMeasure是類似的,唯一不同的是,performDraw的傳遞過程是在draw方法中通過dispatchDraw來實(shí)現(xiàn)的,不過這并沒有本質(zhì)區(qū)別。

measure過程決定了View的寬/高,Measure完成以后,可以通過getMeasuredWidthgetMeasuredHeight方法來獲取到View測量后的寬/高,在幾乎所有的情況下它都等同于View最終的寬/高,但是特殊情況除外,這點(diǎn)在本章后面會進(jìn)行說明。Layout過程決定了View的四個(gè)頂點(diǎn)的坐標(biāo)和實(shí)際的View的寬/高,完成以后,可以通過getTop、getBottom、getLeftgetRight來拿到View的四個(gè)頂點(diǎn)的位置,并可以通過getWidth和getHeight方法來拿到View的最終寬/高。Draw過程則決定了View的顯示,只有draw方法完成以后View的內(nèi)容才能呈現(xiàn)在屏幕上。

如圖2 所示,DecorView作為頂級View,一般情況下它內(nèi)部會包含一個(gè)豎直方向的LinearLayout,在這個(gè)LinearLayout里面有上下兩個(gè)部分(具體情況和Android版本及主題有關(guān)),上面是標(biāo)題欄,下面是內(nèi)容欄。在Activity中我們通過setCon-tentView所設(shè)置的布局文件其實(shí)就是被加到內(nèi)容欄之中的,而內(nèi)容欄的id是content,因此可以理解為Activity指定布局的方法不叫setview而叫setContentView,因?yàn)槲覀兊牟季值拇_加到了id為content的FrameLayout中。如何得到content呢?可以這樣:ViewGroup content= findViewById (R.android.id.content)。如何得到我們設(shè)置的View呢?可以這樣:content.getChil-dAt(0)。同時(shí),通過源碼我們可以知道,DecorView其實(shí)是一個(gè)FrameLayout,View層的事件都先經(jīng)過DecorView,然后才傳遞給我們的View。

圖2 頂級View:DecorView的結(jié)構(gòu)

2 理解MeasureSpec

為了更好地理解View的測量過程,我們還需要理解MeasureSpec。從名字上來看,MeasureSpec看起來像“測量規(guī)格”或者“測量說明書”,不管怎么翻譯,它看起來都好像是或多或少地決定了View的測量過程。通過源碼可以發(fā)現(xiàn),MeasureSpec的確參與了View的measure過程。讀者可能有疑問,MeasureSpec是干什么的呢?確切來說,MeasureSpec在很大程度上決定了一個(gè)View的尺寸規(guī)格,之所以說是很大程度上是因?yàn)?strong>這個(gè)過程還受父容器的影響,因?yàn)楦溉萜饔绊慥iew的MeasureSpec的創(chuàng)建過程。在測量過程中,系統(tǒng)會將View的LayoutParams根據(jù)父容器所施加的規(guī)則轉(zhuǎn)換成對應(yīng)的MeasureSpec,然后再根據(jù)這個(gè)measureSpec來測量出View的寬/高。上面提到過,這里的寬/高是測量寬/高,不一定等于View的最終寬/高。Mea-sureSpec看起來有點(diǎn)復(fù)雜,其實(shí)它的實(shí)現(xiàn)是很簡單的,下面會詳細(xì)地分析MeasureSpec。

2.1 MeasureSpec

MeasureSpec代表一個(gè)32位int值,高2位代表Spec-Mode,低30位代表SpecSize,SpecMode是指測量模式,而SpecSize是指在某種測量模式下的規(guī)格大小。下面先看一下MeasureSpec內(nèi)部的一些常量的定義,通過下面的代碼,應(yīng)該不難理解MeasureSpec的工作原理:

public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

        /** @hide */
        @IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
        @Retention(RetentionPolicy.SOURCE)
        public @interface MeasureSpecMode {}

        /**
         * Measure specification mode: The parent has not imposed any constraint
         * on the child. It can be whatever size it wants.
         */
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;

        /**
         * Measure specification mode: The parent has determined an exact size
         * for the child. The child is going to be given those bounds regardless
         * of how big it wants to be.
         */
        public static final int EXACTLY     = 1 << MODE_SHIFT;

        /**
         * Measure specification mode: The child can be as large as it wants up
         * to the specified size.
         */
        public static final int AT_MOST     = 2 << MODE_SHIFT;


        /**
         * Extracts the mode from the supplied measure specification.
         *
         * @param measureSpec the measure specification to extract the mode from
         * @return {@link android.view.View.MeasureSpec#UNSPECIFIED},
         *         {@link android.view.View.MeasureSpec#AT_MOST} or
         *         {@link android.view.View.MeasureSpec#EXACTLY}
         */
        @MeasureSpecMode
        public static int getMode(int measureSpec) {
            //noinspection ResourceType
            return (measureSpec & MODE_MASK);
        }

        /**
         * Extracts the size from the supplied measure specification.
         *
         * @param measureSpec the measure specification to extract the size from
         * @return the size in pixels defined in the supplied measure specification
         */
        public static int getSize(int measureSpec) {
            return (measureSpec & ~MODE_MASK);
        }
      ...
      ...
}

MeasureSpec通過將SpecMode和SpecSize打包成一個(gè)int值來避免過多的對象內(nèi)存分配,為了方便操作,其提供了打包和解包方法。SpecMode和SpecSize也是一個(gè)int值,一組Spec-Mode和SpecSize可以打包為一個(gè)MeasureSpec,而一個(gè)Mea-sureSpec可以通過解包的形式來得出其原始的SpecMode和SpecSize,需要注意的是這里提到的MeasureSpec是指Mea-sureSpec所代表的int值,而并非MeasureSpec本身。

SpecMode有三類,每一類都表示特殊的含義,如下所示。

  • UNSPECIFIED 父容器不對View有任何限制,要多大給多大,這種情況一般用于系統(tǒng)內(nèi)部,表示一種測量的狀態(tài)。
  • EXACTLY 父容器已經(jīng)檢測出View所需要的精確大小,這個(gè)時(shí)候View的最終大小就是SpecSize所指定的值。它對應(yīng)于LayoutParams中的match_parent和具體的數(shù)值這兩種模式。
  • AT_MOST 父容器指定了一個(gè)可用大小即SpecSize,View的大小不能大于這個(gè)值,具體是什么值要看不同View的具體實(shí)現(xiàn)。它對應(yīng)于LayoutParams中的wrap_content。

2.2 MeasureSpec和LayoutParams的對應(yīng)關(guān)系

上面提到,系統(tǒng)內(nèi)部是通過MeasureSpec來進(jìn)行View的測量,但是正常情況下我們使用View指定MeasureSpec,盡管如此,但是我們可以給View設(shè)置LayoutParams。在View測量的時(shí)候,系統(tǒng)會將LayoutParams在父容器的約束下轉(zhuǎn)換成對應(yīng)的MeasureSpec,然后再根據(jù)這個(gè)MeasureSpec來確定View測量后的寬/高。需要注意的是,MeasureSpec不是唯一由LayoutParams決定的,LayoutParams需要和父容器一起才能決定View的MeasureSpec,從而進(jìn)一步?jīng)Q定View的寬/高。另外,對于頂級View(即DecorView)和普通View來說,MeasureSpec的轉(zhuǎn)換過程略有不同。對于DecorView,其MeasureSpec由窗口的尺寸和其自身的LayoutParams來共同確定;對于普通View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams來共同決定,MeasureSpec一旦確定后,onMeasure中就可以確定View的測量寬/高。

對于DecorView來說,在ViewRootImpl中的measureHierarchy方法中有如下一段代碼,它展示了DecorView的MeasureSpec的創(chuàng)建過程,其中desiredWindowWidth和de-sired-WindowHeight是屏幕的尺寸:

childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);//desireWindowWidth是屏幕的寬度
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

上面代碼調(diào)用的getRootMeasureSpec()方法的代碼如下

private static int getRootMeasureSpec(int windowSize, int rootDimension) {
      int measureSpec;
      switch (rootDimension){
      case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can't resize. Force root view to be windowSize.
            mesureSpec = MeasureSpec.makeMeasureSpec(windowSize,MeasureSpec.EXACTLY);
            break;
      case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize,MeasureSpec.AT_MOST);
            break;
      default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension,MeasureSpec.EXACTLY);
            break;
      }
      return measureSpec;
}

通過上述代碼,DecorView的MeasureSpec的產(chǎn)生過程就很明確了,具體來說其遵守如下規(guī)則,根據(jù)它的LayoutParams中的寬/高的參數(shù)來劃分。

  • LayoutParams.MATCH_PARENT:精確模式,大小就是窗口的大小;
  • LayoutParams.WRAP_CONTENT:最大模式,大小不定,但是不能超過窗口的大??;
  • 固定大?。ū热?00dp):精確模式,大小為LayoutParams中指定的大小。

對于普通View來說,這里是指我們布局中的View,View的measure過程由ViewGroup傳遞而來,先看一下ViewGroup的measureChildWithMargins方法:

    /**
     * Ask one of the children of this view to measure itself, taking into
     * account both the MeasureSpec requirements for this view and its padding
     * and margins. The child must have MarginLayoutParams The heavy lifting is
     * done in getChildMeasureSpec.
     *
     * @param child The child to measure
     * @param parentWidthMeasureSpec The width requirements for this view
     * @param widthUsed Extra space that has been used up by the parent
     *        horizontally (possibly by other children of the parent)
     * @param parentHeightMeasureSpec The height requirements for this view
     * @param heightUsed Extra space that has been used up by the parent
     *        vertically (possibly by other children of the parent)
     */
    protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

上述方法會對子元素進(jìn)行measure,在調(diào)用子元素的measure方法之前會先通過getChildMeasureSpec方法來得到子元素的MeasureSpec。從代碼來看,很顯然,子元素的MeasureSpec的創(chuàng)建與父容器的MeasureSpec和子元素本身的LayoutParams有關(guān),此外還和View的margin及padding有關(guān),具體情況可以看一下ViewGroup的getChildMeasureSpec方法,清楚展示了普通View的MeasureSpec的創(chuàng)建規(guī)則如下所示。

    /**
     * Does the hard part of measureChildren: figuring out the MeasureSpec to
     * pass to a particular child. This method figures out the right MeasureSpec
     * for one dimension (height or width) of one child view.
     *
     * The goal is to combine information from our MeasureSpec with the
     * LayoutParams of the child to get the best possible results. For example,
     * if the this view knows its size (because its MeasureSpec has a mode of
     * EXACTLY), and the child has indicated in its LayoutParams that it wants
     * to be the same size as the parent, the parent should ask the child to
     * layout given an exact size.
     *
     * @param spec The requirements for this view
     * @param padding The padding of this view for the current dimension and
     *        margins, if applicable  (父容器中已占用的空間大小)
     * @param childDimension How big the child wants to be in the current
     *        dimension
     * @return a MeasureSpec integer for the child
     */
    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        //子元素可用的大小為父容器的尺寸減去padding
        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);
    }
表1 普通View的MeasureSpec的創(chuàng)建規(guī)則

這里再做一下說明。前面已經(jīng)提到,對于普通View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams來共同決定,那么針對不同的父容器和View本身不同的LayoutParams,View就可以有多種MeasureSpec。這里簡單說一下,當(dāng)View采用固定寬/高的時(shí)候,不管父容器的MeasureSpec是什么,View的MeasureSpec都是精確模式并且其大小遵循Layoutparams中的大小。當(dāng)View的寬/高是match_parent時(shí),如果父容器的模式是精準(zhǔn)模式,那么View也是精準(zhǔn)模式并且其大小是父容器的剩余空間;如果父容器是最大模式,那么View也是最大模式并且其大小不會超過父容器的剩余空間。當(dāng)View的寬/高是wrap_content時(shí),不管父容器的模式是精準(zhǔn)還是最大化,View的模式總是最大化并且大小不能超過父容器的剩余空間??赡茏x者會發(fā)現(xiàn),在我們的分析中漏掉了UNSPECIFIED模式,那是因?yàn)檫@個(gè)模式主要用于系統(tǒng)內(nèi)部多次Measure的情形,一般來說,我們不需要關(guān)注此模式。


參考書目

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,363評論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,497評論 3 416
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,305評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,962評論 1 311
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,727評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,193評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,257評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,411評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,945評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,777評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,978評論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,519評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,216評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,657評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,960評論 2 373

推薦閱讀更多精彩內(nèi)容