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 @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : View(context, attrs, defStyleAttr) { // 容器1:保存 Drawable 及其 id 的对应关系 private val drawableMap = HashMap<Int, Drawable>() // 容器2:按序保存所有 Drawable 实例 private val drawables = mutableListOf()
// 向父控件中添加 Drawable 对象,它的引用会同时存储在两种容器中 fun addDrawable(drawable: Drawable) { drawables.add(drawable) drawableMap[drawable.id] = drawable }
// 按序测量所有 Drawable override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { super.onMeasure(widthMeasureSpec, heightMeasureSpec) drawables.forEach { it.measure(widthMeasureSpec, heightMeasureSpec) } }
// 按序布局所有 Drawable override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { val parentWidth = right - left val parentHeight = bottom - top drawables.forEach { // 计算 Drawable 的 left val left = getChildLeft(it, parentWidth) // 计算 Drawable 的 top val top = getChildTop(it, parentHeight) // 确定 Drawable 上下左右四个角 it.setRect(left, top, left + it.measuredWidth, top + it.measuredHeight) } }
// 按序绘制所有 Drawable override fun onDraw(canvas: Canvas?) { super.onDraw(canvas) drawables.forEach { it.draw(canvas) } } }
现在OneViewGroup 的代码和自定义控件的代码架子一模一样,都有三个步骤,测量、布局、绘制。只不过现在的对象不是 View,而是自定义的 Drawable。
其中getChildTop() 和getChildTop() 会读取刚才定义一系列属性,并根据属性值计算出Drawable 相对于OneViewGroup 左上角的坐标:
class OneViewGroup @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : View(context, attrs, defStyleAttr) {
private fun getChildTop(drawable: Drawable, parentHeight: Int): Int { return when { // 若指定了上百分比,则可和父控件高度相乘直接得出 drawable 的 top 值 drawable.topPercent != -1f -> (parentHeight * drawable.topPercent).toInt() // 若指定了垂直对齐某 drawable drawable.centerVerticalOf != -1 -> { if (drawable.centerVerticalOf == parent_id) { (parentHeight - drawable.height) / 2 } else { (drawableMap[drawable.centerVerticalOf]?.let { it.top + (it.bottom - it.top) / 2 } ?: 0) - drawable.measuredHeight / 2 } } // 若指定了在某 drawable 下方 drawable.topToBottomOf != -1 -> { val b = if (drawable.topToBottomOf == parent_id) bottom else drawableMap[drawable.topToBottomOf]?.bottom ?: 0 (b + drawable.topMargin) } // 若指定了和某 drawable 上边对齐 drawable.topToTopOf != -1 -> { val t = if (drawable.topToTopOf == parent_id) top else drawableMap[drawable.topToTopOf]?.top ?: 0 (t + drawable.topMargin) } // 若指定了在某 drawable 上方 drawable.bottomToTopOf != -1 -> { val t = if (drawable.bottomToTopOf == parent_id) top else drawableMap[drawable.bottomToTopOf]?.top ?: 0 (t - drawable.bottomMargin) - drawable.measuredHeight } // 若指定了和某 drawable 底边对齐 drawable.bottomToBottomOf != -1 -> { val b = if (drawable.bottomToBottomOf == parent_id) bottom else drawableMap[drawable.bottomToBottomOf]?.bottom ?: 0 (b - drawable.bottomMargin) - drawable.measuredHeight } else -> 0 } }
private fun getChildLeft(drawable: Drawable, parentWidth: Int): Int { return when { // 若指定了左百分比,则可和父控件宽度相乘直接得出 drawable 的 left 值 drawable.leftPercent != -1f -> (parentWidth * drawable.leftPercent).toInt() // 若指定了水平对齐某 drawable drawable.centerHorizontalOf != -1 -> { if (drawable.centerHorizontalOf == parent_id) { (parentWidth - drawable.width) / 2 } else { (drawableMap[drawable.centerHorizontalOf]?.let { it.left + (it.right - it.left) / 2 } ?: 0) - drawable.measuredWidth / 2 } } // 若指定了在某 drawable 右边 drawable.startToEndOf != -1 -> { val r = if (drawable.startToEndOf == parent_id) right else drawableMap[drawable.startToEndOf]?.right ?: 0 (r + drawable.leftMargin) } // 若指定了和某 drawable 左边对齐 drawable.startToStartOf != -1 -> { val l = if (drawable.startToStartOf == parent_id) left else drawableMap[drawable.startToStartOf]?.left ?: 0 (l + drawable.leftMargin) } // 若指定了在某 drawable 左边 drawable.endToStartOf != -1 -> { val l = if (drawable.endToStartOf == parent_id) left else drawableMap[drawable.endToStartOf]?.left ?: 0 (l - drawable.rightMargin) - drawable.measuredWidth } // 若指定了和某 drawable 右边对齐 drawable.endToEndOf != -1 -> { val r = if (drawable.endToEndOf == parent_id) right else drawableMap[drawable.endToEndOf]?.right ?: 0 (r - drawable.rightMargin) - drawable.measuredWidth } else -> 0 } } }
getChildTop() 和getChildTop() 分类讨论了每一种相对布局的情况下,该如何计算 drawable 的 left 和 top 值。
其中被依赖的控件通过drawableMap 获取,这个 Map 结构的目的是可以根据 id 快速获取 Drawable 对象。若只有列表结构的drawables ,则需要遍历,就比较耗时。但遍历 Drawable 进行测量、布局、绘制的时候,使用的是后者,因为 Map 结构是无序的。为了确定一个 Drawable 的位置,必须将它依赖的 Drawable 先完成定位。这要求构建 Drawable 时,被依赖项必须优先定义。定义的顺序被列表结构的drawables 记录。
然后就可以像这样定义具有相对位置的文字:
OneViewGroup { layout_width = match_parent layout_height = match_parent
text { id = “title” width = 100 text = “title” textSize = 40f textColor = “#ffffff” leftPercent = 0.2f // 横向 20% topPercent = 0.2f // 纵向 20% }
text { id = “content” width = 60 text = “content” textSize = 15f textColor ="#88ffffff" topToBottomOf = “title” // 在 title 的下面 startToStartOf = “title” // 与 title 左边对齐 } }
绘制形状
已经可以绘制文字,并且也可以指定文字间的相对位置了。还有一个常见的需求就是为文字添加圆形背景。在 xml 中对应的是<shape> 标签。
可以直接使用canvas.drawRoundRect() 在绘制文字之前先绘制一个圆形矩形作为背景。
抽象出一个形状类,它包含了绘制需要的参数:
class Shape { var color: String? = null // 颜色 var radius: Float = 0f // 圆角半径 var radii: IntArray? = null // 为四个角单独指定圆角 }
Text 持有一个Shape 实例:
class Text : Drawable() { var shapePaint: Paint? = null // 文字背景画笔 var shape: Shape? = null // 文字背景 set(value) { field = value shapePaint = Paint().apply { isAntiAlias = true style = Paint.Style.FILL color = Color.parseColor(value?.color) } } override fun draw(canvas: Canvas?) { canvas?.save() // 平移画布到文字绘制的左上角, 从这个点开始绘制文字背景 canvas?.translate(left, top) // 绘制背景 drawBackground(canvas) // 继续平移画布到文字的绘制点(文字和背景的距离用 padding 表示) canvas?.translate(paddingStart, paddingTop) // 绘制文字 staticLayout?.draw(canvas) canvas?.restore() }
private fun drawBackground(canvas: Canvas?) { // 绘制背景的具体实现 shape?.let { shape -> canvas?.drawRoundRect(0f, 0f, measuredWidth, measuredHeight, shape.radius, shape.radius, shapePaint!!) } } }
为OneViewGroup 新增一个扩展方法,以便用声明式的结构来构建Shape 实例:
fun OneViewGroup.shape(init: OneViewGroup.Shape.() -> Unit): OneViewGroup.Shape = OneViewGroup.Shape().apply(init)
然后就可以像这样为文字添加背景:
OneViewGroup { layout_width = match_parent layout_height = match_parent
text { id = “title” width = 100 text = “title” textSize = 40f textColor = “#ffffff” shape = shape { color = “#ff0000” radius = 20f } } }
如果需要绘制这样的效果咋办?
即左上角和右上角是圆角,其余的不是。
drawRoundRect() 做不到这个效果,只能用canvas.drawPath() 。
抽象一个Corners 类来表示四个角的圆角程度:
class Corners( var leftTopRx: Float = 0f, var leftTopRy: Float = 0f, var leftBottomRx: Float = 0f, var LeftBottomRy: Float = 0f, var rightTopRx: Float = 0f, var rightTopRy: Float = 0f, var rightBottomRx: Float = 0f, var rightBottomRy: Float = 0f ) { // 将 8 个表示圆角的属性,按照 Android api 需要的顺序组织成数组 val radii: FloatArray get() = floatArrayOf( leftTopRx, leftTopRy, rightTopRx, rightTopRy, rightBottomRx, rightBottomRy, leftBottomRx, LeftBottomRy ) }
其中一共有 8 个属性,分为四对分别表示左上,右上,左下,右下四个角。
Shape 会持有一个Corners 实例:
class Shape { var color: String? = null var radius: Float = 0f // 绘制的路径 internal var path: Path? = null var corners: Corners? = null set(value) { field = value // 当 corners 被赋值时构建 Path 实例 path = Path() } }
再需要改写一下Text 中绘制背景的方法:
class Text : OneViewGroup.Drawable() { var shapePaint: Paint? = null var shape: Shape? = null set(value) { field = value shapePaint = Paint().apply { isAntiAlias = true style = Paint.Style.FILL color = Color.parseColor(value?.color) } }
private fun drawBackground(canvas: Canvas?) { if (shape == null) return val _shape = shape!! // 如果设置了 radius 表示四个角都是圆角 if (_shape.radius != 0f) { canvas?.drawRoundRect(0f, 0f, measuredWidth, measuredHeight, _shape.radius, _shape.radius, shapePaint!!) } // 如果设置了 corners 表示有些角是圆角 else if (_shape.corners != null) { // 根据 radii 属性构建 path _shape.path!!.apply { addRoundRect( RectF(0f, 0f, measuredWidth, measuredHeight), _shape.corners!!.radii, Path.Direction.CCW ) } // 绘制 path canvas?.drawPath(_shape.path!!, shapePaint!!) } } }
还是借助于 Kotlin 的语法糖,让构建Corners 变得更加可读:
fun Shape.corners(init: Shape.Corners.() -> Unit): Corners = Corners().apply(init)
然后就可以像这样构建上面截图中的形状了:
OneViewGroup { layout_width = match_parent layout_height = match_parent
text { id = “title” width = 100 text = “title” textSize = 40f textColor = “#000000” shape = shape { color = “#ffffff” corners = corners{ leftTopRx = 30f leftTopRy = 30f rightTopRx = 30f rightTopRy = 30f } } } }
绘制图片
图片的加载就要复杂很多。如何异步获取图片?如何绘制图片?即使解决了这两个问题,如果没有办法做到局部刷新,那当图片显示时,布局中的文字也会跟着闪一下。(欢迎有思路的小伙伴留言)
又没看过ImageView 的源码,自己也很难较好地处理这些问题。那就先退一步,图片依然采用ImageView 控件展示。但这样的话就产生了一个新的问题:如何确定 ImageView 控件和 OneViewGroup 控件中绘制文字的相对位置?
控件与控件之间的相对位置很好确定,但如何确定一个控件和另一个控件中绘制内容的相对位置?
OneViewGroup 中的绘制内容被抽象为一个Drawable 对象,该对象用一组属性来标识和另一个Drawable 对象的相对位置。如果ImageView 也是一个Drawable 对象,那就能很方便的确定它和绘制文字的相对位置了!
怎么把一个类装扮成另一个类?—— 多重继承
但 Kotlin 不支持多重继承,所以只能把抽象类Drawable 重构成接口:
interface Drawable { // 测量后的宽高 var layoutMeasuredWidth: Int var layoutMeasuredHeight: Int // 布局后的上下左右边框 var layoutLeft: Int var layoutRight: Int var layoutTop: Int var layoutBottom: Int // 唯一标识 id var layoutId: Int // 相对布局属性 var leftPercent: Float var topPercent: Float var startToStartOf: Int var startToEndOf: Int var endToEndOf: Int var endToStartOf: Int var topToTopOf: Int var topToBottomOf: Int var bottomToTopOf: Int var bottomToBottomOf: Int var centerHorizontalOf: Int var centerVerticalOf: Int // 记录业务层设置的宽高 var layoutWidth: Int var layoutHeight: Int // 内边距 var layoutPaddingStart: Int var layoutPaddingEnd: Int var layoutPaddingTop: Int var layoutPaddingBottom: Int // 外边距 var layoutTopMargin: Int var layoutBottomMargin: Int var layoutLeftMargin: Int var layoutRightMargin: Int // 布局的终点:确定上下左右 fun setRect(left: Int, top: Int, right: Int, bottom: Int) { this.layoutLeft = left this.layoutRight = right this.layoutTop = top this.layoutBottom = bottom } // 测量的终点:确定宽高 fun setDimension(width: Int, height: Int) { this.layoutMeasuredWidth = width this.layoutMeasuredHeight = height } // 抽象的 测量 布局 绘制 , 供子类实现多态 fun doMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) fun doLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) fun doDraw(canvas: Canvas?) }
然后新建一个类,即继承了ImageView 又实现了Drawable 接口:
class ImageDrawable @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : AppCompatImageView(context, attrs, defStyleAttr), OneViewGroup.Drawable {
override var leftPercent: Float = -1f override var topPercent: Float = -1f override var startToStartOf: Int = -1 override var startToEndOf: Int = -1 override var endToEndOf: Int = -1 override var endToStartOf: Int = -1 override var topToTopOf: Int = -1 override var topToBottomOf: Int = -1 override var bottomToTopOf: Int = -1 override var bottomToBottomOf: Int = -1 override var centerHorizontalOf: Int = -1 override var centerVerticalOf: Int = -1 override var layoutWidth: Int = 0 override var layoutHeight: Int = 0 override var layoutMeasuredWidth: Int = 0 get() = measuredWidth override var layoutMeasuredHeight: Int = 0 get() = measuredHeight override var layoutLeft: Int = 0 get() = left override var layoutRight: Int = 0 get() = right override var layoutTop: Int = 0 get() = top override var layoutBottom: Int = 0 get() = bottom override var layoutId: Int = 0 get() = id override var layoutPaddingStart: Int = 0 get() = paddingStart override var layoutPaddingEnd: Int = 0 get() = paddingEnd override var layoutPaddingTop: Int = 0 get() = paddingTop override var layoutPaddingBottom: Int = 0 get() = paddingBottom override var layoutTopMargin: Int = 0 get() = (layoutParams as? ViewGroup.MarginLayoutParams)?.topMargin ?: 0 override var layoutBottomMargin: Int = 0 get() = (layoutParams as? ViewGroup.MarginLayoutParams)?.bottomMargin ?: 0 override var layoutLeftMargin: Int = 0 get() = (layoutParams as? ViewGroup.MarginLayoutParams)?.leftMargin ?: 0 override var layoutRightMargin: Int = 0 get() = (layoutParams as? ViewGroup.MarginLayoutParams)?.rightMargin ?: 0
override fun doMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { // 在 View 体系中测量 }
override fun doLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { // 在 View 体系中布局 layout(left, top, right, bottom) }
override fun doDraw(canvas: Canvas?) { // 在 View 体系中绘制 } }
接口中的属性都是抽象的,在子类中如果不给它指定一个初始值,就要添加set() 和get() 方法。
ImageDrawable 的测量宽高,上下左右,内外边距的获取都委托给了View 体系中的值,并且在布局自己的时候调用了View.layout() ,以确定自己和其他Drawable 的相对位置。相对位置的计算在OneViewGroup.onLayout() 中完成:
class OneViewGroup @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : ViewGroup(context, attrs, defStyleAttr) {// 重构为 ViewGroup
private val drawables = mutableListOf()
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { val parentWidth = right - left val parentHeight = bottom - top // 依次计算每个 Drawable 的相对位置 drawables.forEach { val left = getChildLeft(it, parentWidth) val top = getChildTop(it, parentHeight) it.doLayout(changed, left, top, left + it.layoutMeasuredWidth, top + it.layoutMeasuredHeight) } } }
为了让OneViewGroup 除了容纳Drawable 之外,还能容纳View ,所以不得不将其继承自ViewGroup 。
OneViewGroup 必须得测量自己的孩子ImageDrawable ,否则孩子就没有宽高数据:
class OneViewGroup @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : ViewGroup(context, attrs, defStyleAttr) {
private val drawables = mutableListOf()
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { // 测量 ImageDrawable measureChildren(widthMeasureSpec,heightMeasureSpec) super.onMeasure(widthMeasureSpec, heightMeasureSpec) // 测量其他 Drawable drawables.forEach { it.doMeasure(widthMeasureSpec, heightMeasureSpec) } } }
用 Kotlin 语法糖构建 ImageDrawable:
inline fun OneViewGroup.image(init: ImageDrawable.() -> Unit) = ImageDrawable(context).apply(init).also { addView(it) // 添加为 OneViewGroup 的子控件 addDrawable(it) // 添加为 OneViewGroup 的子 Drawable }
就像多重继承的语义一样,ImageDrawable 有双重身份,它既是OneViewGroup 的子控件,又是OneViewGroup 的子Drawable 。
ImageDrawable 的测量、布局、绘制都依赖于 View 体系,唯独布局的参数依赖于其他的Drawable 。
然后就可以像这样图文混排了:
OneViewGroup { layout_width = match_parent layout_height = match_parent
text { id = “title” width = 100 text = “title” textSize = 40f textColor = “#000000” }
image { id = “avatar” layout_width = 40 layout_height = 40 scaleType = fit_xy startToEndOf = “title” // 位于 title 的后面 centerVerticalOf = “title” // 和 title 垂直居中 } }
因为没有将 ImageView 去掉,所以这是一个曲线救国的方案。但从另一个角度看,这也是将OneViewGroup 和任何其他控件组合使用的通用方案。
点击事件
原先可以通过View.setOnClickListener() 分别为子控件设置点击事件。OneViewGroup 把子控件抽象为Drawable 后该如何处理点击事件?
更好的 RecyclerView 表项子控件点击监听器中提到一种解决方案,即判断触点坐标是否和子控件有交集。可以沿用到OneViewGroup 上:
先为Drawable 新增一个表示其矩形区域的属性rect :
interface Drawable { var layoutLeft: Int var layoutRight: Int var layoutTop: Int var layoutBottom: Int // 用上下左右构建矩形对象 val rect: Rect get() = Rect(layoutLeft, layoutTop, layoutRight, layoutBottom) … }
再在OneViewGroup 中拦截触摸事件:
class OneViewGroup @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : ViewGroup(context, attrs, defStyleAttr) {
private val drawables = mutableListOf() = “title” // 和 title 垂直居中 } }
因为没有将 ImageView 去掉,所以这是一个曲线救国的方案。但从另一个角度看,这也是将OneViewGroup 和任何其他控件组合使用的通用方案。
点击事件
原先可以通过View.setOnClickListener() 分别为子控件设置点击事件。OneViewGroup 把子控件抽象为Drawable 后该如何处理点击事件?
更好的 RecyclerView 表项子控件点击监听器中提到一种解决方案,即判断触点坐标是否和子控件有交集。可以沿用到OneViewGroup 上:
先为Drawable 新增一个表示其矩形区域的属性rect :
interface Drawable { var layoutLeft: Int var layoutRight: Int var layoutTop: Int var layoutBottom: Int // 用上下左右构建矩形对象 val rect: Rect get() = Rect(layoutLeft, layoutTop, layoutRight, layoutBottom) … }
再在OneViewGroup 中拦截触摸事件:
class OneViewGroup @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : ViewGroup(context, attrs, defStyleAttr) {
private val drawables = mutableListOf()
|