android 自定义view-测量部分不可忽视的三个要点

440 阅读3分钟

尽量不要重写layout方法

来看个例子:

class ErrorView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

    override fun layout(l: Int, t: Int, r: Int, b: Int) {
        //也可以在这里 处理 强制设置自己的位置和尺寸
        super.layout(l, t, r + 100, b + 100)

        //这个方法 结束以后 才拿到实际的宽高
    }
}

这个自定义view 我们把他放到 linearlayout里面 看看

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal">

    <com.example.testmoveviewpager.ErrorView
        android:layout_width="100dp"
        android:background="@color/teal_200"
        android:layout_height="100dp">

    </com.example.testmoveviewpager.ErrorView>

    <com.example.testmoveviewpager.ErrorView
        android:layout_width="100dp"
        android:background="@color/purple_700"
        android:layout_height="100dp">

    </com.example.testmoveviewpager.ErrorView>

</LinearLayout>

看到这个布局文件 我们应该能想到 合理的效果应该是 2个view 并排排列,宽度一样, 当然了这个宽度高度是要比 layout_width 里面配置的宽度和高度 要多出100个px的,毕竟我们在layout里面是这么设置的。 现在来看看效果:

可以很明显看出来 这个效果 和我们预期中的很不一样。 原因就是: 父view 也就是linearlayout这个viewgroup的 layout 是根据 measure的结果 来做的,我们这里 直接修改了子view的layout 而没有修改measure的逻辑,导致父view 还是根据measure的结果 去layout 自然得出来的结果 就很不符合预期了。 实际上:

layout和measure 都可以 修改view最终的宽高,但是我们百分之99的情况在自定义view的时候 是不需要修改layout这个方法的,这个方法一旦被修改 会影响到父view的工作 而且结果难以预测,我们百分之99的情况下 只要修改onMeasure 方法里面的逻辑即可

不要遗漏对wrap_content的处理

看下面这个代码

class CircleView2 @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : androidx.appcompat.widget.AppCompatImageView(context, attrs, defStyleAttr) {

    val paint = Paint().apply {
        color = Color.RED
    }

    val radius = 100f

    override fun onDraw(canvas: Canvas) {
        canvas.drawCircle(
            (width / 2).toFloat(),
            (height / 2).toFloat(),
            radius,
            paint
        )
    }

    
}

就是画一个圆,但是没有重写onMeasure 导致 他实际上 一旦属性被写成了宽高为wrap_content 在界面上就展示不出来了(当然你写成固定的dp值 还是可以展示的)

所以要进行如下改动:

class CircleView2 @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : androidx.appcompat.widget.AppCompatImageView(context, attrs, defStyleAttr) {

    val paint = Paint().apply {
        color = Color.RED
    }

    val radius = 100f

    override fun onDraw(canvas: Canvas) {
        canvas.drawCircle(
            (width / 2).toFloat(),
            (height / 2).toFloat(),
            radius,
            paint
        )
    }


    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        var width = radius * 2
        var height = radius * 2

        width = resolveSize(width.toInt(), widthMeasureSpec).toFloat()
        height= resolveSize(height.toInt(), heightMeasureSpec).toFloat()

        setMeasuredDimension(width.toInt(),height.toInt())
    }

}

其实自定义view 测量这一块 是有固定套路的, 无非就是**先确定自己想要的宽高,然后用resolveSize方法 来重新测量一下自己的宽高,这一部主要是为了 和父view沟通自己到底可以有多大,以父view 为准 ,最后调用 setMeasuredDimension 来设置最后的宽高即可 **

不要遗漏padding

很多人 写的自定义view 不好用 其实最重要的就是遗漏了padding,对padding的计算 是你这个view 是否好用的关键因素。 还是上面那个例子,假设我们设置

 <com.example.testmoveviewpager.CircleView2
        android:layout_width="wrap_content"
        android:paddingLeft="10dp"
        android:background="@color/teal_200"
        android:layout_height="wrap_content">

    </com.example.testmoveviewpager.CircleView2>

最后的结果就是:

你会看到这个paddingleft 压根就没生效吗 ,所以实际上自定义view的测量部分最难的就是 要把padding处理好

为了例子简单,我们 只处理一下paddingleft的情况。

class CircleView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : androidx.appcompat.widget.AppCompatImageView(context, attrs, defStyleAttr) {

    val paint = Paint().apply {
        color = Color.RED
    }

    val radius = 100f

    override fun onDraw(canvas: Canvas) {
        //有的padding 圆心 自然要移动一段距离
        canvas.drawCircle(
            (width / 2).toFloat() + paddingLeft / 2,
            (height / 2).toFloat(),
            radius,
            paint
        )
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        //有了padding 那这里计算自己想要的宽度的时候 就必须把padding 也算在内了
        val widthSize = paddingLeft + radius * 2
        val width = resolveSize(widthSize.toInt(), widthMeasureSpec)
        val height = resolveSize((radius * 2).toInt(), heightMeasureSpec)
        setMeasuredDimension(width, height)
    }

}