自定义View是老生常谈的东西了,早期android开发谈起自定义View都是感觉很复杂,很难的东西,但是android发展到今天,应该说对每个高级开发者都是必备技能了,最近写代码常用kotlin,想起之前学习自定义View这块的流式布局,所以决定用kotlin重写下,顺便复习下自定义View的知识。
自定义View也是我们android开发中的一个大块,可以谈的内容很多,由于这篇文章是由流式布局引出的,所以这里的讨论主要集中在onMeasure这里,其他的内容还有其他文章在来总结,所以下面先介绍下onMeasure这里的知识点,然后在放上kotlin重写的流式布局。
所以我们这里叫自定义View,但是其实可以分为自定义View和自定义ViewGroup,自定义View主要需要关系onMeasure和onDraw,自定义ViewGroup主要关心onMeasure和onLayout,下面我们就来谈下onMeasure,我们可以看下onMeasure方法的签名:
```kotlin
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
......省略
}
```
onMeasure是测试具体某个view的尺寸的,从签名的入参可以看到,一个是宽度,一个是高度,简单理解,就是当前的这个view或者viewGroup就是根据这2个入参来决定自己的大小的,下面来看下怎么使用这2个值:
```kotlin
var widthMode = MeasureSpec.getMode(widthMeasureSpec)
var heightMode = MeasureSpec.getMode(heightMeasureSpec)
var selfWidth = MeasureSpec.getSize(widthMeasureSpec)
var selfHeight = MeasureSpec.getSize(heightMeasureSpec)
```
可以看到一般都是通过MeasureSpec.getMode和MeasureSpec.getSize这2个方法取出这个值得mode和size,为什么一个Int值还会分开有一个mode和一个size,这2个代表什么意思呢?这个我们先要看下入参的那2个值,到底是个什么值,我们看他们赋值的地方,看下getChildMeasureSpec这个方法(这个方法下面就要详细介绍),找到任意一个赋值的地方,如下:
```java
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
```
可以看到正好是一个size,一个mode的赋值,他们都是int类型的,也就是4个字节(32位),size很好理解,就是尺寸的大小,mode上面代码中是MeasureSpec.EXACTLY(具体含义下面介绍),除了这个之外,还有MeasureSpec.AT_MOST和MeasureSpec.UNSPECIFIED,我们跟过去看下这几个值的定义:
```java
private static final int MODE_SHIFT = 30;
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static final int AT_MOST = 2 << MODE_SHIFT;
```
可以看到这个mode的值是由0,1,2左移30位后的值,也就是这个int的高2位是0,1或者2,低30位都是0。
然后在来看size和mode最终是怎么拼装的,如下方法:
```java
@IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
@Retention(RetentionPolicy.SOURCE)
public @interface MeasureSpecMode {}
public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
@MeasureSpecMode int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
```
可以看到最终size和mode的return的值就是size + mode,由于int是32位,mode前面说了左移了30位,然后再和size相加,就得出了我们最初说的那2个入参。顺便提一下,上面这个方法的入参对size和mode都做了限定,size由0到2^30-1,这样就不会覆盖掉最高的2位了,mode是自定义了一个接口,该接口的值也限定了UNSPECIFIED, EXACTLY, AT_MOST这3种。这样MeasureSpec的值产生到此也基本都清楚了。下面再看下get这2个值的方法:
```java
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
@MeasureSpecMode
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
```
可以看到MODE_MASK 是3(也就是11)左移30位,然后getMode是和它做与位运算,getSize是把它取反后做与位运算,得到的值分别也就是高位2和低30位的值,也就是我们需要的mode类型和尺寸size大小。好了,主要设计到的方法基本都知道了,下面结合代码说下这2个值产生的规则和意义。
我们知道,我们在写xml布局文件的时候都是一层一层嵌套的,这里产生最终的值也是从根布局开始一层一层递归着往里面计算,上面我们讨论的onMeasure这个方法都是该view或者viewgroup的父亲通过计算后调用的,然后它如果有孩子的话,会通过getChildMeasureSpec方法去计算孩子的MeasureSpec,然后再调用孩子的onMeasure方法,这样一层层计算下去,最终完成整个界面的绘制,下面我们就看下getChildMeasureSpec这个方法:
```java
//计算孩子结果的MeasureSpec
//@param spec是父亲的MeasureSpec
//@param padding是父亲的padding
//@param childDimension是孩子的layoutParams的尺寸
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec); //父亲的mode
int specSize = MeasureSpec.getSize(spec); //父亲的size
int size = Math.max(0, specSize - padding); //父亲减去padding后可供还在用的size
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// 父亲mode是MeasureSpec.EXACTLY,这个可以理解为父亲的尺寸是个具体的值(比如dp,px等)
case MeasureSpec.EXACTLY:
if (childDimension >= 0) { //孩子需要的尺寸是个具体的值
resultSize = childDimension; //给孩子自己需要的值
resultMode = MeasureSpec.EXACTLY; //mode为MeasureSpec.EXACTLY
} else if (childDimension == LayoutParams.MATCH_PARENT) { //childDimension 等于LayoutParams.MATCH_PARENT(-1)
resultSize = size; //父亲是个具体的值,孩子要和父亲一样,所以父亲把自己的值给孩子
resultMode = MeasureSpec.EXACTLY; 由于父亲已经确定了值,孩子和父亲一样,所以是EXACTLY
} else if (childDimension == LayoutParams.WRAP_CONTENT) { //childDimension 等于LayoutParams.WRAP_CONTENT(-2)
//孩子要自己确定值,由于父亲的值已经确定,所以父亲需要限制孩子最多不能超过父亲给它的
resultSize = size;
resultMode = MeasureSpec.AT_MOST; //mode为MeasureSpec.AT_MOST
}
break;
// 父亲mode是MeasureSpec.AT_MOST,这个可以理解为父亲给了一个最大的尺寸,孩子不能超过这个尺寸
case MeasureSpec.AT_MOST:
if (childDimension >= 0) { // 孩子尺寸是个具体的值,同上
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
//孩子需要和父亲一样大小,父亲由于没有确定的大小,只有一个最大尺寸,所以把最大尺寸给孩子,然后限制孩子不能超过这个大小
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
//孩子要自己确定大小,由于父亲的大小也还没确定,所以只能给孩子一个最大的尺寸,并限制孩子不能超过这个尺寸
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// 下面这种模式一般开发不太用到,系统内部使用,大致是不限制大小,随便用?具体可能还是会有一些限制的,由于平时对于我们也不太用,所以也就不多说了
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);
}
```
以上是onMeasure方法的核心,在<<Android开发艺术探索>>一书中有总结过一个表格:
|child / parent |EXACTLY|AT_MOST|UNSPECIFIED|
|:-:|:-:|:-:|:-:|
|dp/px|EXACTLY<br>childSize|EXACTLY<br>childSize|EXACTLY<br>childSize|
|MATCH_PARENT|EXACTLY<br>parentSize|AT_MOST<br>parentSize|UNSPECIFIED<br>0|
|WRAP_CONTENT|AT_MOST<br>parentSize|AT_MOST<br>parentSize|UNSPECIFIED<br>0|
以上这些基本就是onMeasure的测量的主要流程了,测量完成后就可以onLayout来布局了,布局主要就是调用layout方法,然后根据上面的测量结果来放置具体位置。下面的流式布局就是用kotlin改写后的代码,基本思路就是根据上面来的,下面看代码:
```kotlin
private fun clearMeasureParams(){
allLines.clear()
lineHeights.clear()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
//由于子view的onMeasure的调用次数由父亲决定,可能不是一次,所以每次调用前都要重置的数据集合
clearMeasureParams()
//得到子view的数量
var childCount = childCount
//得到四个方向的padding
var paddingLeft = paddingLeft
var paddingRight = paddingRight
var paddingTop = paddingTop
var paddingBottom = paddingBottom
//得到自己的宽和高
var selfWidth = MeasureSpec.getSize(widthMeasureSpec)
var selfHeight = MeasureSpec.getSize(heightMeasureSpec)
var lineViews:MutableList<View> = ArrayList()
var lineWidthUsed = 0
var lineHeight = 0
var parentNeededWidth = 0
var parentNeededHeight = 0
for(index in 0 until childCount){
var childView = getChildAt(index)
var childLP = childView.layoutParams
if(childView.visibility != View.GONE){
//测量子view的大小,传入自己的MeasureSpec给子view参考,以及padding和子view的尺寸
var childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,paddingLeft+paddingRight,childLP.width)
var childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,paddingTop+paddingBottom,childLP.height)
childView.measure(childWidthMeasureSpec,childHeightMeasureSpec)
//得到子view测量后的宽和高
var childMesauredWidth = childView.measuredWidth
var childMeasuredHeight = childView.measuredHeight
//如果子view的宽度+该行以及使用了的尺寸+元素之间水平间隔 > 父亲可以给的大小,说明要换行了
if(childMesauredWidth + lineWidthUsed + mHorizontalSpacing > selfWidth){
//添加一整行view到集合中
allLines.add(lineViews)
//添加该行的行高
lineHeights.add(lineHeight)
//计算当前需要的总行高
parentNeededHeight += lineHeight + mVerticalSpacing
//计算当前的行宽
parentNeededWidth = Math.max(parentNeededWidth,lineWidthUsed+mHorizontalSpacing)
//重置相关数据,开始下一轮行的计算
lineViews = ArrayList()
lineWidthUsed = 0
lineHeight = 0;
}
//当前行中的元素加入集合
lineViews.add(childView)
//更新当前行的宽度
lineWidthUsed += childMesauredWidth + mHorizontalSpacing
//更新当前行的高度
lineHeight = Math.max(lineHeight,childMeasuredHeight)
//最后一个元素需要特殊处理一下
if(index == childCount - 1){
allLines.add(lineViews)
lineHeights.add(lineHeight)
parentNeededHeight += lineHeight + mVerticalSpacing
parentNeededWidth = Math.max(parentNeededWidth,lineWidthUsed+mHorizontalSpacing)
}
}
}
//得到当前viewGroup的mode和size
var widthMode = MeasureSpec.getMode(widthMeasureSpec)
var heightMode = MeasureSpec.getMode(heightMeasureSpec)
//如果mode是EXACTLY,则用自己的size,否则用上面计算出的parentNeededWidth或者parentNeededHeight
var realWidth = if(widthMode == MeasureSpec.EXACTLY) selfWidth else parentNeededWidth
var realHeight = if(heightMode == MeasureSpec.EXACTLY) selfHeight else parentNeededHeight
//最终确定我自己的width和height
setMeasuredDimension(realWidth,realHeight)
}
```
然后再看下onLayout方法:
```kotlin
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
//获取所有根据onMeasure方法计算出的元素集合
val lineCount = allLines.size
var curL = paddingLeft
var curT = paddingTop
//通过两个for循环,拿出对于行和列的数据,然后布局
for(index in 0 until lineCount){
//获得没一行的数据
val lineViews = allLines[index]
//获得每一行的高
val lineHeight = lineHeights[index]
//获得每一行内的元素
for(i in 0 until lineViews.size){
val view = lineViews[i]
//左边和上面的位置
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
//重置新一行的左边 == paddingLeft
curL = paddingLeft
}
}
```
以上就是用kotlin改写后的流式布局的代码,附上源码https://github.com/qichanna/FlowLayout。
一方面复习下自定义view方面的知识,另一方面也加强kotlin的使用,当然自定义view还有许多相关的内容,具体后面再写吧,这篇文章就简单的写一下。
自定义View小析