為什么要學(xué)習(xí)自定義View
在實際開發(fā)的過程中,我們會發(fā)現(xiàn)Android提供的原生控件無法滿足我們開發(fā)的需要茄靠,于是乎茂契,便需要進(jìn)行自定義View了。說白了慨绳,自定義View是我們Android開發(fā)進(jìn)階的必經(jīng)之路。自定義View的基本方法
自定義View最基本的三個方法分別是:onMeasure()、onLayout()脐雪、onDraw()厌小;View在Activity中顯示出來,要經(jīng)過測量战秋、布局和繪制三個步驟璧亚,分別對應(yīng)三個動作:measure、layout和draw脂信。
測量:onMeasure()決定View的大醒Ⅲ;
布局:onLayout()決定View在ViewGroup中的位置狰闪;
繪制:onDraw()決定繪制這個View疯搅。
- 自定義控件分類
自定義View:只需要重寫onMeasure()和onDraw()
自定義ViewGroup:只需要重寫onMeasure()和onLayout() - 自定義View基礎(chǔ)
自定義View包含布局(onLayout、onMeasure)埋泵、顯示(onDraw)幔欧、事件分發(fā)(onTouchEvent)。
4.1 View的分類
視圖View主要分為兩類
類別 | 描述 |
---|---|
單一視圖 | 即一個View丽声,如TextView礁蔗;不包含子View |
視圖組 | 即多個View組成的ViewGroup,如LinearLayout雁社;包含子View |
4.2 自定義View的繪制流程
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等等赘被。
對于多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)屬性宰衙。
4.5 Android坐標(biāo)系
Android的坐標(biāo)系定義為:
屏幕的左上角為坐標(biāo)原點
向右為x軸增大方向平道,向下為y軸增大方向
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();
- 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的添加
5.3 view的繪制流程
- 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
- 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 |
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ī)則
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