这篇文章总结一下自定义View的实际操作
一、View的绘制流程
onMeasure()->onDraw()。
onMeasure的作用: 测量本控件的大小,测量之后的大小给到父控件(父控件一定是ViewGroup)中的onLayout来使用,对当前控件的onDraw没有直接作用。
onMeasure的过程: 首先获取函数参数widthMeasureSpec和heightMeasureSpec中的size和mode,这个size并不是实际你需要的size,需要拿到mode(EXACTLY、AT_MOST、UNSPECIFIED)重新计算出你需要的size。计算完成之后,必须调用setMeasuredDimension(width, height)
设置你计算出的宽高。
默认情况下,wrap_content和match_parent的size是一样的,所以需要你重新定义wrap_content(对应AT_MOST)模式下的size。
如果你是自定义View控件,并且wrap_content和match_parent是一样的,即都是最大大小,那么就不需要重写onMeasure。
二、ViewGroup的绘制流程
onMeasure()->onLayout()->onDraw()(一般不重写)。
onLayout的作用: 用于摆放子控件,即决定子控件的位置和大小。通过调用view.layout(left,top,right,bottom)来摆放。
onLayout的过程: 如何摆放呢?首先要知道每个子控件的宽高吧?这个宽高就是通过上一步onMeasure函数计算出来的。通过view.getMeasuredWidth()或view.getMeasuredHight()获取onMeasure函数计算的宽高。然后调用view.layout(left,top,right,bottom)来摆放子控件,right一般等于left+view.getMeasuredWidth(),bottom一般等于top+view.getMeasuredHeight()。left就是view左边距离父控件左边的水平距离,top就是view顶部距离父控件顶部的垂直距离。
强调一下,onMeasure函数的作用,它用于测量子控件的大小,但并不是最终的大小,最终的大小由父控件的onLayout来决定。
举个例子,你爸妈问你中午想吃什么,你告诉你爸妈想吃海鲜,你爸妈参考了你的意见之后,最终吃什么决定权在你爸妈手上,并不是你想吃什么就吃什么。
三、需要重写的函数
View:必须重写onDraw来绘制图形,不一定要重写onMeasure,需要定义wrap_content时才需要重写onMeasure。
ViewGroup:必须重写onLayout函数,必须重写onMeasure函数,在onMeasure中必须调用measureChildren函数(否则子view布局中定义的宽高无效)。ViewGroup的onMeasure函数继承自View,ViewGroup本身没有调用过measureChildren,所以我们必须重写ViewGroup的onMeasure函数。
四、ViewGroup的onMeasure重写要点:
获取父布局给的宽高Mode和Size,最终调用setMeasureDimension(width,height)来设置它自己测量的宽高(并不是最终展示出的宽高),同时要调用measureChildren函数来测量子View的宽高。
五、ViewGroup的onLayout重写要点:
这个函数必须重写,用来定义子View的位置及宽高。需要遍历子View,依次调用子View的layout(l,t,r,b)来进行布局。其中l表示左边相对parent的最左边的距离,t表示顶部相对parent的顶部的距离,r表示最右边相对parent最左边的距离,b表示底部相对parent顶部的距离。即坐标系的原点是parent的左上角。一般r=l+getMeasureWidth(),b=t+getMeasuredHeight()。这个getMeasuredWidth()就是onMeasure函数测量的值,getMeasuredHeight同理。所以onMeasure函数测量的宽高并不是最终的宽高,View或ViewGroup位置及宽高是由父容器的onLayout函数决定的。
六、getWidth()与getMeasuredWidth()的区别
只有当前View或ViewGroup()的layout()函数被调用了之后,getWidth()(getHeigth())才能获取正确的值。
在onDraw函数中可以通过getWidth和getHeigth获取正确的值,因为当前view的onLayout函数已经调过了。
只有当前View或ViewGroup()的setMeasuredDimenstion()函数被调用了之后,getMeasuredWidth()(getMeasuredHeight())才能获取到正确的值。
一个简单的自定义文本控件的例子:
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import androidx.annotation.Nullable;
public class MyView extends View {
private int mStepTextSize;//字体大小
private int mStepTextColor;//字体颜色
private Paint mPaint;
private String mText;//文本内容
public MyView(Context context) {
super(context);
}
public MyView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MyView);
mStepTextColor = typedArray.getColor(R.styleable.MyView_textColor, mStepTextColor);
mStepTextSize = typedArray.getDimensionPixelSize(R.styleable.MyView_textSize, mStepTextSize);
mText = typedArray.getString(R.styleable.MyView_text);
mPaint = new Paint();
mPaint.setTextSize(mStepTextSize);
mPaint.setColor(mStepTextColor);
typedArray.recycle();
}
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
/**
* 只需要定义AT_MOST即wrap_content属性下的size,其它用默认值。
**/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = MeasureSpec.getSize(widthMeasureSpec);
if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.AT_MOST) {
Rect rect = new Rect();
mPaint.getTextBounds(mText, 0, mText.length(), rect);
width = rect.width();
}
int height = MeasureSpec.getSize(heightMeasureSpec);
if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST) {
Rect rect = new Rect();
mPaint.getTextBounds(mText, 0, mText.length(), rect);
height = rect.height();
}
//设置计算之后的宽高
setMeasuredDimension(width, height);
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawText(mText, 0, getHeight()-1, mPaint);
}
}