1. 程式人生 > >Android自定義控制元件之測量onMeasure

Android自定義控制元件之測量onMeasure

由上圖可知,語法角度:子類可以重寫onMeasure,只能繼承View的measure,setMeasuredDimension方法。測量流程分為兩種情況討論:容器控制元件ViewGroup,原始的View(非容器控制元件)。原始的View測量,只需要測量自己的寬高;而容器控制元件需要先測量所有的子View的寬高,然後再測量自己的寬高。看懂本篇文章,還需要大家自己先去研究下類View$MeasureSpec,相對比較簡單,本文不描述MeasureSpec相關知識。

二,原始碼分析之View

先分析原始的View,開啟View.java檔案,檢視measure方法:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int oWidth  = insets.left + insets.right;
            int oHeight = insets.top  + insets.bottom;
            widthMeasureSpec  = MeasureSpec.adjust(widthMeasureSpec,  optical ? -oWidth  : oWidth);
            heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
        }
        if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
                widthMeasureSpec != mOldWidthMeasureSpec ||
                heightMeasureSpec != mOldHeightMeasureSpec) {

            // first clears the measured dimension flag
            mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

            resolveRtlPropertiesIfNeeded();

            // measure ourselves, this should set the measured dimension flag back
            onMeasure(widthMeasureSpec, heightMeasureSpec);

            // flag not set, setMeasuredDimension() was not invoked, we raise
            // an exception to warn the developer
            if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
                throw new IllegalStateException("onMeasure() did not set the"
                        + " measured dimension by calling"
                        + " setMeasuredDimension()");
            }

            mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
        }

        mOldWidthMeasureSpec = widthMeasureSpec;
        mOldHeightMeasureSpec = heightMeasureSpec;
    }
呼叫view.measure(w,h)來測量控制元件寬高,那麼這個方法是何時呼叫的呢?在該view的父控制元件測量自己寬高時呼叫。因為該view所在父容器在測量自己寬高時,會先測量子view的寬高,最終都會呼叫child.measure(w,h),最後才測量自己的寬高。後面分析容器控制元件的測量流程時,會一目瞭然。 主要分析measure(w,h)的兩個關鍵點: 一,欄位mPrivateFlags 1.1 欄位mPrivateFlags在呼叫onMeasure(w,h)前,執行mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET,設定成員變數mPrivateFlags的MEASURED_DIMENSION_SET位設定為0;
1.2 在onMeasure(w,h)執行完成後,判斷if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET)決定是否丟擲IllegalStateException異常; 二,實際測量方法onMeasure(w,h),進入該方法檢視原始碼:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
進入setMeasuredDimension(w,h)檢視原始碼:
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int opticalWidth  = insets.left + insets.right;
            int opticalHeight = insets.top  + insets.bottom;

            measuredWidth  += optical ? opticalWidth  : -opticalWidth;
            measuredHeight += optical ? opticalHeight : -opticalHeight;
        }
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;

        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }
setMeasuredDimension(w,h)是真正完成給view測量寬高,至於引數measuredWidth,measuredHeight是如何計算得來,下面會有分析。小結:測量一個view實際上是給欄位mMeasuredWidth,mMeasuredHeight設定值,最後執行mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET,將欄位mPrivateFlags的EASURED_DIMENSION_SET位設定為1。 mPrivateFlags更像是一個標誌位,在onMeasure測量前設定一個值,在onMeasure執行的最後設定一個值,測量完成後判斷mPrivateFlags的值。若前面沒有執行setMeasuredDimension(w,h)完成測量,那麼mPrivateFlags值則不會重新設定,判斷mPrivateFlags時會執行if語句中內容,丟擲IllegalStateException異常。繼續分析前面提到:引數measuredWidth,measuredHeight是如何計算得來?只分析寬度(高度計算方式類似),分析這樣一段程式碼:getDefaultSize(getSuggestedMinimumWidth(),widthMeasureSpec),於是進入方法getDefaultSize檢視原始碼:
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;
    }
第二個引數measureSpec是父容器呼叫child.measure(w,h)傳入的引數,measureSpec取決於父容器的measureSpec(爺爺容器給的建議值)和自身佈局引數LayoutParams(eg:控制元件寬高,外邊距,內邊距等),後面會具體分析。這裡只需要記住,measureSpec是父容器測量子View時給的建議值。這個建議值measureSpec配合第一個引數size,決定view的最終寬度,也就是getDefaultSize方法返回值。那麼size是什麼東西呢?檢視getSuggestedMinimumWidth()原始碼:
protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }
mMinWidth的值取決於view的佈局引數android:minWidth="",如果沒有設定,則default為0;mBackground.getMinimumWidth()返回該view的背景圖片需要最小寬度值。如果沒有背景圖片,則返回mMinWidth,否則max(mMinWidth,mBackground.getMinimumWidth()取較大值。也就是說引數size:要麼取佈局引數中最小寬度(還可能為0),要麼取背景圖片所需最小寬度。繼續回到getDefaultSize方法來分析寬度(高度計算方式類似),取出父控制元件給的建議值的測量大小specSize,測量模式specMode。 判斷specMode,1,如果是模式AT_MOST / EXACTLY,返回specSize;2,如果specMode是UNSPECIFIED,則父容器不對子view做任何限制,返回size。(UNSPECIFIED這種測量模式一般不做分析,不用管它)  注意,specMode是EXACTLY,說明父控制元件已經知道子view需要的精確值,那麼直接使用specSize容易理解;那specMode是AT_MOST時,說明父控制元件給的建議值是一個子view可以使用的最大值(<=specSize),為什麼直接返回specSize呢?這裡肯定需要修改,因為測量模式為EXACTLY,說明子view寬度:要麼是match_parent,要麼是具體的值(100dp)。測量模式為AT_MOST,說明子View寬度:只可能是wrap_content。在使用這個自定義的view時,不能讓match_parent和wrap_content的體現的效果一樣。 於是可以得出結論:在extends View的自定義控制元件中,需要重寫onMeasure(w,h),並單獨判斷MeasureSpec.getMode(w)為AT_MOST時,返回一個寬度值(具體邏輯按需求來吧),高度同理!檢視TextView原始碼,onMeasure方法有對specMode為AT_MOST進行處理。 原始的View(非容器控制元件)的測量,程式碼流程圖大致如下,儲存該圖片到本地可以清晰展示資訊哦!

三,原始碼分析之容器控制元件

接下來分析容器控制元件測量流程,前面說到容器控制元件(繼承ViewGroup)的測量過程:先測量所有子view,然後再測量容器控制元件本身。每種容器控制元件測量的細節不盡相同,但都遵循上面的方式。閱讀LinearLayout原始碼時,發現裡面if條件判斷極為噁心,於是本篇以FrameLayout為例子分析容器控制元件的測量流程。首先貼上涉及的類,方法結構圖如下,可以看完後面分析回過頭來看此圖。
檢視FrameLayout原始碼,分析容器控制元件的測量過程:
@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
	int count = getChildCount();
        // ...code
        int maxHeight = 0;
        int maxWidth = 0;
        int childState = 0;

        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (mMeasureAllChildren || child.getVisibility() != GONE) {
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                maxWidth = Math.max(maxWidth,
                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
                childState = combineMeasuredStates(childState, child.getMeasuredState());
                if (measureMatchParentChildren) {
                    if (lp.width == LayoutParams.MATCH_PARENT ||
                            lp.height == LayoutParams.MATCH_PARENT) {
                        mMatchParentChildren.add(child);
                    }
                }
            }
        }

        // Account for padding too
        maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
        maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();

        // Check against our minimum height and width
        maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

        // Check against our foreground's minimum height and width
        final Drawable drawable = getForeground();
        if (drawable != null) {
            maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
            maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
        }

        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));
    }
執行for迴圈遍歷一個儲存子View的物件陣列,呼叫measureChildWithMargins方法,首先測量子View,該方法是從父類ViewGroup繼承過來。然後呼叫setMeasuredDimension方法測量容器控制元件自己的寬高,該方法從爺爺類View繼承過來。先來分析measureChildWithMargins,檢視原始碼:
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);
    }
引數child-->子View;引數parentWidthMeasureSpec,parentHeightMeasureSpec--容器控制元件的MeasureSpec;引數widthUsed,heightUsed-->容器控制元件中已被使用的寬/高的數值,這裡為0。 執行getChildMeasureSpec(parentWidthMeasureSpec,mPaddingLeft+mPaddingRight+ lp.leftMargin +lp.rightMargin+widthUsed, lp.width)獲取子View的寬度測量建議值,這個值最終要傳入到child.measure(w,h)中,開始測量子View。 從該方法引數可知:測量時,子View的MeasureSpec值由兩部分組成:(parentWidthMeasureSpec-->父控制元件的MeasureSpec),以及子View本身的佈局引數LayoutParams決定的。 其中LayoutParams中,涉及子view的寬高值,外邊距margin。至於mPaddingLeft 是指FrameLayout與其內容之間的距離,欄位繼承於類View。 接下來分析如何合成子View的MeasureSpec,上面已經得出結論,這裡從程式碼角度具體分析,檢視getChildMeasureSpec的原始碼:
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 = 0;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = 0;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }
引數spec是FrameLayout的MeasureSpec,就是子view的爺爺給爸爸的測量建議值;引數padding是子View的外邊距margin與FrameLayout的padding相加;引數childDimession是子view的寬/高值,由lp.width/lp.height得到。當爸爸FrameLayout的測量模式specMode為EXACTLY時,裡面還要分三種情況討論: 1,當childDimension >= 0(android:layout_width="50dp"),resultSize為50dp,resultMode為精確EXACTLY; 2,當childDimension == LayoutParams.MATCH_PARENT時,由於父容器specMode是精確的,子view又填充所有空間,那麼resultSize大小就為size = Math.max(0, specSize - padding),有具體數值,屬於精確模式Exactly; 3,當childDimension == LayoutParams.WRAP_CONTENT時,子view要小於等於size,於是大小為size,模式為AT_MOST。爸爸FrameLayout的測量模式為AT_MOST,UNSPECIFIED的情況,就不再具體分析了。最後,使用MeasureSpec合併resultSize,resultMode。 至於,child.measure(w,h)的繼續分析,無非就是兩種情況:child如果是容器控制元件,則繼續重複上面的測量流程;如果child是一個原始的view,那就是進入文章前半部分的測量流程。 容器控制元件測量自己,呼叫方法setMeasuredDimension,由方法resolveSizeAndState的返回值,得到測量的寬高大小(不包含測量模式specMode)。這裡不再分析resolveSizeAndState方法,跟原始的view在測量時的getDefaultSize方法很類似,前面已經分析了getDefaultSize方法。子view的測量後寬高,影響到了容器控制元件測量自己,這也是為什麼要先測量所有子view,然後才測量容器控制元件自己。 下面展示了程式碼走向流程圖:
這裡,原始的view(非容器控制元件)與容器控制元件的測量流程分析完畢了。

相關推薦

Android定義控制元件測量onMeasure

由上圖可知,語法角度:子類可以重寫onMeasure,只能繼承View的measure,setMeasuredDimension方法。測量流程分為兩種情況討論:容器控制元件ViewGroup,原始的View(非容器控制元件)。原始的View測量,只需要測量自己的寬高;而容器控制元件需要先測量所有的子View

Android定義控制元件區域性圖片放大鏡--BiggerView

零、前言: 本文的知識點一覽 1.自定義控制元件及自定義屬性的寫法,你也將對onMesure有更深的認識 2.關於bitmap的簡單處理,及canvas區域裁剪 3.本文會實現兩個自定義控制元件:FitImageView(圖片自適應)和BiggerView(放大鏡),前者為後者作為鋪墊。 4.最後會

Android定義控制元件仿汽車家下拉重新整理

關於下拉重新整理的實現原理我在上篇文章Android自定義控制元件之仿美團下拉重新整理中已經詳細介紹過了,這篇文章主要介紹錶盤的動畫實現原理 汽車之家的下拉重新整理分為三個狀態: 第一個狀態為下拉重新整理狀態(pull to refresh),在這個狀

Android定義控制元件《折線圖的繪製》

金融軟體裡的行情分時圖,這是我們最常見的折線圖,當然了,折線圖的用途並不僅僅侷限於此,像一般在一定區間內,為了更好的能顯示出幅度的變化,那麼用折線圖來展示無疑是最符合效果的,當然了,網上也有很多的第

Android定義控制元件實現滑動選擇開關

前言:今天我們仿照著Google給我們提供的Switch控制元件來進行一次模仿,自己動手打造一個可以換滑動圖片以及背景的圖片。 -----------------分割線--------------- 先看一下google提供的Switc控制元件: 其實用法很簡單就當普通的

Android 定義控制元件繼承view

一.自定義控制元件的型別:            1.繼承view(自繪檢視:view中的內容是我們自己繪製出來的,需要重寫onDraw方法)            2.繼承已有原生控制元件            3.自定義組合控制元件(將系統原生的控制元件組合到一起) 本

android定義控制元件圓形進度條(帶動畫)

首先貼上圖片: 額,感覺還行吧,就是進度條的顏色醜了點,不過咱是程式設計師,不是美工,配色這種問題當然不在考慮範圍之內了 下面說重點,如何來寫一個這樣的自定義控制元件。 首先,需要有一個灰色的底圖,來作為未填充時的進度條; 然後,根據傳入的當前進度值,繪製填充時的進度圓

Android定義控制元件百分比圓環進度條

首先我們先來看一下效果 分析 我們來看這個進度條應該分為3個小部分 1.中間的圓 2.外邊的圓環 3.中間的文字 分開畫 這3部分就是需要我們自己畫出來的,因此我們需要3根畫筆 //設定中心園的畫筆

Android定義控制元件圓形頭像

重寫ImageView public class CircleImageView extends ImageView { private static final ScaleType SCALE_TYPE = ScaleType.CENTER_CROP; p

Android 定義控制元件命運輪(抽獎轉盤)

1 思路 首先肯定是要繪製扇形的,每一個獎品為一個扇形區分開,然後在扇形中得有當前獎品的說明,最後讓這個輪盤轉起來就行了。說起來很簡單,但是在繪製的時候,特別是繪製文字的時候還有有一些細節需要注意的,也不是難點,只是要理清楚那些地方應該怎麼去畫,怎麼獲取需要繪製的座標。  

Android 定義控制元件ViewPager Indicator實現方式

介紹 ViewPager 的Indicator實現兩種效果 如下圖所示: 效果一 效果二 佈局檔案: <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmln

Android定義控制元件掃描動畫UI

前言 最近有一個需求,就是做一個掃描的UI,看了很多掃描動畫,發現酷狗的掃描動畫挺漂亮的,所以就做了一個相似的掃描動畫,廢話不多說,先看一下最終的效果吧。 最終效果圖 介紹 首先我們看一下這個效果,它由以下幾部分組成: 1.中間一個音符圖片

Android 定義控制元件 繼承佈局檔案

首先定義一個layout實現按鈕內部佈局: <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:andr

Android定義控制元件入門篇---整理網路上的資源

前言, 我的視訊系列 http://edu.csdn.net/course/detail/2741, 一起來學習Android… 本篇部落格主要是想要講解一下自定義控制元件如何入門,其中有好多資料資源來源自網路,綜合了網路上一些有些的博文

Android定義控制元件畫圓,並且修改其填充色

畫圓就是簡答呼叫了drawCircle的api public class Dot extends View { public Dot(Context context) { s

Android 定義控制元件打造流佈局實現熱門搜尋標籤

最終效果 首先來看看效果圖: 其他地方很好實現,就是熱門搜尋有點麻煩,由於資料的不確定性,那麼像GridView明顯不能滿足了,這時候就只能自己來定義一個佈局了。 最終實現後的效果: 具體實現 1,自定義一個類繼承

Android定義控制元件定義TextView,實現drawableLeft可以和文字一起居中

LZ-Says:給大家推薦一個網站,有興趣可以查閱,想為大家貢獻一點自己的力量也可以投稿,老大稽核通過會發表,更好的幫助有需要的人~歡迎大家踴躍投稿~地址如下: http://ww

Android 定義控制元件基礎幾何圖形繪製詳解

前言 距離寫上一篇自定義View文章已經大半年過去了,一直想繼續寫,但是無奈技術有限,生怕誤人子弟。這段時間專案剛剛完成,有點時間,跟著大神的腳步,鞏固下自定義View的相關基礎知識。 Canvas&Paint Canvas和Pa

Android定義控制元件虛線的用法

Android實現畫虛線的方法 Android中可以通過DashPathEffect來實現,想知道關於PathEffect的詳細用法,請移步PathEffect的詳細用法 程式碼示例: PathE

Android 定義控制元件標籤控制元件

一、首先這是效果 二、實現原理 通過繼承ViewGroup,然後在重寫 onMeasure測量每個View的寬度,重新onLayout控制每個控制元件的位置, 並新增點選事件 三、實現 1、在onMeasure方法中得到顯示方式,並得到寬高 int widt