Android 自定義View及流程

自定義View繪制流程:


概述


自定義View的基本方法

自定義 View 的最基本的三個方法分別是: onMeasure()、onLayout()、onDraw(); View 在 Activity 中顯示出來,要經(jīng)歷測量、布局和繪制三個步驟,分別對應(yīng)三個動作:measure、layout 和 draw。

  • 測量:onMeasure() 決定 View 的大小;
  • 布局:onLayout() 決定 View 在 ViewGroup 中的位置;
  • 繪制:onDraw() 決定繪制這個 View。

自定義 View 控件分類

  • 自定義 View: 只需要重寫 onMeasure() 和 onDraw()
  • 自定義 ViewGroup: 則只需要重寫 onMeasure() 和 onLayout()
1. 自定義ViewGroup

自定義ViewGroup一般是利用現(xiàn)有的組件根據(jù)特定的布局方式來組成新的組件,大多繼承自ViewGroup或各種Layout,包含有子View。

例如:應(yīng)用底部導(dǎo)航條中的條目,一般都是上面圖標(biāo)(ImageView),下面文字(TextView),那么這兩個就可以用自定義ViewGroup組合成為一個Veiw,提供兩個屬性分別用來設(shè)置文字和圖片,使用起來會更加方便。

2. 自定義View

在沒有現(xiàn)成的View,需要自己實現(xiàn)的時候,就使用自定義View,一般繼承自View,SurfaceView或其他的View,不包含子View。

例如:制作一個支持自動加載網(wǎng)絡(luò)圖片的ImageView,制作圖表等

PS: 自定義View在大多數(shù)情況下都有替代方案,利用圖片或者組合動畫來實現(xiàn),但是使用后者可能會面臨內(nèi)存耗費過大,制作麻煩等諸多問題。

自定義View基礎(chǔ)


View 類簡介

  • View 類是Android中各種組件的基類,如View是ViewGroup基類
  • View表現(xiàn)為顯示在屏幕上的各種視圖

Android中的UI組件都由View、ViewGroup組成。

  • View的構(gòu)造函數(shù):共有4個
    構(gòu)造函數(shù)是View的入口,可以用于初始化一些內(nèi)容,和獲取自定義屬性
    View的構(gòu)造函數(shù)有四種重載分別如下:
  // 如果View是在Java代碼里面new的,則調(diào)用第一個構(gòu)造函數(shù)
  public void SloopView(Context context) {}
  // 如果View是在.xml里聲明的,則調(diào)用第二個構(gòu)造函數(shù) 
  // 自定義屬性是從AttributeSet參數(shù)傳進(jìn)來的 public
  public void SloopView(Context context, AttributeSet attrs) {}
  // 不會自動調(diào)用
  // 一般是在第二個構(gòu)造函數(shù)里主動調(diào)用 
  // 如 view 有 style 屬性時
  public void SloopView(Context context, AttributeSet attrs, int defStyleAttr) {}
  // API 21 之后才使用 
  // 不會自動調(diào)用 
  // 一般是在第二個構(gòu)造函數(shù)里主動調(diào)用 
  // 如View有style屬性時
  public void SloopView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {}

可以看出,關(guān)于View構(gòu)造函數(shù)的參數(shù)有多有少。有四個參數(shù)的構(gòu)造函數(shù)在API21的時候才添加上,暫不考慮

有三個參數(shù)的構(gòu)造函數(shù)中第三個參數(shù)是默認(rèn)的Style,這里的默認(rèn)的Style是指它在當(dāng)前Application或Activity所用的Theme中的默認(rèn)Style,且只有在明確調(diào)用的時候才會生效,以系統(tǒng)中的ImageButton為例說明:

    public ImageButton(Context context, AttributeSet attrs) {
        //調(diào)用了三個參數(shù)的構(gòu)造函數(shù),明確指定第三個參數(shù)
        this(context, attrs, com.android.internal.R.attr.imageButtonStyle);
    }

    public ImageButton(Context context, AttributeSet attrs, int defStyleAttr) {
        //此處調(diào)了四個參數(shù)的構(gòu)造函數(shù),無視即可
        this(context, attrs, defStyleAttr, 0); 
    }

注意:即使你在View中使用了Style這個屬性也不會調(diào)用三個參數(shù)的構(gòu)造函數(shù),所調(diào)用的依舊是兩個參數(shù)的構(gòu)造函數(shù)。

由于三個參數(shù)的構(gòu)造函數(shù)第三個參數(shù)一般不用,暫不考慮,第三個參數(shù)的具體用法會在以后用到的時候詳細(xì)介紹。

排除了兩個之后,只剩下一個參數(shù)和兩個參數(shù)的構(gòu)造函數(shù),他們的詳情如下:

 //一般在直接New一個View的時候調(diào)用。
  public void SloopView(Context context) {}
  
  //一般在layout文件中使用的時候會調(diào)用,關(guān)于它的所有屬性(包括自定義屬性)都會包含在attrs中傳遞進(jìn)來。
  public void SloopView(Context context, AttributeSet attrs) {}

以下方法調(diào)用的是一個參數(shù)的構(gòu)造函數(shù):

  //在Avtivity中
  SloopView view = new SloopView(this);

以下方法調(diào)用的是兩個參數(shù)的構(gòu)造函數(shù):

  //在layout文件中 - 格式為: 包名.View名
  <com.sloop.study.SloopView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>

AttributeSet 與自定義屬性

系統(tǒng)自帶的 View 可以在 xml 中配置屬性,對于寫的好的自定義 View 同樣可以在 xml 中配置屬性,為了使自定義的 View 的屬性可以在 xml 中配置,需要以下4個步驟:

  1. 通過 <declare-styleable>為自定義 View 添加屬性
  2. 在 xml 中為相應(yīng)的屬性聲明屬性值
  3. 在運行時(一般為構(gòu)造函數(shù))獲取屬性值
  4. 將獲取到的屬性值應(yīng)用到 View

View 視圖結(jié)構(gòu)

  1. PhoneWindow 是 Android 系統(tǒng)中最基本的窗口系統(tǒng),繼承自 Windows 類,負(fù)責(zé)管理界面顯示以及事件響應(yīng)。它是 Activity 與 View 系統(tǒng)交互的接口
  2. DecorView 是 PhoneWindow 中的起始節(jié)點 View,繼承于 View 類,作為整個視圖容器來使用。用于設(shè)置窗口屬性。它本質(zhì)上是一個 FrameLayout。
  3. ViewRoot 在 Activity 啟動時創(chuàng)建,負(fù)責(zé)管理、布局、渲染窗口 UI 等。

對于多 View 的視圖,結(jié)構(gòu)是樹形結(jié)構(gòu):最頂層是 ViewGroup,ViewGroup下可能有多個 ViewGroup 或 View,如下圖:

無論是 measure 過程、layout 過程還是 draw 過程,永遠(yuǎn)都是從 View 樹的根節(jié)點開始測量或計算(即從樹的頂端開始),一層一層、一個分支一個分支地進(jìn)行(即樹形遞歸),最終計算整個 View 樹中各個 View,最終確定整個 View 樹的相關(guān)屬性。

Android 坐標(biāo)系及位置獲取方式

Android 中的坐標(biāo)系

Android 中顏色相關(guān)內(nèi)容

Android 支持的顏色模式:

以 ARGB8888 為例介紹顏色定義:

測量View大小(onMeasure)

View 的大小不僅由自身所決定,同時也會受到父控件的影響,為了我們的控件能更好的適應(yīng)各種情況,一般會自己進(jìn)行測量。
測量View大小使用的是onMeasure函數(shù),我們可以從onMeasure的兩個參數(shù)中取出寬高的相關(guān)數(shù)據(jù):

  @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //取出寬度的確切數(shù)值
        int widthsize = MeasureSpec.getSize(widthMeasureSpec);    
        //取出寬度的測量模式 
        int widthmode = MeasureSpec.getMode(widthMeasureSpec);      
       
        //取出高度的確切數(shù)值
        int heightsize = MeasureSpec.getSize(heightMeasureSpec);   
        //取出高度的測量模式
        int heightmode = MeasureSpec.getMode(heightMeasureSpec);    
    }

從上面可以看出 onMeasure 函數(shù)中有 widthMeasureSpec 和 heightMeasureSpec 這兩個 int 類型的參數(shù), 毫無疑問他們是和寬高相關(guān)的, 但它們其實不是寬和高, 而是由寬、高和各自方向上對應(yīng)的測量模式來合成的一個值:

測量模式一共有三種, 被定義在 Android 中的 View 類的一個內(nèi)部類View.MeasureSpec中:

模式 二進(jìn)制數(shù)值 描述
UNSPECIFIED 00 默認(rèn)值,父控件沒有給子view任何限制,子View可以設(shè)置為任意大小。
EXACTLY 01 表示父控件已經(jīng)確切的指定了子View的大小。
AT_MOST 10 表示子View具體大小沒有尺寸限制,但是存在上限,上限一般為父View大小。

在int類型的32位二進(jìn)制位中,31-30這兩位表示測量模式,29~0這三十位表示寬和高的實際值
用 MeasureSpec 的 getSize是獲取數(shù)值, getMode是獲取模式即可。

注意:
如果對View的寬高進(jìn)行修改了,不要調(diào)用super.onMeasure(widthMeasureSpec,heightMeasureSpec);要調(diào)用setMeasuredDimension(widthsize,heightsize); 這個函數(shù)。

確定View大小(onSizeChanged)

這個函數(shù)在視圖大小發(fā)生改變時調(diào)用。
Q: 在測量完View并使用setMeasuredDimension函數(shù)之后View的大小基本上已經(jīng)確定了,那么為什么還要再次確定View的大小呢?

A: 這是因為View的大小不僅由View本身控制,而且受父控件的影響,所以我們在確定View大小的時候最好使用系統(tǒng)提供的onSizeChanged回調(diào)函數(shù)。

onSizeChanged如下:

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

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

確定子View布局位置(onLayout)

確定布局的函數(shù)是onLayout,它用于確定子View的位置,在自定義ViewGroup中會用到,他調(diào)用的是子View的layout函數(shù)。

在自定義ViewGroup中,onLayout一般是循環(huán)取出子View,然后經(jīng)過計算得出各個子View位置的坐標(biāo)值,然后用以下函數(shù)設(shè)置子View位置。

 child.layout(l, t, r, b);

四個參數(shù)分別為:

名稱 說明 對應(yīng)的函數(shù)
l View左側(cè)距父View左側(cè)的距離 getLeft();
t View頂部距父View頂部的距離 getTop();
r View右側(cè)距父View左側(cè)的距離 getRight();
b View底部距父View頂部的距離 getBottom();

具體可以參考 坐標(biāo)系 這篇文章。

5.繪制內(nèi)容(onDraw)

onDraw是實際繪制的部分,也就是我們真正關(guān)心的部分,使用的是Canvas繪圖。

 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
    }

getMeasureWidth 與 getWidth 的區(qū)別

  • getWidth 在layout()過程結(jié)束后才能獲取到;通過視圖右邊的坐標(biāo)減去左邊的坐標(biāo)計算出來的.
  • getMeasuredWidth 在measure()過程結(jié)束后就可以獲取到對應(yīng)的值;通過setMeasuredDimension()方法來進(jìn)行設(shè)置的.

LayoutParams

LayoutParams 翻譯過來就是布局參數(shù),子 View 通過 LayoutParams 告訴父容器(ViewGroup)應(yīng)該如何放置自己。從這個定義中也可以看出來 LayoutParams 與 ViewGroup 是息息相關(guān)的,因此脫離 ViewGroup 談 LayoutParams 是沒有意義的。

事實上,每個 ViewGroup 的子類都有自己對應(yīng)的 LayoutParams 類,典型的如 LinearLayout.LayoutParams 和 FrameLayout.LayoutParams 等,可以看出來 LayoutParams 都是對應(yīng) ViewGroup 子類的內(nèi)部類

MarginLayoutParams

MarginLayoutParams 是和外間距有關(guān)的。事實也確實如此,和 LayoutParams 相比,MarginLayoutParams 只是增加了對上下左右外間距的支持。實際上大部分 LayoutParams 的實現(xiàn)類都是繼承自 MarginLayoutParams,因為基本所有的父容器都是支持子 View 設(shè)置外間距的。

  • 屬性優(yōu)先級問題
    MarginLayoutParams 主要就是增加了上下左右4種外間距。在構(gòu)造方法中,先是獲取了 margin 屬性;如果該值不合法,就獲取 horizontalMargin;如果該值不合法,再去獲取 leftMargin 和 rightMargin 屬性(verticalMargin、topMargin和bottomMargin同理)。我們可以據(jù)此總結(jié)出這幾種屬性的優(yōu)先級

margin > horizontalMargin和verticalMargin > leftMargin和RightMargin、topMargin和bottomMargin

  • 屬性覆蓋問題
    優(yōu)先級更高的屬性會覆蓋掉優(yōu)先級較低的屬性。此外,還要注意一下這幾種屬性上的注釋

Call {@link ViewGroup#setLayoutParams(LayoutParams)} after reassigning a new value

LayoutParams 與 View 如何建立聯(lián)系

  • 在XML中定義 View
  • 在 Java 代碼中直接生成 View 對應(yīng)的實例對象

addView

/**
 * 重載方法1:添加一個子View
 * 如果這個子View還沒有LayoutParams,就為子View設(shè)置當(dāng)前ViewGroup默認(rèn)的LayoutParams
 */
public void addView(View child) {
    addView(child, -1);
}

/**
 * 重載方法2:在指定位置添加一個子View
 * 如果這個子View還沒有LayoutParams,就為子View設(shè)置當(dāng)前ViewGroup默認(rèn)的LayoutParams
 * @param index View將在ViewGroup中被添加的位置(-1代表添加到末尾)
 */
public void addView(View child, int index) {
    if (child == null) {
        throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
    }
    LayoutParams params = child.getLayoutParams();
    if (params == null) {
        params = generateDefaultLayoutParams();// 生成當(dāng)前ViewGroup默認(rèn)的LayoutParams
        if (params == null) {
            throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
        }
    }
    addView(child, index, params);
}

/**
 * 重載方法3:添加一個子View
 * 使用當(dāng)前ViewGroup默認(rèn)的LayoutParams,并以傳入?yún)?shù)作為LayoutParams的width和height
 */
public void addView(View child, int width, int height) {
    final LayoutParams params = generateDefaultLayoutParams();  // 生成當(dāng)前ViewGroup默認(rèn)的LayoutParams
    params.width = width;
    params.height = height;
    addView(child, -1, params);
}

/**
 * 重載方法4:添加一個子View,并使用傳入的LayoutParams
 */
@Override
public void addView(View child, LayoutParams params) {
    addView(child, -1, params);
}

/**
 * 重載方法4:在指定位置添加一個子View,并使用傳入的LayoutParams
 */
public void addView(View child, int index, LayoutParams params) {
    if (child == null) {
        throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
    }

    // addViewInner() will call child.requestLayout() when setting the new LayoutParams
    // therefore, we call requestLayout() on ourselves before, so that the child's request
    // will be blocked at our level
    requestLayout();
    invalidate(true);
    addViewInner(child, index, params, false);
}

private void addViewInner(View child, int index, LayoutParams params,
        boolean preventRequestLayout) {
    .....
    if (mTransition != null) {
        mTransition.addChild(this, child);
    }

    if (!checkLayoutParams(params)) { // ① 檢查傳入的LayoutParams是否合法
        params = generateLayoutParams(params); // 如果傳入的LayoutParams不合法,將進(jìn)行轉(zhuǎn)化操作
    }

    if (preventRequestLayout) { // ② 是否需要阻止重新執(zhí)行布局流程
        child.mLayoutParams = params; // 這不會引起子View重新布局(onMeasure->onLayout->onDraw)
    } else {
        child.setLayoutParams(params); // 這會引起子View重新布局(onMeasure->onLayout->onDraw)
    }

    if (index < 0) {
        index = mChildrenCount;
    }

    addInArray(child, index);

    // tell our children
    if (preventRequestLayout) {
        child.assignParent(this);
    } else {
        child.mParent = this;
    }
    .....
}

自定義LayoutParams

  1. 創(chuàng)建自定義屬性
<resources>
    <declare-styleable name="xxxViewGroup_Layout">
        <!-- 自定義的屬性 -->
        <attr name="layout_simple_attr" format="integer"/>
        <!-- 使用系統(tǒng)預(yù)置的屬性 -->
        <attr name="android:layout_gravity"/>
    </declare-styleable>
</resources>
  1. 繼承MarginLayout
public static class LayoutParams extends ViewGroup.MarginLayoutParams {
    public int simpleAttr;
    public int gravity;

    public LayoutParams(Context c, AttributeSet attrs) {
        super(c, attrs);
        // 解析布局屬性
        TypedArray typedArray = c.obtainStyledAttributes(attrs, R.styleable.SimpleViewGroup_Layout);
        simpleAttr = typedArray.getInteger(R.styleable.SimpleViewGroup_Layout_layout_simple_attr, 0);
        gravity=typedArray.getInteger(R.styleable.SimpleViewGroup_Layout_android_layout_gravity, -1);

        typedArray.recycle();//釋放資源
    }

    public LayoutParams(int width, int height) {
        super(width, height);
    }

    public LayoutParams(MarginLayoutParams source) {
        super(source);
    }

    public LayoutParams(ViewGroup.LayoutParams source) {
        super(source);
    }
}
  1. 重寫ViewGroup中幾個與LayoutParams相關(guān)的方法
// 檢查LayoutParams是否合法
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 
    return p instanceof SimpleViewGroup.LayoutParams;
}

// 生成默認(rèn)的LayoutParams
@Override
protected ViewGroup.LayoutParams generateDefaultLayoutParams() { 
    return new SimpleViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
}

// 對傳入的LayoutParams進(jìn)行轉(zhuǎn)化
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { 
    return new SimpleViewGroup.LayoutParams(p);
}

// 對傳入的LayoutParams進(jìn)行轉(zhuǎn)化
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { 
    return new SimpleViewGroup.LayoutParams(getContext(), attrs);
}

LayoutParams常見的子類

在為View設(shè)置LayoutParams的時候需要根據(jù)它的父容器選擇對應(yīng)的LayoutParams,否則結(jié)果可能與預(yù)期不一致,這里簡單羅列一些常見的LayoutParams子類:

  • ViewGroup.MarginLayoutParams
  • FrameLayout.LayoutParams
  • LinearLayout.LayoutParams
  • RelativeLayout.LayoutParams
  • RecyclerView.LayoutParams
  • GridLayoutManager.LayoutParams
  • StaggeredGridLayoutManager.LayoutParams
  • ViewPager.LayoutParams
  • WindowManager.LayoutParams

MeasureSpec

定義

測量規(guī)格,封裝了父容器對 view 的布局上的限制,內(nèi)部提供了寬高的信息( SpecMode 、 SpecSize ),SpecSize 是指在某種 SpecMode 下的參考尺寸,其中 SpecMode 有如下三種:

  • UNSPECIFIED
    父控件不對你有任何限制,你想要多大給你多大,想上天就上天。這種情況一般用于系統(tǒng)內(nèi)部,表示一種測量狀態(tài)。(這個模式主要用于系統(tǒng)內(nèi)部多次Measure的情形,并不是真的說你想要多大最后就真有多大)
  • EXACTLY
    父控件已經(jīng)知道你所需的精確大小,你的最終大小應(yīng)該就是這么大。
  • AT_MOST
    你的大小不能大于父控件給你指定的size,但具體是多少,得看你自己的實現(xiàn)。

MeasureSpecs 的意義

通過將 SpecMode 和 SpecSize 打包成一個 int 值可以避免過多的對象內(nèi)存分配,為了方便操作,其提供了打包 / 解包方法

MeasureSpec值的確定

MeasureSpec值到底是如何計算得來的呢?

子 View 的 MeasureSpec 值是根據(jù)子 View 的布局參數(shù)(LayoutParams)和父容器的 MeasureSpec 值計算得來的,具體計算邏輯封裝在 getChildMeasureSpec()

  /**
     *
     * 目標(biāo)是將父控件的測量規(guī)格和child view的布局參數(shù)LayoutParams相結(jié)合,得到一個
     * 最可能符合條件的child view的測量規(guī)格。  

     * @param spec 父控件的測量規(guī)格
     * @param padding 父控件里已經(jīng)占用的大小
     * @param childDimension child view布局LayoutParams里的尺寸
     * @return child view 的測量規(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) {
        // 當(dāng)父控件的測量模式 是 精確模式,也就是有精確的尺寸了
        case MeasureSpec.EXACTLY:
            //如果child的布局參數(shù)有固定值,比如"layout_width" = "100dp"
            //那么顯然child的測量規(guī)格也可以確定下來了,測量大小就是100dp,測量模式也是EXACTLY
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } 

            //如果child的布局參數(shù)是"match_parent",也就是想要占滿父控件
            //而此時父控件是精確模式,也就是能確定自己的尺寸了,那child也能確定自己大小了
            else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            }
            //如果child的布局參數(shù)是"wrap_content",也就是想要根據(jù)自己的邏輯決定自己大小,
            //比如TextView根據(jù)設(shè)置的字符串大小來決定自己的大小
            //那就自己決定唄,不過你的大小肯定不能大于父控件的大小嘛
            //所以測量模式就是AT_MOST,測量大小就是父控件的size
            else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // 當(dāng)父控件的測量模式 是 最大模式,也就是說父控件自己還不知道自己的尺寸,但是大小不能超過size
        case MeasureSpec.AT_MOST:
            //同樣的,既然child能確定自己大小,盡管父控件自己還不知道自己大小,也優(yōu)先滿足孩子的需求
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } 
            //child想要和父控件一樣大,但父控件自己也不確定自己大小,所以child也無法確定自己大小
            //但同樣的,child的尺寸上限也是父控件的尺寸上限size
            else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            //child想要根據(jù)自己邏輯決定大小,那就自己決定唄
            else if (childDimension == LayoutParams.WRAP_CONTENT) {
                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);
    }

針對上表,這里再做一下具體的說明

  • 對于應(yīng)用層 View ,其 MeasureSpec 由父容器的 MeasureSpec 和自身的 LayoutParams 來共同決定
  • 對于不同的父容器和view本身不同的LayoutParams,view就可以有多種MeasureSpec。
    1. 當(dāng)view采用固定寬高的時候,不管父容器的MeasureSpec是什么,view的MeasureSpec都是精確模式并且其大小遵循Layoutparams中的大小;
    2. 當(dāng)view的寬高是match_parent時,這個時候如果父容器的模式是精準(zhǔn)模式,那么view也是精準(zhǔn)模式并且其大小是父容器的剩余空間,如果父容器是最大模式,那么view也是最大模式并且其大小不會超過父容器的剩余空間;
    3. 當(dāng)view的寬高是wrap_content時,不管父容器的模式是精準(zhǔn)還是最大化,view的模式總是最大化并且大小不能超過父容器的剩余空間。
    4. Unspecified模式,這個模式主要用于系統(tǒng)內(nèi)部多次measure的情況下,一般來說,我們不需要關(guān)注此模式(這里注意自定義View放到ScrollView的情況 需要處理)。

實例 流式布局

public class FlowLayout extends ViewGroup {

  /**
   * 每個item 橫向間距
   */
  private final int mHorizontalSpacing = dp2px(16);
  /**
   * 每個item 豎向間距
   */
  private final int mVerticallSpacing = dp2px(16);

  /**
   * 記錄所有的行,一行一行的存儲,用于layout
   */
  private List<List<View>> allLines = new ArrayList<>();

  /**
   * 記錄每一行的行高,用于layout
   */
  private List<Integer> lineHeights = new ArrayList<>();

  public FlowLayout(Context context) {
    super(context);
  }

  public FlowLayout(Context context, AttributeSet attrs) {
    super(context, attrs);
  }

  public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
  }

  public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);
  }

  private void clearMeasureParams() {
    allLines.clear();
    lineHeights.clear();
  }

  @Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // 內(nèi)存 抖動
    clearMeasureParams();
    // 先測量孩子
    int childCount = getChildCount();
    int paddingLeft = getPaddingLeft();
    int paddingRight = getPaddingRight();
    int paddingBottom = getPaddingBottom();
    int paddingTop = getPaddingTop();
    //記錄這行已經(jīng)使用了多寬的size
    int lineWidthUsed = 0;
    // ViewGroup解析的父親給我的寬度
    int selfWidth = MeasureSpec.getSize(widthMeasureSpec);
    // ViewGroup解析的父親給我的高度
    int selfHeight = MeasureSpec.getSize(heightMeasureSpec);
    // 保存一行中的所有的view
    List<View> lineView = new ArrayList<>();
    // 一行的行高
    int lineHeight = 0;
    // measure過程中,子View要求的父ViewGroup的寬
    int parentNeededWidth = 0;
    // measure過程中,子View要求的父ViewGroup的高
    int parentNeededHeight = 0;
    for (int i = 0; i < childCount; i++) {
      View childView = getChildAt(i);
      LayoutParams childLP = childView.getLayoutParams();
      if (childView.getVisibility() != View.GONE) {
        // 將layoutParams轉(zhuǎn)變成為 measureSpec
        // 作用:根據(jù)父視圖的MeasureSpec & 布局參數(shù)LayoutParams,計算單個子View的MeasureSpec
        // 注:子view的大小由父view的MeasureSpec值 和 子view的LayoutParams屬性 共同決定
        // 參數(shù)說明
        //  * @param spec 父view的詳細(xì)測量值(MeasureSpec)
        //  * @param padding view當(dāng)前尺寸的的內(nèi)邊距和外邊距(padding,margin)
        //  * @param childDimension 子視圖的布局參數(shù)(寬/高)
        int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, paddingLeft + paddingRight, childLP.width);
        int childHeigthMeasureSpec = getChildMeasureSpec(heightMeasureSpec, paddingTop + paddingBottom, childLP.height);
        // 測量子view的方法,就把孩子測量完了
        childView.measure(childWidthMeasureSpec, childHeigthMeasureSpec);
        // 獲取子view的測量寬高
        int childMesauredWidth = childView.getMeasuredWidth();
        int childMeasuredHeight = childView.getMeasuredHeight();
        // 這里需要換行,等于說 接下來要放置的控件放不下了,需要換行
        if (childMesauredWidth + lineWidthUsed + mHorizontalSpacing > selfWidth) {
          // 一旦換行,我們就可以判斷當(dāng)前行需要的寬和高了,所以此時要記錄下來
          allLines.add(lineView);
          lineHeights.add(lineHeight);
          // 一旦換行,我們就可以判斷當(dāng)前需要的寬和高了,所以要記錄起來
          parentNeededHeight = parentNeededHeight + lineHeight + mVerticallSpacing;
          parentNeededWidth = Math.max(parentNeededWidth, lineWidthUsed + mHorizontalSpacing);

          lineView = new ArrayList<>();
          lineHeight = 0;
          lineWidthUsed = 0;
        }
        // view 是分行l(wèi)ayout的,所以要記錄每一行有哪些view,這樣可以方便layout布局
        lineView.add(childView);
        // 每行也需要加上空格
        lineWidthUsed = lineWidthUsed + childMesauredWidth + mHorizontalSpacing;
        // 獲取每行最高的高度
        lineHeight = Math.max(lineHeight, childMeasuredHeight);
        //處理最后一行數(shù)據(jù)
        if (i == childCount - 1) {
          allLines.add(lineView);
          lineHeights.add(lineHeight);
          // 一旦換行,我們就可以判斷當(dāng)前需要的寬和高了,所以要記錄起來
          parentNeededHeight = parentNeededHeight + lineHeight + mVerticallSpacing;
          parentNeededWidth = Math.max(parentNeededWidth, lineWidthUsed + mHorizontalSpacing);
        }
      }
    }

    // setMeasuredDimension 此接口是設(shè)置自己的大小,并且保存起來
    // 在度量自己,并保存,父親想要獲取的時候,直接調(diào)用孩子的 child.getMeasureWidth就行
    // setMeasuredDimension(width,height);

    // 再測量自己,保存
    // 根據(jù)子View的度量結(jié)果,來重新度量自己ViewGroup
    // 作為一個ViewGroup,它自己也是一個View,它的大小也需要根據(jù)它的父親給它提供的寬高來度量
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int realWidth = widthMode == MeasureSpec.EXACTLY ? selfWidth : parentNeededWidth;
    int realHeight = heightMode == MeasureSpec.EXACTLY ? selfHeight : parentNeededHeight;
    // 這個傳遞的是具體的size,不是MeasureSpec
    setMeasuredDimension(realWidth, realHeight);
  }

  @Override
  protected void onLayout(boolean changed, int l, int t, int r, int b) {
    // 總共的行數(shù)
    int lineCount = allLines.size();
    int curT = getPaddingTop();
    int curL = getPaddingLeft();
    for (int i = 0; i < lineCount; i++) {
      List<View> lineViews = allLines.get(i);
      int lineHeight = lineHeights.get(i);
      // 每行的view進(jìn)行布局
      for (int j = 0; j < lineViews.size(); j++) {
        View view = lineViews.get(j);
        int left = curL;
        int top = curT;
        // getWidth 在layout()過程結(jié)束后才能獲取到;通過視圖右邊的坐標(biāo)減去左邊的坐標(biāo)計算出來的.
        // int right = left + view.getWidth();
        // int bottom = top + view.getHeight();
        // getMeasuredWidth 在measure()過程結(jié)束后就可以獲取到對應(yīng)的值;通過setMeasuredDimension()方法來進(jìn)行設(shè)置的.
        int right = left + view.getMeasuredWidth();
        int bottom = top + view.getMeasuredHeight();
        view.layout(left, top, right, bottom);
        curL = right + mHorizontalSpacing;
      }
      curL = getPaddingLeft();
      curT = curT + lineHeight + mVerticallSpacing;
    }
  }

  public static int dp2px(int dp) {
    return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, Resources.getSystem().getDisplayMetrics());
  }
}

效果圖:


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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