布景
前文完成了如下示例图的布景制作,如果没有看过前文的建议先看前文了解一下布景。《被逼内卷之 Android 自定义View进阶(一)》 本文的目标是完成右边文本的排版作用。文本的排版作用需类似 FlexboxLayout 的作用,只不过 FlexboxLayout 是根据 View 宽度来判别 N 个 View 摆放到同一行还是换行,而咱们需求完成的是判别一个 View 能否摆放到一段文本的最终一行,如果能够则放在其最终一行,否则换行摆放。
整体思路
当然完成办法是多种多样的。本次的完成办法是右边的文本整体放入一个不行翻滚的 RecyclerView 中,每一个小点是 RecyclerView 的一个 Item。这个 Item 的最外层布局咱们选用自定义 ViewGroup 的办法进行完成。文本的丈量以及制作放在自定义的 ViewGroup 的中,而另一个可点击的按钮由布局文件中自定义传入。
文本的丈量制作
文本的制作咱们其实也能够有很多种选择,比如咱们能够经过 Paint.breakText() 去精准控制每一行的宽度,然后经过一行一行的 drawText() 去制作。不过这个一般是用于做图文混排的“大招”,往常不容易运用的。从时间本钱方面考虑,最符合咱们此次需求的是 StaticLayout。部分同学或许对这个类比较陌生,可是其实这个类咱们间接的用过很多次了,因为 TextView 的文本丈量制作实际上就是凭借这个 StaticLayout 完成的。凭借 StatictLayout 的功用,咱们能够很容易的完成一个“丐版” TextView。
- 将文本传入 StaticLayout 后,经过 StaticLayout.getHeight(),能够很便利的丈量出这个文本的高度
- 经过 StaticLayout.getLineWidth() 咱们也能够很便利的知道最终一行的高度
- 需求制作的时分,之后直接运用 StaticLayout.draw(Canvas) ,直接制作出传入的文本,关于咱们此次的需求来说几乎不要太好用。
可点击按钮摆放思路
一般情况下自定义 ViewGroup 和一般的自定义 View 有一些区别。
- 如果是一般的自定义 View,丈量的时分只需求考虑自己就行了,自定义 ViewGroup 还需求考虑其子View的宽度和高度来决议自己宽高。
- 自定义 ViewGroup 需求重写 onLayout() 来“手动”摆放子 View 的位置。
所以自定义 ViewGroup 要麻烦一点,可是也给咱们供给了满意的灵敏度来完成咱们的需求。咱们能够在 onMeasure() 中构建 StaticLayout ,然后根据其供给的高度,加上其最终一行的宽度,以及子 View 的宽度知道这个子 View 是应该摆放在其最终一行的后边还是其下一行。为了便利起见,咱们这个自定义 View 仅答应最多一个子 View。
代码完成
丈量
丈量是咱们完成此功用的关键步骤。
// 辅佐文本丈量制作用的 StaticLayout
private lateinit var staticLayout: StaticLayout
// 可点击的按钮是否放到文本的下一行
private var hasExtraLine = false
// 制作文本用的paint
private val textPaint = TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
textSize = 13.sp
}
// 文本
var text: CharSequence = ""
set(value) {
if (field != value) {
field = value
requestLayout()
}
}
// 丈量的详细完成
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
// 因为本次需求丈量办法只或许是固定的宽度,所以直接获取其宽度作为本控件的宽度
val width = MeasureSpec.getSize(widthMeasureSpec)
// 构建StaicLayout目标
staticLayout = StaticLayout.Builder
.obtain(text, 0, text.length, textPaint, width)
.build()
if (childCount > 0) {
// 最终一行宽度
val lastLineWidth = staticLayout.getLineWidth(staticLayout.lineCount - 1)
val childView = getChildAt(0)
if (childView.visibility == View.GONE) {
// 子 View 不行见,按照无子View处理
hasExtraLine = false
setMeasuredDimension(
width + paddingStart + paddingEnd,
staticLayout.height + paddingTop + paddingBottom
)
return
}
// 丈量子 View,直接按照整个父 View 的宽高去丈量
measureChildWithMargins(childView, widthMeasureSpec, 0, heightMeasureSpec, 0)
val childWidth = childView.measuredWidth
val childViewLp = childView.layoutParams as MarginLayoutParams
if (lastLineWidth + childWidth + childViewLp.marginStart + childView.marginEnd > width) {
// 无法摆放到同一行,所需高度则为 StaticLayout 的高度 + 可点击View的高度 + Padding
hasExtraLine = true
setMeasuredDimension(
width + paddingStart + paddingEnd,
staticLayout.height + paddingTop + paddingBottom + childView.measuredHeight
+ childViewLp.topMargin + childViewLp.bottomMargin
)
} else {
// 能够摆放到同一行,所需高度则为 StaticLayout 的高度 + Padding
hasExtraLine = false
setMeasuredDimension(
width + paddingStart + paddingEnd,
staticLayout.height + paddingTop + paddingBottom
)
}
} else {
// 无子 View
hasExtraLine = false
setMeasuredDimension(
width + paddingStart + paddingEnd,
staticLayout.height + paddingTop + paddingBottom
)
}
}
上面的代码其实并不是非常严谨,比如没有判别可点击的View是否有或许比最终一行高,可是已经能够满意此次需求了,因为在规划图上,可点击的按钮的文本要比前面的文本小一点的。
布局
其次咱们需求在 onLayout() 中摆放那个可点击的View。
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
if (childCount <= 0 || getChildAt(0).visibility == View.GONE) {
return
}
val childView = getChildAt(0)
if (hasExtraLine) {
// 有多余的一行,childView在StaticLayout的下面
val childViewLp = childView.layoutParams as MarginLayoutParams
childView.layout(
paddingStart + childViewLp.marginStart,
staticLayout.height + childViewLp.topMargin,
childView.measuredWidth + paddingStart + childViewLp.marginStart,
staticLayout.height + childView.measuredHeight + childViewLp.topMargin
)
} else {
// 没有多余的一行,childView摆放到staticLayout的最终一行的最终
val childViewLp = childView.layoutParams as MarginLayoutParams
val childViewLeft = staticLayout.getLineWidth(staticLayout.lineCount - 1)
.roundToInt() + paddingStart + childViewLp.marginStart
val childViewTop =
staticLayout.height - childView.measuredHeight + childViewLp.topMargin
childView.layout(
childViewLeft,
childViewTop,
childViewLeft + childView.measuredWidth,
childViewTop + childView.measuredHeight
)
}
}
制作
有了 StaticLayout 的协助,制作就显得很简单了。
override fun onDraw(canvas: Canvas) {
staticLayout.draw(canvas)
}
当然也不能忘了在结构办法中调用setWillNotDraw(false),不然无法正确制作出所需作用。
其他细节
咱们的需求只需求一个子 View 就能够了,所以咱们能够在布局填充完成 onFinishInflate() 判别子 View 的个数,如果子 View 的数量大于1,则抛出反常。
override fun onFinishInflate() {
super.onFinishInflate()
if (childCount > 1) {
throw IllegalArgumentException("VipPrivilegesItemView can only have 1 children")
}
}
当然为了严谨,能够在 addView 中也判别一下子 View 个数。 因为咱们在丈量的时分运用了 MarginLayoutParams ,所以需求重写 generateLayoutParams() 办法,使其返回 MarginLayoutParams,不然子 View 的 margin 属性不会生效。
作用
自定义 ItemView 完成后,咱们来把这个放到 RecyclerView 中看下作用。 到这儿咱们的需求完成已完成99%。接下来需求做的就是把左面的文本 + 图片,以及右下角的图片在 XML 中放上去。因为没啥难度,这儿就不贴代码了。完整代码已放到 github。