我正在参加「启航方案」

1.前语

最近的版别呢,产品更新了一个直播间的需求,本来直播间的公屏谈天内容根本只展现粉丝等级、会员等级等一两个根本的标签,新的版别呢又加入了许多勋章类型的标签,需求一同展现出来(搞不懂为啥整这么多)! 差异大概就如下图所示(这些等级标签及勋章看着是不是很眼熟):

直播间自定义公屏视图的升级之路(View版)
直播间自定义公屏视图的升级之路(View版)
简略整理了下,大致的差异便是:

  • 旧版的规划只有固定的一两个标签,然后跟上用户发送的文字等信息;
  • 新版的规划要求带不固定数量的标签,少的话或许一两个,多的话标签或许还需求换行,然后跟上用户发送的文字等信息;

那么从旧版到新版需求阅历哪些修正呢,一同来温习下自界说View的进程吧。

注: 为了简化处理,一切的标签自界说View都运用图片(ImageView)代替了。

2.旧版规划的剖析

先来看下旧版是怎么处理的,旧版UI的蓝图如下:

直播间自定义公屏视图的升级之路(View版)
能够看到,蓝图中标签视图(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蓝图,依据需求,多个勋章要顺序摆放下来,假如过长还要换行处理,如下所示。相比旧版固定的一两个标签来说,增加了一丢丢难度。

直播间自定义公屏视图的升级之路(View版)
这个时分旧版的功用彻底无法满意咱们现在的需求了,现在的标签数量不固定,或许没有,或许多到换行,所以咱们只能经过自界说布局去搞定了。

具体要怎么做呢,再细心琢磨下,标签类的控件其实都是流式布局,一切标签依照流式布局顺序摆放即可,需求换行则处理换行,可是最终一个TextView就比较特殊了,假如也依照流式布局处理的话,如下蓝图所示:

直播间自定义公屏视图的升级之路(View版)
当文本长度较短且剩余空间正好够的时分,仍是刚好能达到作用的。可是当文本长度过长的时分作用肯定是下层蓝图这样的作用,文本直接新起一行,标签后边一大段的空间就都浪费了。

所以呢,这个时分咱们就结合一下旧版的规划,将最终一行的几个标签所占的宽度计算出来,然后给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的状况,其他暂不考虑,以减少示例的杂乱程度,需求完结的蓝图如下所示:

直播间自定义公屏视图的升级之路(View版)

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版的还远吗?