RecyclerView 性能优化

RecyclerView 性能优化,第1张

RecyclerView 性能优化

isAntiAlias = true
textSize = [email protected]
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()
// 容器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 中对应的是标签。

可以直接使用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()

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/zaji/5716060.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存