isAntiAlias = true
textSize = this@Text.textSize
color = textColor
}
}
if (staticLayout == null) {
staticLayout = StaticLayout.Builder.obtain(text, 0, text.length, textPaint!!, textWidth)
.setAlignment(Layout.Alignment.ALIGN_NORMAL)
.setLineSpacing(spaceAdd, spcaeMult)
.setIncludePad(false)
.build()
}
}
override fun draw(canvas: Canvas?) {
staticLayout?.draw(canvas)
}
}
这样的设计符合依赖倒置原则,即上层类OneViewGroup
不依赖下层类Text
,它们都依赖一个抽象Drawalbe
。
这样一来OneViewGroup
就又符合开闭原则了,即新增可绘制类型时不需要修改OneViewGroup
类,只需要新建一个Drawable
的子类即可。
再定义一个扩展方法用于构建Text
对象:
inline fun OneViewGroup.text(init: Text.() -> Unit) =
// 构建 Text 实例并应用属性,再加入到 OneViewGroup 中
Text().apply(init).also { addDrawable(it) }
方法被定义为OneViewGroup
的扩展方法,这样的好处是只要在OneViewGroup
上下文环境中就可以轻松的构建Text
实例。
扩展方法传入的参数是一个带接收者的 lambda,它是一种特殊的 lambda,kotlin 中特有的。可以把它理解成“为接收者声明的一个匿名扩展函数”。
带接收者的 lambda 的函数体除了能访问其所在类的成员外,还能访问接收者的所有非私有成员,这个特性使它能够轻松地构建结构。
Text.() -> Unit
的接收者是Text
,意味着,可以在 lambda 函数体中轻松的设置Text
实例的属性。
再配合构造OneViewGroup
的扩展法方法:
// 在 Context 上下文中轻松地构建 OneViewGroup 实例
inline fun Context.OneViewGroup(init: OneViewGroup.() -> Unit): OneViewGroup =
return OneViewGroup(this).apply(init)
就可以用声明式的语法来构建布局了:
OneViewGroup {
layout_width = match_parent
layout_height = match_parent
text {
text = “title”
textSize = 40f
textColor = “#ffffff”
textWidth = 200
}
text {
text = “content”
textSize = 30f
textColor = “#ffffff”
textWidth = 300
}
}
上述代码会在OneViewGroup
控件的左上角绘制两行文字,不过这两行文字是重叠在一起的,因为还没有指定他们的相对位置。
文字相对布局
staticLayout.draw(canvas)
并没有提供绘制坐标的参数。所以只能通过平移画布来实现在不同位置绘制文字:
class Text : Drawable {
var left: Float = 0f
var right: Float = 0f
override fun draw(canvas: Canvas?) {
canvas?.save() // 记忆当前画布位置
canvas?.translate(left, top) // 平移画布到绘制点(left, top)
staticLayout?.draw(canvas) // 绘制文字
canvas?.restore() // 还原当初画布位置
}
}
然后就可以像这样指定文字的绝对位置:
OneViewGroup {
layout_width = match_parent
layout_height = match_parent
text {
text = “title”
textSize = 40f
textColor = “#ffffff”
textWidth = 200
left = 10 // 距离父控件左边 10 像素
top = 20 // 距离父控件顶部 20 像素
}
text {
text = “content”
textSize = 30f
textColor = “#ffffff”
textWidth = 300
left = 10 // 距离父控件左边 10 像素
top = 50 // 距离父控件顶部 50 像素
}
}
用绝对像素值显然不能满足实际项目的要求。像素布局无法解决多屏幕适配的问题,用相对于父控件的绝对位置来布局也不能满足子控件间相对布局的需求。
还记得在RecyclerView 性能优化 | 把加载表项耗时减半 (一)中介绍的PercentLayout
吗?,它是一个自定义ViewGroup
,其中的子控件有一组相对属性来指定相对位置。将这套相对布局方法移植过来。
相对属性不是Text
独有的,应该将它们上提到Drawable
中:
abstract class Drawable {
// 用 Int 值作为唯一标识
var id: Int = -1
// 距离父控件左上角的百分比值
var leftPercent: Float = -1f
var topPercent: Float = -1f
// 相对布局属性
var startToStartOf: Int = -1
var startToEndOf: Int = -1
var endToEndOf: Int = -1
var endToStartOf: Int = -1
var topToTopOf: Int = -1
var topToBottomOf: Int = -1
var bottomToTopOf: Int = -1
var bottomToBottomOf: Int = -1
var centerHorizontalOf: Int = -1
var centerVerticalOf: Int = -1
// 业务层指定的宽高
var width = 0
var height = 0
// 上下左右边距
var topMargin = 0
var bottomMargin = 0
var leftMargin = 0
var rightMargin = 0
// 用于保存测量宽高结果的变量
var measuredWidth = 0
var measuredHeight = 0
// 上下左右用于描述可绘制对象所处矩形
var left = 0
var right = 0
var top = 0
var bottom = 0
// 上下左右内边距
var paddingStart = 0
var paddingEnd = 0
var paddingTop = 0
var paddingBottom = 0
// 如何测量及绘制由子类定义
abstract fun measure(widthMeasureSpec: Int, heightMeasureSpec: Int)
abstract fun draw(canvas: Canvas?)
// 布局的结果保存在上下左右四个变量组成的矩形中
fun setRect(left: Int, top: Int, right: Int, bottom: Int) {
this.left = left
this.right = right
this.top = top
this.bottom = bottom
}
// 测量的结果保存在 measuredWidth 和 measuredHeight
fun setDimension(width: Int, height: Int) {
this.measuredWidth = width
this.measuredHeight = height
}
}
为Drawable
新增了很多属性,用于描述它的尺寸及相对位置。还新增了两个方法用于保存测量和布局的结果。因为同时存在抽象和非抽象方法,就把原先的接口重构成了抽象类。
然后重写onLayout()
以定位所有Drawable
对象相对于父控件的位置:
class OneViewGroup