自定义View之onMeasure

Android面试技术要点汇总
2021-05-17 14:29 · 阅读时长9分钟
小课

关于自定义View,我们一般会重写onDraw方法来实现视图绘制,而很少重写onMeasure方法,但是其实onMeasure也同样重要,尤其是当自定义View是提供给别人用,因为这种情况下View可能会处于各种不同的父容器中,以及设置不同的属性,下面看看某些情况下,重写和不重写onMeasure方法的差别,本文涉及的所有代码都在文章末尾。

一、处理wrap_content

首先创建一个自定义View,代码如下

1class CustomView(context: Context, attrs: AttributeSet?) : View(context, attrs) {
2
3    private val paint: Paint = Paint()
4    private val radius:Float
5
6    init {
7        val typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomView)
8        radius =  typedArray.getDimension(R.styleable.CustomView_radius, 0f)
9        typedArray.recycle()
10    }
11
12    override fun onDraw(canvas: Canvas) {
13        super.onDraw(canvas)
14        paint.color = Color.GREEN
15        canvas.drawCircle(width / 2f, height / 2f, radius, paint)
16    }
17}

功能是可自定义半径,在View中画一个圆,我们想实现根据半径来决定View的大小,XML代码如下:

1<?xml version="1.0" encoding="utf-8"?>
2<androidx.constraintlayout.widget.ConstraintLayout 
3    xmlns:android="http://schemas.android.com/apk/res/android"
4    xmlns:app="http://schemas.android.com/apk/res-auto"
5    xmlns:tools="http://schemas.android.com/tools"
6    android:layout_width="match_parent"
7    android:layout_height="match_parent"
8    tools:context=".MainActivity">
9
10    <com.example.customview.CustomView
11        android:layout_width="wrap_content"
12        android:layout_height="wrap_content"
13        app:radius="100dp"
14        android:background="@color/black"
15        app:layout_constraintBottom_toBottomOf="parent"
16        app:layout_constraintLeft_toLeftOf="parent"
17        app:layout_constraintRight_toRightOf="parent"
18        app:layout_constraintTop_toTopOf="parent" />
19
20</androidx.constraintlayout.widget.ConstraintLayout>

根据代码预期一个是一个居中带黑色背景的圆形,是实际效果却不是这样,对比图如下。

自定义View之onMeasure

这是因为在测量宽高的时候默认并没有依据半径来决定,下面看看onMeasure的定义和默认实现。

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}

默认会调用到父类的onMeasure方法,这里的父类是View,下面是View中onMeasure的实现。

1protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
2    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
3            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
4}
5
6public static int getDefaultSize(int size, int measureSpec) {
7    int result = size;
8    int specMode = MeasureSpec.getMode(measureSpec);
9    int specSize = MeasureSpec.getSize(measureSpec);
10
11    switch (specMode) {
12    case MeasureSpec.UNSPECIFIED:
13        result = size;
14        break;
15    case MeasureSpec.AT_MOST:
16    case MeasureSpec.EXACTLY:
17        result = specSize;
18        break;
19    }
20    return result;
21}

View里面调用了getDefaultSize,在这个示例中测量模式是MeasureSpec.AT_MOST,所以最后的测量大小是父视图给的最大尺寸,自然也就充满全屏了。为了实现上述的效果,我们可以通过重写onMeasure方法,根据半径来决定测量的大小,重写后的onMeasure实现

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    val expectSize = 2 * radius.toInt()
    val width = resolveSize(expectSize, widthMeasureSpec)
    val height = resolveSize(expectSize, heightMeasureSpec)
    setMeasuredDimension(width, height)
}

修改后就实现了期望的效果。

二、处理padding

如果想在View中添加padding,通常我们会在XML中添加padding属性,XML代码如下

1<?xml version="1.0" encoding="utf-8"?>
2<androidx.constraintlayout.widget.ConstraintLayout 
3    xmlns:android="http://schemas.android.com/apk/res/android"
4    xmlns:app="http://schemas.android.com/apk/res-auto"
5    xmlns:tools="http://schemas.android.com/tools"
6    android:layout_width="match_parent"
7    android:layout_height="match_parent"
8    tools:context=".MainActivity">
9
10    <com.example.customview.CustomView
11        android:layout_width="wrap_content"
12        android:layout_height="wrap_content"
13        app:radius="100dp"
14        android:padding="10dp"
15        android:background="@color/black"
16        app:layout_constraintBottom_toBottomOf="parent"
17        app:layout_constraintLeft_toLeftOf="parent"
18        app:layout_constraintRight_toRightOf="parent"
19        app:layout_constraintTop_toTopOf="parent" />
20
21</androidx.constraintlayout.widget.ConstraintLayout>

预期黑色背景与圆形之间会有一个10dp的间距,但是运行后发现并没有达到预期,对比图如下

自定义View之onMeasure

这是因为自定义View的padding在测量的时候需要开发者自己处理,将onMeasure改成如下即可实现预期效果。

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    val expectSize = 2 * radius.toInt()
    val width = resolveSize(expectSize, widthMeasureSpec) + paddingLeft + paddingRight
    val height = resolveSize(expectSize, heightMeasureSpec) + paddingTop + paddingBottom
    setMeasuredDimension(width, height)
}

项目完整代码如下

加载中...
自定义viewonDrawonMeasureandroidview