1.自定義View
自定義View可以分為三個流程:測量、布局、繪制 分別對應(yīng)著onMeasure、onLayout、onDraw方法。
自定義View可以分為兩種類型:
1.自定義ViewGroup :主要是onMeasure()、onLayout()測量和布局方法。
2.自定義View:主要是onMeasure()、onDarw()測量和繪制方法。
- 在ViewGroup和View中都使用了測量onMeasure()方法,接下來通過一個寫一個自定義一個瀑布流ViewGroup學習onMeasure()是怎么測量的。
2.onMeasure()測量
2.1 MeasureSpec
首先要搞清楚,我們?yōu)槭裁匆獪y量?
因為在我們的xml布局文件中,我們設(shè)置width和height時會使用match_parent或warp_content來設(shè)置,測量的方法就是要將之變成具體的值,如250dp等。MeasureSpec的基本知識
每一個ViewGroup和View都會有MeasureSpec。MeasureSpec有32位字節(jié)組成,前兩位:放三個模式,后30位:放控件的大小。
每個子View的MeasureSpec:由父ViewGroup的MeasureSpec和子view的LayoutParams確定。在getChildMeasureSpec()方法中可以查看。
MeasureSpec:exactly、at_most、unspecified
LayoutParams: 100dp、match_parent、warp_content
以下是為每個子View確定MeasureSpec的方法代碼:
如果父View的MeasureSpec模式是exactly,那么子View的是如精確值100dp,則子View的
MeasureSpec的模式是exactly,大小是100dp;子view的layoutparams是match_parent,
那么模式exactly,大小為父view的默認大小;子View是warp_content時,模式是at_most,大小為父view的默認大小。
如果父View的MeasureSpec模式是at_most,那么子View的是如精確值100dp,則子View的
MeasureSpec的模式是exactly,大小是100dp;子view的layoutparams是match_parent,
那么模式at_most,大小為父view的默認大小;子View是warp_content時,模式是at_most,大小為父view的默認大小。
如果父View的MeasureSpec模式是upspecified,那么子View的是如精確值100dp,則子View的
MeasureSpec的模式是exactly,大小是100dp;子view的layoutparams是match_parent,
那么模式upspecified,大小為父view的默認大小;子View是warp_content時,模式是upspecified,大小為父view的默認大小。
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 = 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);
}
2.2 onMeasure()測量的通用流程:
不同ViewGroup由于子View的顯示布局樣式不同,所以代碼都可能不一樣。但是它們在測量時會有一個基本的通用流程:
- 遍歷子View為其設(shè)置MeasureSpec并調(diào)用子view的measure()方法:這一步通過getChildMeasureSpec()方法,也可以使用measureChildMargins()方法完成。
- 確定自定義view的大?。和ㄟ^自身的measureSpec的模式和子view的所需大小,確定自定義view的大小。如viewGroup本身的模式是exactly則不需要理會子view所需的大小。這種模式關(guān)系來確定大小的關(guān)系可以自己定義,也可以有resolveSizeAndState()方法來確定。最后調(diào)用setMeasureDimension()來確定自定義view的大小。
onMeasure()通用流程的代碼:
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
//怎么測孩子呢?
for (i in 0 until childCount) {
val childView = getChildAt(i)
//獲得子View的LayoutParams來確定子View的measureSpec
//可用替代measureChildWithMargins(childView,widthMeasureSpec,0,heightMeasureSpec,0)
var childLP = childView.layoutParams
//讓ViewGroup的MeasureSpec和子View的LayoutParams,來確定子view的MeasureSpec
val childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, paddingLeft + paddingRight, childLP.width)
val childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, paddingTop + paddingBottom, childLP.height)
childView.measure(childWidthMeasureSpec, childHeightMeasureSpec)
}
//自己定義不同模式的大小,也可以調(diào)用resolveSizeAndState()方法定義大小。
val realWidth = if (widthMode == MeasureSpec.EXACTLY) selfWidth else parentNeedWidth
val realHeight = if (heightMode == MeasureSpec.EXACTLY) selHeight else parentNeedHeight
setMeasuredDimension(realWidth, realHeight)
}
2.3 舉例瀑布流onMeasure()的測量:
布局分析:子View得換行:我們要先根據(jù)ViewGroup里MeasureSpec拿到默認的大小(一般都是上一個父View的最大值),然后跟子View所使用的寬度,如果比ViewGroup的默認大小要大,則換行。每一行的view都需要記錄下來,以便在布局中確定位置。同時每一行的最大高度也需要記錄下來,以便在布局中確定位置。同時也把每一行的最大寬度記錄下來。下面是瀑布流布局的OnMeasure()方法代碼
private val mVerticalSpacing = 0
private val mHorizontalSpacing = 0
private val allLines: MutableList<List<View?>> = ArrayList()
private val lineHeights: MutableList<Int> = ArrayList()
private var lineViews: MutableList<View> = ArrayList()
fun clearList(){
allLines.clear()
lineHeights.clear()
lineViews.clear()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
clearList()
val childCount = childCount
//拿到ViewGroup的默認的寬高
val selfWidth = MeasureSpec.getSize(widthMeasureSpec)
val selHeight = MeasureSpec.getSize(heightMeasureSpec)
var parentNeedHeight = 0
var parentNeedWidth = 0
var lineWidthUsed = 0
var lineHeight = 0 //一行的高度
//怎么測孩子呢?
for (i in 0 until childCount) {
val childView = getChildAt(i)
//獲得子View的LayoutParams來確定子View的measureSpec
//measureChildWithMargins(childView,widthMeasureSpec,0,heightMeasureSpec,0)
var childLP = childView.layoutParams
//讓ViewGroup的MeasureSpec和子View的LayoutParams,來確定子view的MeasureSpec
val childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, paddingLeft + paddingRight, childLP.width)
val childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, paddingTop + paddingBottom, childLP.height)
childView.measure(childWidthMeasureSpec, childHeightMeasureSpec)
//獲取子view的測量寬高
val childMeasureWidth = childView.measuredWidth
val childMeasureHeight = childView.measuredHeight
//這個時候需要換行
if (lineWidthUsed + childMeasureWidth > selfWidth) {
allLines.add(lineViews)
lineHeights.add(lineHeight)
parentNeedWidth = Math.max(parentNeedWidth, lineWidthUsed) + mHorizontalSpacing
parentNeedHeight = parentNeedHeight + lineHeight
lineViews = ArrayList()
lineWidthUsed = 0
lineHeight = 0
}
//記錄每一行的view
lineViews.add(childView)
lineWidthUsed = lineWidthUsed + childMeasureWidth //每行已經(jīng)添加的寬度值
lineHeight = Math.max(lineHeight, childMeasureHeight)
if (i == childCount - 1) {
allLines.add(lineViews)
lineHeights.add(lineHeight)
parentNeedWidth = Math.max(parentNeedHeight, lineWidthUsed)
parentNeedHeight = parentNeedHeight + lineHeight
}
}
//再測量自己,確定自己得大小
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val realWidth = if (widthMode == MeasureSpec.EXACTLY) selfWidth else parentNeedWidth
val realHeight = if (heightMode == MeasureSpec.EXACTLY) selHeight else parentNeedHeight
setMeasuredDimension(realWidth, realHeight)
}
3. onLayout()布局方法:
通過上面的onMeasure()方法,已經(jīng)確定了ViewGroup的大小。這時通過調(diào)用子view的.layout(l,t,r,b)來確定每個子view的位置。
3.1 getLeft()、getX()、getRawX()的區(qū)別:
getLeft():是控件左邊到手機屏幕坐標系的左邊的位置。
getX():是手勢點擊的點,到所屬控件的里面左邊位置。
getRawX():是手勢點擊的點,到手機屏幕的左邊位置。
3.2 getWidth()和getMeasureWidth()的區(qū)別
getWidth()和getHeight()是在onLayout()方法執(zhí)行完才有效。
getMeasureWidth()和getMeasureHeight()在onMeasure()就有效。
3.3 例子瀑布流的onLayout()分析:
布局分析:在上面的onMeasure()方法中,我們已經(jīng)記錄了每行都有哪些view,因此我們只要計算每個子view的左上坐標,然后通過view.getMeasureWidth和view.getMeasureHeight的被測量過子view的寬高以此來確定四個點的位置。以下是代碼。
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
//分為幾行
val lineCount = allLines.size
var curL = paddingLeft
var curT = paddingTop
for (i in 0 until lineCount) {
val lineView = allLines[i]
val lineHeight = lineHeights[i]
for (j in lineView.indices) {
val view = lineView[j]
val left = curL
val top = curT
val right = left + view!!.measuredWidth
val bottom = top + view!!.measuredHeight
view.layout(left, top, right, bottom)
curL = right + mHorizontalSpacing
}
curT = curT + lineHeight + mVerticalSpacing
curL = paddingLeft
}
}
寫在最后:
這里寫的一個FlowLayout只是用作學習自定義ViewGroup如何測量和布局的一個簡單例子,由于時間關(guān)系只寫出一個大概。不過現(xiàn)在我們要用到流布局一般會使用recyclerView+FlexboxLayoutManager來寫既方便又快速,這里有篇文章可以參考:RecyclerView之使用FlexboxLayoutManager