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
}
}
}
如果需要绘制这样的效果咋办?
![微信图片_20210416174604.jpg](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3aa82af00e8e436c972091e1d79bf5ee~tplv-k3u1fbpfcp-watermark.image)
即左上角和右上角是圆角,其余的不是。
`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<Drawable>()
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<Drawable>()
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<Drawable>()
// 手势监听器, 用于将触摸事件解析成点击事件
private var gestureDetector: GestureDetector? = null
// 设置 Drawable 点击监听器
fun setOnItemClickListener(onItemClickListener: (String) -> Unit) {
// 构造手势监听器
gestureDetector = GestureDetector(context, object : GestureDetector.OnGestureListener {
override fun onShowPress(e: MotionEvent?) {
}
// 当单击事件发生时
override fun onSingleTapUp(e: MotionEvent?): Boolean {
e?.let {
// 若在触摸点找到对应 Drawable 则回调点击事件
findDrawableUnder(e.x, e.y)?.let { onItemClickListener.invoke(it.layoutIdString) }
}
return true
}
// 必须返回 true 表示处理 ACTION_DOWN 事件, 否则后续事件不会传递到 OneViewGroup
override fun onDown(e: MotionEvent?): Boolean {
return true
}
override fun onFling(e1: MotionEvent?, e2: MotionEvent?, velocityX: Float, velocityY: Float): Boolean {
return false
}
override fun onScroll(e1: MotionEvent?, e2: MotionEvent?, distanceX: Float, distanceY: Float): Boolean {
return false
}
override fun onLongPress(e: MotionEvent?) {
}
})
}
// 根据坐标查找 Drawable 对象
private fun findDrawableUnder(x: Float, y: Float): Drawable? {
// 遍历所有 Drawable ,返回矩形区域内包含触点的 Drawable
drawables.forEach {
if (it.rect.contains(x.toInt(), y.toInt())) {
return it
}
}
return null
}
// 将触摸事件传递给手势监听器
override fun onTouchEvent(event: MotionEvent?): Boolean {
return gestureDetector?.onTouchEvent(event) ?: super.onTouchEvent(event)
}
}
然后就可以像这样为`OneViewGroup`设置子 Drawable 的点击事件了:
OneViewGroup { layout_width = match_parent layout_height = match_parent
text {
id = "title"
width = 100
text = "title"
textSize = 40f
textColor = "#000000"
}
setOnItemClickListener { id->
when (id) {
"title" -> {
Log.v("test", "title is clicked")
}
}
}
}
Talk is cheap, show me the code
-------------------------------
* `OneViewGroup`源码可以点击[这里]( )
* Demo 源码地址可以点击[这里]( ) (其中的`RecyclerViewPerformanceActivity`)
最后附上,文章开头截图布局用`OneViewGroup`的重构版本(经重构, 其中和 OneViewGroup 有关的属性都以 drawable 开头):
class OneRankProxy : VarietyAdapter2.Proxy<BetterRank, OneRankViewHolder>() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { val itemView = parent.context.run { LinearLayout { layout_id = “container” layout_width = match_parent layout_height = wrap_content orientation = vertical margin_start = 20 margin_end = 20 padding_bottom = 16 shape = shape { corner_radius = 20 solid_color = “#ffffff” }
OneViewGroup { // 用 OneViewGroup 重构的表头
layout_id = "one"
layout_width = match_parent
layout_height = 60
shape = shape {
corner_radii = intArrayOf(20, 20, 20, 20, 0, 0, 0, 0)
solid_color = "#ffffff"
}
text {
drawable_layout_id = "tvTitle"
drawable_max_width = 60
drawable_text_size = 16f
drawable_text_color = "#3F4658"
drawable_start_to_start_of = parent_id
drawable_left_margin = 20
topPercent = 0.23f
}
text {
drawable_layout_id = "tvRank"
drawable_max_width = 60
drawable_text_size = 11f
drawable_text_color = "#9DA4AD"
leftPercent = 0.06f
topPercent = 0.78f
}
text {
drawable_layout_id = "tvName"
drawable_max_width = 60
drawable_text_size = 11f
drawable_text_color = "#9DA4AD"
leftPercent = 0.18f
topPercent = 0.78f
}
text {
drawable_layout_id = "tvCount"
drawable_max_width = 100
drawable_text_size = 11f
drawable_text_color = "#9DA4AD"
drawable_end_to_end_of = parent_id
drawable_right_margin = 20
topPercent = 0.78f
}
}
}
}
return OneRankViewHolder(itemView)
}
override fun onBindViewHolder(holder: OneRankViewHolder, data: BetterRank, index: Int, action: ((Any?) -> Unit)?) {
holder.tvTitle?.text = data.title
holder.tvCount?.text = data.countColumn
holder.tvRank?.text = data.rankColumn
holder.tvName?.text = data.nameColumn
holder.container?.apply {
// 遍历主播数据, 动态构建每个主播的布局
data.ranks.forEachIndexed { index, rank ->
OneViewGroup {
layout_width = match_parent
layout_height = 35
background_color = "#ffffff"
text {
drawable_layout_id = "tvRank"
drawable_layout_width = 18
drawable_text_size = 14f
drawable_text_color = "#9DA4AD"
leftPercent = 0.08f
drawable_center_vertical_of = parent_id
text = rank.rank.toString()
}
image {
layout_id = "ivAvatar"
layout_width = 20
layout_height = 20
scaleType = scale_center_crop
drawable_center_vertical_of = parent_id
leftPercent = 0.15f
load(rank.avatarUrl)
}
text {
drawable_layout_id = "tvName"
drawable_max_width = 200
drawable_text_size = 11f
drawable_text_color = "#3F4658"
drawable_gravity = gravity_center
drawable_max_lines = 1
drawable_start_to_end_of = "ivAvatar"
drawable_top_to_top_of = "ivAvatar"
drawable_left_margin = 5
drawable_text = rank.name
}
text {
drawable_layout_id = "tvTag"
drawable_max_width = 100
drawable_text_size = 8f
drawable_text_color = "#ffffff"
drawable_gravity = gravity_center
drawable_padding_top = 1
drawable_padding_bottom = 1
drawable_padding_start = 2
drawable_padding_end = 2
drawable_text = "save"
drawable_shape = drawableShape {
radius = 4f
color = "#8cc8c8c8"
}
drawable_start_to_start_of = "tvName"
drawable_top_to_bottom_of = "tvName"
}
image {
layout_id = "ivLevel"
layout_width = 10
layout_height = 10
scaleType = scale_fit_xy
drawable_center_vertical_of = "tvName"
drawable_start_to_end_of = "tvName"
drawable_left_margin = 5
load(rank.levelUrl)
}
text {
drawable_layout_id = "tvLevel"
drawable_max_width = 200
drawable_text_size = 7f
drawable_text_color = "#ffffff"
drawable_gravity = gravity_center
drawable_padding_start = 2
drawable_padding_end = 2
drawable_shape = drawableShape {
color = "#FFC39E"
radius = 20f
}
drawable_center_vertical_of = "tvName"
drawable_start_to_end_of = "ivLevel"
drawable_left_margin = 5
drawable_text = rank.level.toString()
}
text {
drawable_layout_id = "tvCount"
drawable_max_width = 200
drawable_text_size = 14f
drawable_text_color ="#3F4658"
drawable_gravity = gravity_center
drawable_center_vertical_of = parent_id
drawable_end_to_end_of = parent_id
drawable_right_margin = 20
drawable_text = rank.count.formatNums()
}
}
}
}
}
}
class OneRankViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val oneViewGroup = itemView.find(“one”) val container = itemView.find(“container”) val tvTitle = oneViewGroup?.findDrawable
(“tvTitle”)
val tvRank = oneViewGroup?.findDrawable
(“tvRank”)
val tvName = oneViewGroup?.findDrawable
(“tvName”)
val tvCount = oneViewGroup?.findDrawable
(“tvCount”)
}
代码中沿用了[上一篇]( )中提到的将首屏的多个表项合并成一个表项的方案,动态地为每个主播构建表项并添加到表项容器中。
其中`OneViewGroup.findDrawable()`的作用类似于`View.findViewById()`:
**最后我还整理了很多Android中高级的PDF技术文档。以及一些大厂面试真题解析文档。**
**[CodeChina开源项目地址:https://codechina.csdn.net/m0_60958482/android_p7](https://codechina.csdn.net/m0_60958482/android_p7)**
![image](https://img-blog.csdnimg.cn/img_convert/96a6ceea8caac70c5e359488f8d0d173.png)
Android高级架构师之路很漫长,一起共勉吧!
drawable_text = rank.count.formatNums()
}
}
}
}
}
}
class OneRankViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val oneViewGroup = itemView.find<OneViewGroup>("one")
val container = itemView.find<LinearLayout>("container")
val tvTitle = oneViewGroup?.findDrawable<Text>("tvTitle")
val tvRank = oneViewGroup?.findDrawable<Text>("tvRank")
val tvName = oneViewGroup?.findDrawable<Text>("tvName")
val tvCount = oneViewGroup?.findDrawable<Text>("tvCount")
}
代码中沿用了上一篇中提到的将首屏的多个表项合并成一个表项的方案,动态地为每个主播构建表项并添加到表项容器中。
其中OneViewGroup.findDrawable() 的作用类似于View.findViewById() :
最后我还整理了很多Android中高级的PDF技术文档。以及一些大厂面试真题解析文档。
CodeChina开源项目地址:https://codechina.csdn.net/m0_60958482/android_p7
[外链图片转存中…(img-veWzb13B-1630584398582)]
Android高级架构师之路很漫长,一起共勉吧!
|