自定义TagView

2021-04-14  本文已影响0人  王灵

自定义viewGroup的重点是如何测量子view,并根据自己的规则去计算子view的位置和尺寸
本文通过在TagView的实现中来探究自定义viewgroup的实现细节

一、最终的效果

二、实现步骤

1、创建TagView继承ViewGroup

class TagView(context: Context?, attrs: AttributeSet?) : ViewGroup(context, attrs) {
    // 创建list保存子view的坐标属性
    val childrenBounds = mutableListOf<Rect>()

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        //遍历子view,测量 计算并保存子view的属性
        for ((index, child) in children.withIndex()) {
            if (index >= childrenBounds.size) {
                childrenBounds.add(Rect())
            }
            childrenBounds[index].set(l, t, r, b)
        }
    }

    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        //循环 把属性传递给子view
        for ((index, child) in children.withIndex()) {
            var rect=childrenBounds[index]
            child.layout(rect.left, rect.top, rect.right, rect.bottom)
        }
    }
}

2、测量子view

我们需要用到一个函数

     * @param child The child to measure 
     * @param parentWidthMeasureSpec The width requirements for this view
     * @param widthUsed Extra space that has been used up by the parent
     *        horizontally (possibly by other children of the parent)
     * @param parentHeightMeasureSpec The height requirements for this view
     * @param heightUsed Extra space that has been used up by the parent
     *        vertically (possibly by other children of the parent)
     */
    protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed)

使用这个函数需要重写generateLayoutParams函数,不然会报错

    override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams {
        return MarginLayoutParams(context, attrs)
    }

如效果图,child的x在不换行的情况下是递增的,换行时清零。而y则是在换行时递增
所以需要新建两个变量lineWidthUsedheightUsed

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        var lineWidthUsed = 0 //当前行 已经使用的宽度 即下一个child开始绘制的x位置(不换行的情况下)
        var heightUsed = 0 //已经使用的高度 即下一个child开始绘制的y位置(不换行的情况下)

        //遍历子view,测量 计算并保存子view的属性
        for ((index, child) in children.withIndex()) {
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0)
            if (index >= childrenBounds.size) {
                childrenBounds.add(Rect())
            }
            childrenBounds[index].set(
                lineWidthUsed,
                heightUsed,
                lineWidthUsed + child.measuredWidth,
                heightUsed + child.measuredHeight
            )

            lineWidthUsed += child.measuredWidth
        }
    }

上面的代码,并没有换行,也就没有递增heightUsed

3、换行

换行的条件:当前行使用的宽度lineWidthUsed+child的宽度child.measuredWidth大于viewGroup的宽度MeasureSpec.getSize(widthMeasureSpec)
heightUsed该递增多少: 上一行的view中的最高高度

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        var lineWidthUsed = 0 //当前行 已经使用的宽度 即下一个child开始绘制的x位置(不换行的情况下)
        var heightUsed = 0 //已经使用的高度 即下一个child开始绘制的y位置(不换行的情况下)
        var lineMaxHeight = 0//当前行的最高高度
        //遍历子view,测量 计算并保存子view的属性
        for ((index, child) in children.withIndex()) {
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0)
            //判断换行
            if (MeasureSpec.getMode(widthMeasureSpec)!=MeasureSpec.UNSPECIFIED&&lineWidthUsed+child.measuredWidth>MeasureSpec.getSize(widthMeasureSpec)){
                lineWidthUsed=0//重置 当前行占用
                heightUsed+=lineMaxHeight// 递增高度占用
                lineMaxHeight=0//重置 当前行的最高高度
            }
            lineMaxHeight= max(lineMaxHeight,child.measuredHeight)

            if (index >= childrenBounds.size) {
                childrenBounds.add(Rect())
            }
            childrenBounds[index].set(
                lineWidthUsed,
                heightUsed,
                lineWidthUsed + child.measuredWidth,
                heightUsed + child.measuredHeight
            )

            lineWidthUsed += child.measuredWidth
        }
    }

4、计算viewGroup的尺寸

widthUsed计算出历史的最宽的宽度

var widthUsed=0//最宽的宽度
...
widthUsed= max(widthUsed,lineWidthUsed)
...
val selfWidth = widthUsed
val selfHeight = heightUsed + lineMaxHeight
setMeasuredDimension(selfWidth,selfHeight)

完整代码

TagView

class TagView(context: Context?, attrs: AttributeSet?) : ViewGroup(context, attrs) {
    // 创建list保存子view的坐标属性
    val childrenBounds = mutableListOf<Rect>()
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        var lineWidthUsed = 0 //当前行 已经使用的宽度 即下一个child开始绘制的x位置(不换行的情况下)
        var heightUsed = 0 //已经使用的高度 即下一个child开始绘制的y位置(不换行的情况下)
        var lineMaxHeight = 0//当前行的最高高度
        var widthUsed=0//最宽的宽度
        //遍历子view,测量 计算并保存子view的属性
        for ((index, child) in children.withIndex()) {
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0)
            //判断换行
            if (MeasureSpec.getMode(widthMeasureSpec)!=MeasureSpec.UNSPECIFIED&&lineWidthUsed+child.measuredWidth>MeasureSpec.getSize(widthMeasureSpec)){
                lineWidthUsed=0//重置 当前行占用
                heightUsed+=lineMaxHeight// 递增高度占用
                lineMaxHeight=0//重置 当前行的最高高度
            }
            lineMaxHeight= max(lineMaxHeight,child.measuredHeight)

            if (index >= childrenBounds.size) {
                childrenBounds.add(Rect())
            }
            childrenBounds[index].set(
                lineWidthUsed,
                heightUsed,
                lineWidthUsed + child.measuredWidth,
                heightUsed + child.measuredHeight
            )

            lineWidthUsed += child.measuredWidth
            widthUsed= max(widthUsed,lineWidthUsed)
        }
        val selfWidth = widthUsed
        val selfHeight = heightUsed + lineMaxHeight
        setMeasuredDimension(selfWidth,selfHeight)
    }

    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        //循环 把属性传递给子view
        for ((index, child) in children.withIndex()) {
            var rect = childrenBounds[index]
            child.layout(rect.left, rect.top, rect.right, rect.bottom)
        }
    }

    override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams {
        return MarginLayoutParams(context, attrs)
    }
}

ColoredTextView

private val COLORS = intArrayOf(
  Color.parseColor("#E91E63"),
  Color.parseColor("#673AB7"),
  Color.parseColor("#3F51B5"),
  Color.parseColor("#2196F3"),
  Color.parseColor("#009688"),
  Color.parseColor("#FF9800"),
  Color.parseColor("#FF5722"),
  Color.parseColor("#795548")
)
private val TEXT_SIZES = intArrayOf(16, 18, 20)
private val CORNER_RADIUS = 4.dp
private val X_PADDING = 16.dp.toInt()
private val Y_PADDING = 8.dp.toInt()

class ColoredTextView(context: Context, attrs: AttributeSet?) : AppCompatTextView(context, attrs) {
  private var paint = Paint(Paint.ANTI_ALIAS_FLAG)
  private val random = Random()

  init {
    setTextColor(Color.WHITE)
    textSize = TEXT_SIZES[random.nextInt(3)].toFloat()
    paint.color = COLORS[random.nextInt(COLORS.size)]
    setPadding(X_PADDING, Y_PADDING, X_PADDING, Y_PADDING)
  }

  override fun onDraw(canvas: Canvas) {
    canvas.drawRoundRect(0f, 0f, width.toFloat(), height.toFloat(), CORNER_RADIUS, CORNER_RADIUS, paint)
    super.onDraw(canvas)
  }

}

关于measureChildWithMargins是可以根据自己的需求自定义计算规则的

上一篇 下一篇

猜你喜欢

热点阅读