Android自定義View介紹

  1. 為什么要學(xué)習(xí)自定義View
    在實際開發(fā)的過程中,我們會發(fā)現(xiàn)Android提供的原生控件無法滿足我們開發(fā)的需要茄靠,于是乎茂契,便需要進(jìn)行自定義View了。說白了慨绳,自定義View是我們Android開發(fā)進(jìn)階的必經(jīng)之路。

  2. 自定義View的基本方法
    自定義View最基本的三個方法分別是:onMeasure()、onLayout()脐雪、onDraw()厌小;View在Activity中顯示出來,要經(jīng)過測量战秋、布局和繪制三個步驟璧亚,分別對應(yīng)三個動作:measure、layout和draw脂信。

測量:onMeasure()決定View的大醒Ⅲ;
布局:onLayout()決定View在ViewGroup中的位置狰闪;
繪制:onDraw()決定繪制這個View疯搅。

  1. 自定義控件分類
    自定義View:只需要重寫onMeasure()和onDraw()
    自定義ViewGroup:只需要重寫onMeasure()和onLayout()
  2. 自定義View基礎(chǔ)
    自定義View包含布局(onLayout、onMeasure)埋泵、顯示(onDraw)幔欧、事件分發(fā)(onTouchEvent)。

4.1 View的分類
視圖View主要分為兩類

類別 描述
單一視圖 即一個View丽声,如TextView礁蔗;不包含子View
視圖組 即多個View組成的ViewGroup,如LinearLayout雁社;包含子View

4.2 自定義View的繪制流程


image.png
image.png

4.2 View類簡介
View類是Android中各種組件的基類浴井,如View是ViewGroup基類
View表現(xiàn)為顯示在屏幕上的各種視圖
Android中的UI組件都由View、ViewGroup組成
View的構(gòu)造函數(shù):共有4個

    // 如果 View是在 Java代碼里面 new的霉撵,則調(diào)用第一個構(gòu)造函數(shù)
    public ViewTest(Context context) {
        super(context);
    }
    // 如果 View是在 .xml里聲明的滋饲,則調(diào)用第二個構(gòu)造函數(shù)
    // 自定義屬性是從 AttributeSet參數(shù)傳進(jìn)來的
    public ViewTest(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }
    // 不會自動調(diào)用
    // 一般是在第二個構(gòu)造函數(shù)里主動調(diào)用
    // 如 View有style屬性時
    public ViewTest(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    // API21之后才使用
    // 不會自動調(diào)用
    // 一般是在第二個構(gòu)造函數(shù)里主動調(diào)用
    // 如 View有style屬性時
    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public ViewTest(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

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

通過< declare-styleable >為自定義View添加屬性
在xml中為相應(yīng)的屬性聲明屬性值
在運行時(一般為構(gòu)造函數(shù))獲取屬性值
將獲取到的屬性值應(yīng)用到View
4.4 View視圖結(jié)構(gòu)
PhoneWindow是Android系統(tǒng)中最基本的窗口系統(tǒng),繼承自Windows類崭参,負(fù)責(zé)管理界面顯示以及事件響應(yīng)呵曹。它是Activity與View系統(tǒng)交互的接口。
DecorView是PhoneWindow中的起始節(jié)點View何暮,繼承于View類奄喂,作為整個視圖容器來使用。用于設(shè)置窗口屬性海洼。它本質(zhì)上是一個FrameLayout跨新。
ViewRoot在Activity啟動時創(chuàng)建,負(fù)責(zé)管理坏逢、布局域帐、渲染窗口UI等等赘被。


image.png

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


image.png

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

4.5 Android坐標(biāo)系
Android的坐標(biāo)系定義為:

屏幕的左上角為坐標(biāo)原點
向右為x軸增大方向平道,向下為y軸增大方向


image.png

image.png

4.6 View位置(坐標(biāo))描述
View的位置由4個頂點決定的4個頂點的位置描述分別由4個值決定:
View的位置時相對于父控件而言的

Top:子View上邊界到父View上邊界的距離
Left:子View左邊界到父View左邊界的距離
Bottom:子View下邊界到父View上邊界的距離
Right:子View右邊界到父View左邊界的距離
4.7 位置獲取方式
View的位置時通過view.getxxx()函數(shù)進(jìn)行獲取:(以Top為例)

// 獲取Top位置
public final int getTop() {
        return mTop;
    }

    // 其余如下:
    getLeft();  // 獲取子View左上角距父View左側(cè)的距離
    getBottom();    // 獲取子View右下角距父View頂部的距離
    getRight(); // 獲取子View右下角距父View左側(cè)的距離

與MotionEvent中g(shù)et()和getRaw()的區(qū)別:

// get():觸摸點相對于其所在組件坐標(biāo)系的坐標(biāo)
event.getX();
event.getY();

// getRaw():觸摸點相對于屏幕默認(rèn)坐標(biāo)系的坐標(biāo)
event.getRawX();
event.getRawY();
  1. View樹的繪制流程
    5.1 View樹的繪制流程是誰負(fù)責(zé)的菩浙?
    view樹的繪制流程是通過ViewRoot去負(fù)責(zé)繪制的巢掺,ViewRoot這個類的命名有點坑,最初看到這個名字劲蜻,翻譯過來是view的根節(jié)點陆淀,但是事實完全不是這樣,ViewRoot其實不是View的根節(jié)點先嬉,它連view節(jié)點都算不上轧苫,它的主要作用是View樹的管理者,負(fù)責(zé)將DecorView和PhoneWindow“組合”起來疫蔓,而View樹的根節(jié)點嚴(yán)格意義上來說只有DecorView含懊;每個DecorView都有一個ViewRoot與之關(guān)聯(lián),這種關(guān)聯(lián)關(guān)系是由WindowManager去進(jìn)行管理的衅胀。

5.2 view的添加


image.png

5.3 view的繪制流程


image.png
  1. 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)部類确沸。

6.1 LayoutParams與View如何建立聯(lián)系
在xml中定義View
在Java代碼中直接生成View對應(yīng)的實例對象
6.2 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);
}

/**
 1. 重載方法3:添加一個子View
 2. 使用當(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);
}
/**
 3. 重載方法4:添加一個子View愿待,并使用傳入的LayoutParams
*/
@Override
public void addView(View child, LayoutParams params) {
    addView(child, -1, params);
}
/**
 4. 重載方法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;
    }
    .....
}

6.3 自定義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>

2.繼承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);
    }
}

3.重寫ViewGroup中幾個與LayoutParams相關(guān)的方法

// 檢查LayoutParams是否合法
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
    return p instanceof SimpleViewGroup.LayoutParams;
}
// 生成默認(rèn)的LayoutParam
@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);
}

6.4 LayoutParams常見的子類
ViewGroup.MarginLayoutParams
FrameLayout.LayoutParams
LinearLayout.LayoutParams
RelativeLayout.LayoutParams
RecyclerView.LayoutParams
GridLayoutManager.LayoutParams
StaggeredGridLayoutManager.LayoutParams
ViewPager.LayoutParams
WindowManager.LayoutParams

  1. MeasureSpec
    7.1 定義
    MeasureSpec是View中的內(nèi)部類,基本都是二進(jìn)制運算年扩。由于int是32位蚁廓,用高2位表示mode(UNSPECIFIED访圃、EXACTLY厨幻、AT_MOST),低30位表示size腿时,MODE_SHIFT=30的作用是移位况脆。
    測量規(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)。

specMode 描述
UNSPECIFIED 不對View大小做限制隐解,系統(tǒng)使用
EXACTLY 確切的大小鞍帝,如:100dp
AT_MOST 大小不可超過某數(shù)值,如:matchParent煞茫,最大不能超過父View
image.png

7.2 MeasureSpecs的意義

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

7.3 MeasureSpec值的確定

/**
*
* 目標(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);
}

7.4 普通View的MeasureSpec的創(chuàng)建規(guī)則

image.png

7.5 getMeasureWidth和getWidth的區(qū)別

getMeasureWidth:

在measure()過程結(jié)束后就可以獲取到對應(yīng)的值
通過setMeasuredDimension()方法來進(jìn)行設(shè)置的
getWidth:

在layout()過程結(jié)束后才能獲取到
通過視圖右邊的坐標(biāo)減去左邊的坐標(biāo)計算出來的

8 自定義View的例子流式布局CustomFlowLayout:

package com.example.nqct.customview;

import android.content.Context;
import android.content.res.Resources;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;

import java.util.ArrayList;
import java.util.List;

/**
 * 自定義ViewGroup,流式布局
 */
public class CustomFlowLayout extends ViewGroup {

    private static final String TAG = CustomFlowLayout.class.getName();
    private int mHorizontalSpacing = dp2px(16); //每個item橫向間距
    private int mVerticalSpacing = dp2px(8); //每個item橫向間距

    //記錄所有的行吠卷,一行一行的存儲锡垄,用于layout,其中List<View>代表一行的所有View集合
    private List<List<View>> mAllLineViews = new ArrayList<>();
    //記錄每一行的行高撤嫩,用于layout
    private List<Integer> mLineHeights = new ArrayList<>();

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

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

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

    private void clearMeasureParams() {
        mAllLineViews.clear();
        mLineHeights.clear();
    }

    /**
     * 遍歷子View偎捎,測量子View大小和測量父View本身大小
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //內(nèi)存抖動
        clearMeasureParams();

        int childCount = getChildCount();

        int paddingLeft = getPaddingLeft();
        int paddingRight = getPaddingRight();
        int paddingTop = getPaddingTop();
        int paddingBottom = getPaddingBottom();
        Log.i(TAG, "onMeasure==>paddingLeft="+paddingLeft+", paddingRight="+paddingRight
            +", paddingTop="+paddingTop+", paddingBottom="+paddingBottom
            +", childCount="+childCount);

        //onMeasure==>viewGroupWidth=1080, viewGroupHeight=1990
        //當(dāng)前ViewGroup的寬高(即CustomFlowLayout的寬高)
        int viewGroupWidth = MeasureSpec.getSize(widthMeasureSpec);
        int viewGroupHeight = MeasureSpec.getSize(heightMeasureSpec);
        //當(dāng)前ViewGroup的測量模式(即CustomFlowLayout的測量模式)
        int viewGroupWidthMode = MeasureSpec.getMode(widthMeasureSpec);
        int viewGroupHeightMode = MeasureSpec.getMode(heightMeasureSpec);

        List<View> lineViews = new ArrayList<>(); //保存一行中的所有的view
        int lineWidthUsed = 0; //記錄這行已經(jīng)使用了多寬的size
        int lineHeight = 0; // 一行的行高

        int parentNeededWidth = 0;  // measure過程中,子View要求的父ViewGroup的寬
        int parentNeededHeight = 0; // measure過程中序攘,子View要求的父ViewGroup的高

        Log.i(TAG, "onMeasure==>viewGroupWidth="+viewGroupWidth+", viewGroupHeight="+viewGroupHeight);

        for (int i=0;i<childCount;i++) {
            View childView = getChildAt(i);
            ViewGroup.LayoutParams childLp = childView.getLayoutParams();
            //子view不為gone茴她,才可以測量子view的寬高
            if (childView.getVisibility() != View.GONE) {
                /**
                 * 根據(jù)父view的測量規(guī)格結(jié)合子View的LayoutParams,得到符合條件的子View的測量規(guī)格
                 * getChildMeasureSpec:參數(shù)一:父View的spec程奠,參數(shù)二:父View的padding丈牢,參數(shù)三:子View的寬或高
                 */
                int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                        paddingLeft + paddingRight, childLp.width);
                int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
                        paddingTop + paddingBottom, childLp.height);
                //根據(jù)子view的測量規(guī)格,測量子View的寬高
                childView.measure(childWidthMeasureSpec, childHeightMeasureSpec);

                //獲取子view的測量寬高
                int childMeasuredWidth = childView.getMeasuredWidth();
                int childMeasuredHeight = childView.getMeasuredHeight();

                //childView==>i=0, childWidthMeasureSpec=-2147482568, childHeightMeasureSpec=-2147481658, childMeasuredWidth=126, childMeasuredHeight=57
                Log.i(TAG, "childView==>i="+i+", childWidthMeasureSpec="+childWidthMeasureSpec
                        +", childHeightMeasureSpec="+childHeightMeasureSpec
                        +", childMeasuredWidth="+childMeasuredWidth
                        +", childMeasuredHeight="+childMeasuredHeight);

                if (childMeasuredWidth + lineWidthUsed + mHorizontalSpacing > viewGroupWidth) {
                    Log.i(TAG, "childView==>i="+i+", 需要換行了");
                    //如果需要換行的情況瞄沙,一旦換行己沛,就可以判斷當(dāng)前行需要的寬和高了,所以需要記錄下來
                    mAllLineViews.add(lineViews);
                    mLineHeights.add(lineHeight);

                    //父View需要的寬度
                    parentNeededWidth = Math.max(parentNeededWidth, lineWidthUsed + mHorizontalSpacing);
                    //父View需要的高度
                    parentNeededHeight = parentNeededHeight + lineHeight + mVerticalSpacing;

                    //重新創(chuàng)建下一行的View集合距境,重置已用的行寬度和行高
                    lineViews = new ArrayList<>();
                    lineWidthUsed = 0;
                    lineHeight = 0;
                }
                //view 是分行l(wèi)ayout的申尼,所以要記錄每一行有哪些view,這樣可以方便layout布局
                lineViews.add(childView);
                //每行都會有自己的寬和高(每加一個View垫桂,就會執(zhí)行一遍子view寬度和橫向空格加的操作)
                lineWidthUsed = lineWidthUsed + childMeasuredWidth + mHorizontalSpacing;
                //每一行的高度取子View高度的最大值
                lineHeight = Math.max(lineHeight, childMeasuredHeight);

                if (i == childCount - 1) {
                    mAllLineViews.add(lineViews);
                    mLineHeights.add(lineHeight);

                    //父View需要的寬度
                    parentNeededWidth = Math.max(parentNeededWidth, lineWidthUsed + mHorizontalSpacing);
                    //父View需要的高度
                    parentNeededHeight = parentNeededHeight + lineHeight + mVerticalSpacing;
                }
            }
        }

        //根據(jù)子View的測量結(jié)果师幕,來測量ViewGroup自己的寬高
        int viewGroupRealWidth  = (viewGroupWidthMode == MeasureSpec.EXACTLY) ? viewGroupWidth : parentNeededWidth;
        int viewGroupRealHeight  = (viewGroupHeightMode == MeasureSpec.EXACTLY) ? viewGroupHeight : parentNeededHeight;
        //測量ViewGroup寬高
        setMeasuredDimension(viewGroupRealWidth, viewGroupRealHeight);

        //測試代碼
//        int measuredWidth = getMeasuredWidth();
//        int measuredHeight = getMeasuredHeight();
//        Log.i(TAG, "onMeasure==>ViewGroup測量寬高...measuredWidth="+measuredWidth+", measuredHeight="+measuredHeight);
    }

    /**
     * 遍歷每一行,擺放子View
     * @param changed
     * @param l
     * @param t
     * @param r
     * @param b
     */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        //onLayout==>changed=true, l=0, t=0, r=1080, b=1990
        Log.i(TAG, "onLayout==>changed="+changed+", l="+l+", t="+t+", r="+r+", b="+b);
        //獲取總行數(shù)
        int lineCount = mAllLineViews.size();

        int curL = getPaddingLeft();
        int curT = getPaddingTop();

        /**
         * 遍歷每一行View
         */
        for (int i = 0; i < lineCount; i++){
            List<View> lineViews = mAllLineViews.get(i);

            //得到每行的行高
            int lineHeight = mLineHeights.get(i);
            /**
             * 遍歷一行View诬滩,進(jìn)行擺放位置
             */
            for (int j = 0; j < lineViews.size(); j++){
                View childView = lineViews.get(j);
                int left = curL;
                int top =  curT;

                int right = left + childView.getMeasuredWidth();
                int bottom = top + childView.getMeasuredHeight();
                childView.layout(left,top,right,bottom);
                curL = right + mHorizontalSpacing;
            }
            //每遍歷一行霹粥,為了下一行擺放View灭将,改變一次top的值
            curT = curT + lineHeight + mVerticalSpacing;
            curL = getPaddingLeft();
        }
    }

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

對上述代碼中其中的getChildMeasureSpec方法拿來參考

/**
* ViewGroup的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 them 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);
    }

布局文件如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <com.example.nqct.customview.CustomFlowLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="哈哈哈"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="嘿嘿嘿"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="嘎嘎嘎"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@drawable/flow_item_text_bg"
            android:text="嘻嘻嘻"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@drawable/flow_item_text_bg"
            android:text="呵呵呵"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@drawable/flow_item_text_bg"
            android:text="哦哦哦"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@drawable/flow_item_text_bg"
            android:text="呱呱呱"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="嘩嘩嘩"/>
    </com.example.nqct.customview.CustomFlowLayout>

</LinearLayout>

引用的TextView的背景drawable文件flow_item_text_bg.xml

<?xml version="1.0" encoding="utf-8"?>
<!--相當(dāng)于做了一張圓角的圖片作為背景圖片-->
<shape android:shape="rectangle" xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="#FFE7BA"/>
    <padding android:top="8dp" android:right="8dp" android:left="8dp" android:bottom="8dp"/>
    <!--設(shè)置圓角-->
    <corners android:radius="25dp"/>
</shape>

————————————————
版權(quán)聲明:本文為CSDN博主「禿禿禿禿禿禿禿頭」的原創(chuàng)文章,遵循CC 4.0 BY-SA版權(quán)協(xié)議后控,轉(zhuǎn)載請附上原文出處鏈接及本聲明庙曙。
原文鏈接:https://blog.csdn.net/qq_45716076/article/details/120460575

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市浩淘,隨后出現(xiàn)的幾起案子捌朴,更是在濱河造成了極大的恐慌,老刑警劉巖馋袜,帶你破解...
    沈念sama閱讀 211,639評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件男旗,死亡現(xiàn)場離奇詭異舶斧,居然都是意外死亡欣鳖,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,277評論 3 385
  • 文/潘曉璐 我一進(jìn)店門茴厉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來泽台,“玉大人,你說我怎么就攤上這事矾缓』晨幔” “怎么了?”我有些...
    開封第一講書人閱讀 157,221評論 0 348
  • 文/不壞的土叔 我叫張陵嗜闻,是天一觀的道長蜕依。 經(jīng)常有香客問我,道長琉雳,這世上最難降的妖魔是什么样眠? 我笑而不...
    開封第一講書人閱讀 56,474評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮翠肘,結(jié)果婚禮上檐束,老公的妹妹穿的比我還像新娘。我一直安慰自己束倍,他們只是感情好被丧,可當(dāng)我...
    茶點故事閱讀 65,570評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著绪妹,像睡著了一般甥桂。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上邮旷,一...
    開封第一講書人閱讀 49,816評論 1 290
  • 那天黄选,我揣著相機與錄音,去河邊找鬼廊移。 笑死糕簿,一個胖子當(dāng)著我的面吹牛探入,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播懂诗,決...
    沈念sama閱讀 38,957評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼蜂嗽,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了殃恒?” 一聲冷哼從身側(cè)響起植旧,我...
    開封第一講書人閱讀 37,718評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎离唐,沒想到半個月后病附,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,176評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡亥鬓,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,511評論 2 327
  • 正文 我和宋清朗相戀三年完沪,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片嵌戈。...
    茶點故事閱讀 38,646評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡覆积,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出熟呛,到底是詐尸還是另有隱情宽档,我是刑警寧澤,帶...
    沈念sama閱讀 34,322評論 4 330
  • 正文 年R本政府宣布庵朝,位于F島的核電站吗冤,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏九府。R本人自食惡果不足惜椎瘟,卻給世界環(huán)境...
    茶點故事閱讀 39,934評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望昔逗。 院中可真熱鬧降传,春花似錦、人聲如沸勾怒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,755評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽笔链。三九已至段只,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間鉴扫,已是汗流浹背赞枕。 一陣腳步聲響...
    開封第一講書人閱讀 31,987評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人炕婶。 一個月前我還...
    沈念sama閱讀 46,358評論 2 360
  • 正文 我出身青樓姐赡,卻偏偏與公主長得像,于是被迫代替她去往敵國和親柠掂。 傳聞我的和親對象是個殘疾皇子项滑,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,514評論 2 348

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