我正在参加「启航方案」
1.前语
最近的版别呢,产品更新了一个直播间的需求,本来直播间的公屏谈天内容根本只展现粉丝等级、会员等级等一两个根本的标签,新的版别呢又加入了许多勋章类型的标签,需求一同展现出来(搞不懂为啥整这么多)! 差异大概就如下图所示(这些等级标签及勋章看着是不是很眼熟): 简略整理了下,大致的差异便是:
- 旧版的规划只有固定的一两个标签,然后跟上用户发送的文字等信息;
- 新版的规划要求带不固定数量的标签,少的话或许一两个,多的话标签或许还需求换行,然后跟上用户发送的文字等信息;
那么从旧版到新版需求阅历哪些修正呢,一同来温习下自界说View的进程吧。
注: 为了简化处理,一切的标签自界说View都运用图片(ImageView)代替了。
2.旧版规划的剖析
先来看下旧版是怎么处理的,旧版UI的蓝图如下: 能够看到,蓝图中标签视图(ImageView)和文本视图(TextView)是重叠在一同的,然而事实也是这样,在旧版的处理中,设置数据后需求手动丈量一切标签视图的宽度,丈量结束后让文本缩进一切标签宽度的长度即可。
那么如何做到文本缩进的作用呢?对滴,经过对SpannableString设置相应的Span即可,它支撑设置许多类型,常用的如下所示(未罗列彻底):
- ForegroundColorSpan
设置文字色彩
- BackgroudColorSpan
设置文字背景色彩
- ClickableSpan
设置点击作用
- URLSpan
设置超链接作用,点击跳转浏览器
- StrikethroughSpan
设置文字删除线作用
- UnderlineSpan
设置文字下划线作用
- ImageSpan
设置文字中插入图片的作用
- LeadingMarginSpan
设置文字缩进作用
增加文本缩进功用的伪代码则如下所示:
val marginWidth = 1000 // 设置缩进的长度(也便是一切标签丈量出来的长度)
val marginSpan = LeadingMarginSpan.Standard(marginWidth, 0)
val spannableString = SpannableString("小青龙")
spannableString.setSpan(marginSpan, 0, 0, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
textView.text = spannableString
3.新版规划的剖析
再来看下新版的UI蓝图,依据需求,多个勋章要顺序摆放下来,假如过长还要换行处理,如下所示。相比旧版固定的一两个标签来说,增加了一丢丢难度。 这个时分旧版的功用彻底无法满意咱们现在的需求了,现在的标签数量不固定,或许没有,或许多到换行,所以咱们只能经过自界说布局去搞定了。
具体要怎么做呢,再细心琢磨下,标签类的控件其实都是流式布局,一切标签依照流式布局顺序摆放即可,需求换行则处理换行,可是最终一个TextView就比较特殊了,假如也依照流式布局处理的话,如下蓝图所示: 当文本长度较短且剩余空间正好够的时分,仍是刚好能达到作用的。可是当文本长度过长的时分作用肯定是下层蓝图这样的作用,文本直接新起一行,标签后边一大段的空间就都浪费了。
所以呢,这个时分咱们就结合一下旧版的规划,将最终一行的几个标签所占的宽度计算出来,然后给TextView设置一个MarginSpan,然后从头丈量其宽度和高度,最终摆放的时分同最终一行标签的顶部和左端对齐布局即可。
4.代码完结(View版别)
剖析结束后咱们的思路就大致定下来了,先完结流式布局,针对最终一个TextView需求从头优化再处理。
4.1.流式布局的完结
流式布局的完结,或许对大家都不陌生了,这里扼要罗列下几个基础的进程(去除了margin和padding等其他杂乱的逻辑,只留下了骨干代码),首要自界说MyFlowLayout继承自ViewGroup。
4.1.1.丈量
在onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int)办法中,调用measureChild()办法,循环丈量子View,并且每次累加当时行子View的宽度lineWidth,记载当时行子View的最高高度lineHeight。因为咱们要完结流式布局,所以当下一个子View累计的宽度超出了父容器的宽度时,那么就需求进行换行处理了,如下代码中第19行的注释。此刻咱们需求记载上一行子View的所占的实际宽度width,以及高度height。
每次丈量完一行后,width要取一切行中的最大值lineWidth,height需求累加每行子View中的最高值lineHeight,以此类推,直到一切子View丈量结束,扼要代码如下所示:
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
var width = 0
var height = 0
var lineWidth = 0
var lineHeight = 0
for (i in 0 until childCount) {
val child = getChildAt(i)
measureChild(child, widthMeasureSpec, heightMeasureSpec)
val childWidth = child.measuredWidth
val childHeight = child.measuredHeight
// 换行操作
if (lineWidth + childWidth > widthSize) {
height += lineHeight
lineWidth = childWidth
lineHeight = childHeight
} else {
lineWidth += childWidth
lineHeight = lineHeight.coerceAtLeast(childHeight)
}
width = width.coerceAtLeast(lineWidth)
}
// 加上最终一行的高度
height += lineHeight
setMeasuredDimension(
if (widthMode == MeasureSpec.EXACTLY) widthSize else width,
if (heightMode == MeasureSpec.EXACTLY) heightSize else height
)
丈量的最终一步是确定父容器的巨细,当咱们调用 setMeasuredDimension() 办法时,便是在告诉父布局或容器咱们自界说布局的实际尺度。能够将这个办法类比为:一个画家完结绘画后,将画作的实际尺度奉告展览场所,以便场所为画作供给正确的展现空间。
4.1.2.布局
接下来呢就开端布局了,在onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) 函数中,对子View调用layout(int l, int t, int r, int b)函数挨个摆放现已丈量好的子View,第一个子View的方位从(left = 0, top = 0, right = 当时子view的宽度, bottom = 当时子View的高度)开端摆放,后续子View的方位便是从上一个方位的结尾开端摆放(left = 累计子View的宽度, top = 0, right = 累计子View的宽度 + 当时子view的宽度, bottom=当时子View的高度)。
以此类推,当摆放下一个子View的宽度超过父容器的宽度时,则进行换行处理,此刻left = 0,top = 上一行子View的最大高度,扼要代码如下所示:
var childLeft = 0
var childTop = 0
val width = right - left
var lineWidth = 0
var lineHeight = 0
for (i in 0 until childCount) {
val child = getChildAt(i)
val childWidth = child.measuredWidth
val childHeight = child.measuredHeight
// 换行操作
if (lineWidth + childWidth > width) {
childLeft = 0
childTop += lineHeight
lineWidth = 0
lineHeight = childHeight
}
val childRight = childLeft + childWidth
val childBottom = childTop + childHeight
child.layout(
childLeft,
childTop,
childRight,
childBottom
)
childLeft += childWidth
lineWidth += childWidth
lineHeight = lineHeight.coerceAtLeast(childHeight)
}
4.2针对需求优化流式布局
接下来便是在流式布局上的优化进程了,这里咱们只针对最终一个子View是TextView的状况,其他暂不考虑,以减少示例的杂乱程度,需求完结的蓝图如下所示:
4.2.1.丈量
所以上述优化的需求,咱们剖析后统一的处理方法便是:在丈量TextView前,先将最终一行标签的长度丈量出来lineWidth,然后给TextView设置一个MarginSpan,长度便是lineWidth,最终再丈量这个TextView。
给TextView增加MarginSpan的代码如下所示:
private var marginSpan: LeadingMarginSpan? = null
private fun addMarginSpanToTextView(textView: TextView, lineWidth: Int) {
val oldText = textView.text
val spannableString = if (oldText is SpannableString) {
oldText
} else {
SpannableString(oldText ?: "")
}
// 假如之前有设置过marginSpan的话先清除去
if (marginSpan != null) {
spannableString.removeSpan(marginSpan)
}
// 设置新的marginSpan
marginSpan = LeadingMarginSpan.Standard(lineWidth, 0)
spannableString.setSpan(marginSpan, 0, 0, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
textView.text = spannableString
}
设置完MarginSpan后的丈量代码扼要如下:
// 省掉重复代码
...
for (i in 0 until childCount) {
val child = getChildAt(i)
// 先增加一个MarginSpan
if (child is TextView) {
addMarginSpanToTextView(child, lineWidth)
}
measureChild(child, widthMeasureSpec, heightMeasureSpec)
// 省掉重复代码
...
if (child is TextView) {
lineWidth = childWidth
lineHeight = childHeight
} else {
// 换行操作
// 省掉重复代码
....
}
}
// 省掉重复代码
....
4.2.2.布局
布局的时分,前面一切的标签都正常依照流式布局摆放即可,当摆放到最终一个TextView的时分,咱们将其直接将其从最终一行标签的起点方位左端对齐,顶部对齐摆放即可,扼要代码如下所示:
// 省掉重复代码
....
for (i in 0 until childCount) {
// 省掉重复代码
....
// 假如是TextView的话直接从头开端摆放
if (child is TextView) {
childLeft = paddingLeft
} else {
// 换行操作
// 省掉重复代码
....
}
// 省掉重复代码
....
}
5.总结
经过上述进程之后咱们现已完结了一个简略的升级版的流式布局,他支撑对最终一个TextView设置MarginSpan的处理,以使得整条公屏的内容愈加紧凑。
可是呢,咱们还有许多的内容未增加支撑,例如margin、padding的处理,子View间横向距离、竖向距离的处理,每行之间子View的对齐方法处理,每列之间子View的对齐方法处理等等,相信剩余的内容难不倒你我,冲啊,去完结它。
最终的最终,View版的自界说作用完结了,Compose版的还远吗?