尽量不要重写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)
}
}