自定義 view - 測(cè)量 onMeasure

自定義 view 的3個(gè)核心方法

  • onMeasure
    根據(jù) view 的測(cè)量模式計(jì)算確定 view 的寬高
  • onLayout
    ViewGroup 中對(duì)所有的子 view 排版,決定子 view 的位置
  • onDraw
    具體繪制 view

本節(jié)我們來(lái)說(shuō)說(shuō) onMeasure ,view 的寬高的大小如何決定


自定義 View 繪制流程

005Xtdi2jw1f638wreu74j30fc0heaay.jpg

看完上面的圖,那么今天我們呢就來(lái)說(shuō)說(shuō) view 的測(cè)量 onMeasure


onMeasure 的經(jīng)典寫(xiě)法

在自定義 view 的 onMeasure 測(cè)量方法中,所有的資料都是建議下面這種經(jīng)典寫(xiě)法

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    // 獲取寬的測(cè)量模式
    int wSpecMode = MeasureSpec.getMode(widthMeasureSpec); 
    // 獲取符控件提供的 view 寬的最大值
    int wSpecSize = MeasureSpec.getSize(widthMeasureSpec);

    int hSpecMode = MeasureSpec.getMode(heightMeasureSpec);
    int hSpecSize = MeasureSpec.getSize(heightMeasureSpec);

    if (wSpecMode == MeasureSpec.AT_MOST && hSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(300, 300);
    } else if (wSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(300, hSpecSize);
    } else if (hSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(wSpecSize, 300);
    }
}

先不要問(wèn)為什么,先熟悉下代碼,一會(huì)會(huì)有用到


view 的測(cè)量模式

view 的測(cè)量涉及到一個(gè)重要的點(diǎn),測(cè)量模式,view 有3個(gè)測(cè)量模式,看下圖:


944365-e631b96ea1906e34.png
  • USPENCIFIED
    是不限制子 view 大小的,我們?cè)谧远x view 時(shí)用不到,一般也不用處理

  • EXACTLY :精準(zhǔn)模式
    當(dāng) view 的寬高設(shè)置為 MATCH_PARENT 或者固定大小時(shí),view 的測(cè)量就是 EXACTLY 類(lèi)型的。

  • AT_MOST :最大值模式
    當(dāng) view 的寬高設(shè)置為 WARP_CONTENT 時(shí),ew 的測(cè)量就是 AT_MOST 類(lèi)型的。這是我們需要返回給系統(tǒng)一個(gè)值,已通知系統(tǒng)這個(gè) view 的寬高應(yīng)該是多少,若是我們不做處理,那么就會(huì)按 EXACTLY 測(cè)量,最大值不會(huì)查過(guò)父控件的寬高

清楚了這3個(gè)模式,尤其是 EXACTLY 和 EXACTLY 代表什么意思之后,我們要來(lái)說(shuō)一說(shuō)這個(gè)測(cè)量模式了。

測(cè)量模式對(duì)應(yīng)的 matchParent,warp_content,具體寬高數(shù)值,都是寫(xiě)在 android:layout_width,android:layout_height xml 屬性里面的,注意這都是 layout 的 xml 標(biāo)簽, layout 的 xml 標(biāo)簽最終都會(huì)把數(shù)據(jù)寫(xiě)入到 LayoutParams 里面,LayoutParams 是給 view 的父控件 ViewGorup 用的,父控件解析所有子 view 的 LayoutParams 參數(shù),然后把這些參數(shù)經(jīng)過(guò)處理包裝到 widthMeasureSpec,heightMeasureSpec 里面?zhèn)鬟f給自子 view 的,自定義 view onMeasure 方法里面的參數(shù)就是這么來(lái)的。

這就帶出一個(gè)問(wèn)題,一個(gè) view 的寬高不僅僅是自己決定的,也是父控件決定的。一個(gè) view 先估算自己的寬高,然后告知父控件,父控件再最終決定view 的寬高是多少。這里面起決定作用的還是 view 自己的估算,父控件只是做一個(gè)最終的上限復(fù)核,子 view 的大小不嗯呢乖超過(guò)父控件的,這下大家都懂了吧


view 自己估算自家的寬高

還是上面哪段經(jīng)典代碼,我們放出來(lái),方便觀看

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    // 獲取寬的測(cè)量模式
    int wSpecMode = MeasureSpec.getMode(widthMeasureSpec); 
    // 獲取符控件提供的 view 寬的最大值
    int wSpecSize = MeasureSpec.getSize(widthMeasureSpec);

    int hSpecMode = MeasureSpec.getMode(heightMeasureSpec);
    int hSpecSize = MeasureSpec.getSize(heightMeasureSpec);

    if (wSpecMode == MeasureSpec.AT_MOST && hSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(300, 300);
    } else if (wSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(300, hSpecSize);
    } else if (hSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(wSpecSize, 300);
    }
}

在上面的經(jīng)典代碼中,我們其實(shí)干了幾件很簡(jiǎn)單的事:

  1. 拿到 view 寬高的測(cè)量模式和父控件允許 view 寬高的最大值

  2. 根據(jù) view 寬高不同的測(cè)量模式區(qū)分對(duì)待

  3. 當(dāng)寬和高是 EXACTLY 時(shí),表示 寬高已經(jīng)設(shè)定了一個(gè)固定值,match_parent 其實(shí)也是表示6個(gè)固定值,就是用父控件允許 view 寬高的最大值(符空間在寬高方向上剩余的最大值),就是一個(gè)具體的,父控件返回給我們什么值,我們用什么值就行了。當(dāng)我們?cè)?xml 中給寬高設(shè)置一個(gè)具體的值時(shí),比如 35dp,那么父控件就會(huì)把 35dp 返回給我們,而不是 match_parent 時(shí)返回給我們?cè)试S的最大值,這點(diǎn)區(qū)別注意下

  4. onMeasure 里面最值得我們費(fèi)腦子的就是 AT_MOST 了,當(dāng) AT_MOST 出現(xiàn)時(shí),就表示我們給寬高使用了 warp_content,這個(gè)時(shí)候父控件是不知道子 view 寬高應(yīng)該是多少的,就需要子 view 明確聲明自己的寬高是多少了。一個(gè)典型的例子,textview 就是根據(jù)文字矩陣的寬高來(lái)計(jì)算具體寬高的值的,但是自定義 view 的寬和高同時(shí)是 warp_content 是不多的,多數(shù)時(shí)都是寬是 match_parent 的,高是 warp_content 的,我們?cè)谠O(shè)計(jì)一個(gè)自定義 view 時(shí),自定義 view 圖案的寬高總是成比例的,這樣我們就可以根據(jù)寬高一方的具體值按比例計(jì)算出寬高中的另一個(gè)值。但是我們碰到了寬高都是 warp_content 時(shí)呢,我們根據(jù)寬高的比例和父控件寬高允許的最大值計(jì)算出 子 view 不出父控件時(shí)成比例的寬高值。warp_content 時(shí)我們計(jì)算寬高值的基礎(chǔ)就是 自定義 view 的圖案必須成比例,要不誰(shuí)知道這個(gè) view 應(yīng)該有多大呢。

  5. 最后用 setMeasuredDimension 通知父控件子 view 的大小。

基本上自定義 view 計(jì)算自己的寬高就是這么搞的,說(shuō)的簡(jiǎn)單,但是碰到 warp_content 時(shí)計(jì)算真的不是很輕松。


Margin 和 padding 的問(wèn)題

這個(gè)我們根據(jù)需要處理

  • Margin
    外邊局的處理很簡(jiǎn)單,Margin 是會(huì)寫(xiě)到 LayoutParams 里面的,在邏輯上都是交給父控件 ViewGroup 在 onLayout 方法中處理的

  • padding
    內(nèi)邊距就得我們自己在測(cè)量中處理了。我們使用這幾個(gè) api 就能在 view 中拿到 padding 的大小,getPaddingLeft() 、getPaddingRight 、getPaddingTop() 、getPaddingBottom(),然后把的 padding 數(shù)值加到 view 的寬高里面去,不難處理,詳細(xì)處理可以看篇文章:


onSizeChange

onSizeChange 方法很好理解,在 view 大小改變時(shí)會(huì)調(diào)用,我們來(lái)看看這個(gè)方法

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
}

四個(gè)參數(shù),分別為 寬度,高度,上一次寬度,上一次高度,我們只需關(guān)注 寬度(w), 高度(h) 即可,這兩個(gè)參數(shù)才是 View 的最終大小

onSizeChange 可能會(huì)多次觸發(fā),view 會(huì)緩存上一次的大小,在 view 的大小改變時(shí)就會(huì)觸發(fā)這個(gè)回調(diào)了。觸發(fā) onSizeChange 回調(diào)的方法挺多,比如 settop ,setleft 這類(lèi)改變 view 的大小方法, addView,removeview 也會(huì)觸發(fā) onSizeChange


父控件 ViewGroup 對(duì)子 view 測(cè)量的影響

ViewGroup相當(dāng)于一個(gè)放置View的容器,并且我們?cè)趯?xiě)布局xml的時(shí)候,會(huì)告訴容器(凡是以layout為開(kāi)頭的屬性,都是為用于告訴容器的),我們的寬度(layout_width)、高度(layout_height)、對(duì)齊方式(layout_gravity)等;當(dāng)然還有margin等;于是乎,ViewGroup的職能為:給childView計(jì)算出建議的寬和高和測(cè)量模式 ;決定childView的位置;為什么只是建議的寬和高,而不是直接確定呢,別忘了childView寬和高可以設(shè)置為wrap_content,這樣只有childView才能計(jì)算出自己的寬和高。

然后 view 根據(jù)測(cè)量模式和ViewGroup給出的建議的寬和高,計(jì)算出自己的寬和高;同時(shí)還有個(gè)更重要的職責(zé)是:在ViewGroup為其指定的區(qū)域內(nèi)繪制自己的形態(tài)。

這里面最重要的點(diǎn)是 view 的測(cè)量模式不是自己解析出來(lái)的,是父控件 ViewGroup 傳遞給子 view 的,父控件 ViewGroup 傳遞給子 view 之前,是經(jīng)過(guò)處理的,這個(gè)處理我們需要清楚,不難,實(shí)際頁(yè)沒(méi)啥用,但是我們得知道。另外這一段是我摘抄過(guò)來(lái)的,看著有不通順的地方大家腦補(bǔ)一下就都能理解了。

ViewGroup的測(cè)量過(guò)程主要用到了三個(gè)方法:

  • measureChildren()
    遍歷所有的childView
  • getChildMeasureSpec()
    確定測(cè)量規(guī)格
  • measureChild()調(diào)用測(cè)量規(guī)格

measureChildren() 調(diào)用了 measureChild() ,measureChild() 又調(diào)用了 getChildMeasureSpec(),getChildMeasureSpec() 是核心,需要看一下的,看過(guò)我們就可以知道 view 的測(cè)量模式不僅收自己影響,還受到父控件影響

/**
  * 源碼分析:getChildMeasureSpec()
  * 作用:根據(jù)父視圖的MeasureSpec & 布局參數(shù)LayoutParams,計(jì)算單個(gè)子View的MeasureSpec
  * 注:子view的大小由父view的MeasureSpec值 和 子view的LayoutParams屬性 共同決定
  **/

    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {  

         //參數(shù)說(shuō)明
         * @param spec 父view的詳細(xì)測(cè)量值(MeasureSpec) 
         * @param padding view當(dāng)前尺寸的的內(nèi)邊距和外邊距(padding,margin) 
         * @param childDimension 子視圖的布局參數(shù)(寬/高)

            //父view的測(cè)量模式
            int specMode = MeasureSpec.getMode(spec);     

            //父view的大小
            int specSize = MeasureSpec.getSize(spec);     
          
            //通過(guò)父view計(jì)算出的子view = 父大小-邊距(父要求的大小,但子view不一定用這個(gè)值)   
            int size = Math.max(0, specSize - padding);  
          
            //子view想要的實(shí)際大小和模式(需要計(jì)算)  
            int resultSize = 0;  
            int resultMode = 0;  
          
            //通過(guò)父view的MeasureSpec和子view的LayoutParams確定子view的大小  


            // 當(dāng)父view的模式為EXACITY時(shí),父view強(qiáng)加給子view確切的值
           //一般是父view設(shè)置為match_parent或者固定值的ViewGroup 
            switch (specMode) {  
            case MeasureSpec.EXACTLY:  
                // 當(dāng)子view的LayoutParams>0,即有確切的值  
                if (childDimension >= 0) {  
                    //子view大小為子自身所賦的值,模式大小為EXACTLY  
                    resultSize = childDimension;  
                    resultMode = MeasureSpec.EXACTLY;  

                // 當(dāng)子view的LayoutParams為MATCH_PARENT時(shí)(-1)  
                } else if (childDimension == LayoutParams.MATCH_PARENT) {  
                    //子view大小為父view大小,模式為EXACTLY  
                    resultSize = size;  
                    resultMode = MeasureSpec.EXACTLY;  

                // 當(dāng)子view的LayoutParams為WRAP_CONTENT時(shí)(-2)      
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {  
                    //子view決定自己的大小,但最大不能超過(guò)父view,模式為AT_MOST  
                    resultSize = size;  
                    resultMode = MeasureSpec.AT_MOST;  
                }  
                break;  
          
            // 當(dāng)父view的模式為AT_MOST時(shí),父view強(qiáng)加給子view一個(gè)最大的值。(一般是父view設(shè)置為wrap_content)  
            case MeasureSpec.AT_MOST:  
                // 道理同上  
                if (childDimension >= 0) {  
                    resultSize = childDimension;  
                    resultMode = MeasureSpec.EXACTLY;  
                } else if (childDimension == LayoutParams.MATCH_PARENT) {  
                    resultSize = size;  
                    resultMode = MeasureSpec.AT_MOST;  
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {  
                    resultSize = size;  
                    resultMode = MeasureSpec.AT_MOST;  
                }  
                break;  
          
            // 當(dāng)父view的模式為UNSPECIFIED時(shí),父容器不對(duì)view有任何限制,要多大給多大
            // 多見(jiàn)于ListView、GridView  
            case MeasureSpec.UNSPECIFIED:  
                if (childDimension >= 0) {  
                    // 子view大小為子自身所賦的值  
                    resultSize = childDimension;  
                    resultMode = MeasureSpec.EXACTLY;  
                } else if (childDimension == LayoutParams.MATCH_PARENT) {  
                    // 因?yàn)楦竩iew為UNSPECIFIED,所以MATCH_PARENT的話(huà)子類(lèi)大小為0  
                    resultSize = 0;  
                    resultMode = MeasureSpec.UNSPECIFIED;  
                } else if (childDimension == LayoutParams.WRAP_CONTENT) {  
                    // 因?yàn)楦竩iew為UNSPECIFIED,所以WRAP_CONTENT的話(huà)子類(lèi)大小為0  
                    resultSize = 0;  
                    resultMode = MeasureSpec.UNSPECIFIED;  
                }  
                break;  
            }  
            return MeasureSpec.makeMeasureSpec(resultSize, resultMode);  
        }

xml 中所有 layout 的屬性都不是給 view 看的,是給 view 的父控件 ViewGorup 看的,layout 的參數(shù)封裝在 LayoutParams 里面。ViewGorup 父控件遍歷所有的子 view 的測(cè)量模式,然后結(jié)合自身測(cè)量模式,決定子 view 的測(cè)量模式及傳給子 view 的寬高建議值

在上面代碼中我們可以看到:

  • 當(dāng)父控件的測(cè)量模式是 EXACTLY 精確值時(shí),子 view 在 layout 中設(shè)置的是什么測(cè)量模式就是什么測(cè)量模式。
  • 當(dāng)父控件的測(cè)量模式是 AT_MOST 包括內(nèi)容時(shí),即便子 view 在 layout 中設(shè)置的是 MATCH_PARENT ,父控件也會(huì)把子 view 的測(cè)量模式設(shè)置為 AT_MOST
  • 父控件返回給子 view 寬高的建議值時(shí),除了子 view 在 layout 中聲明了一個(gè)確切的數(shù)時(shí),返回這給子 view 這個(gè)確切的數(shù),其實(shí)都是返回的父控件最大的寬高值

父控件寬高是 AT_MOST 時(shí),父控件也不知道自己的寬高應(yīng)該是多少,這時(shí)他要依賴(lài)子 view 的大小才能確定自己的寬高,所以會(huì)給子 view 設(shè)置成 AT_MOST 的測(cè)量模式,就是希望子 view 明確zi view 自己具體的大小,以便父控件計(jì)算自己的大小。所以我們?cè)?自定義 view 碰到 AT_MOST 時(shí),就當(dāng) warp_content 處理就行了,經(jīng)典的 onMeasure 方法是久經(jīng)考驗(yàn)的

onMearsu 還有更多詳細(xì)的東西,比如源碼分析,搭建看我下面提供的鏈接吧,如果你有興趣額的話(huà):


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

友情鏈接更多精彩內(nèi)容