自定义view可继承自View,也可以继承自View的一些子类,如TextView、EditView等,编写一个自定义View,首先需要实现构造方法:XXX(Context context)
是必须的,如需要在xml中引用的方式,则需要生成XXX(Context context, AttributeSet attrs)
构造方法,原因是android自带的属性如layout_width、layout_height、id、margin等需要用AttributeSet类型的签名来解析。
具体绘画过程在OnDraw(Canvas canvas)去做,canvas相当于画布,Paint相当于画笔,画笔中有个属性是ANTI_ALIAS_FLAG
,表示抗锯齿,重点关注下。如果想重绘View,Android提供了invalidate()
方法,当然这个方法必须运行在UI线程,如果需要在线程(非UI线程)中刷新View,那你就需要调用postInvalidate()
。
自定义View中如果想控制控件的大小,就需要重载onMeasure(int widthMeasureSpec, int heightMeasureSpec)
,如果没有,该控件将按照根视图的大小布局(即全屏)。实现该方法必须用到的MeasureSpec类不得不说,有三个常量,其中UNSPECIFIED
表示未指定,父容器不对子容器做任何限制,想要多大就多大;EXACTLY
表示完全的,父容器已经给你指定好了你该多大了;AT_MOST表示至多,父容器设置了个最大值,子容器不能超过此值。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int resultWidth = 0;
int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
if (modeWidth == MeasureSpec.EXACTLY) {
resultWidth = sizeWidth;
} else {
resultWidth = mBitmap.getWidth();
if (modeWidth == MeasureSpec.AT_MOST) {
resultWidth = Math.min(resultWidth, sizeWidth);
}
}
int resultHeight = 0;
int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
if (modeHeight == MeasureSpec.EXACTLY) {
resultHeight = sizeHeight;
} else {
resultHeight = mBitmap.getHeight();
if (modeHeight == MeasureSpec.AT_MOST) {
resultHeight = Math.min(resultHeight, sizeHeight);
}
}
setMeasuredDimension(resultWidth, resultHeight);
}
Android字体绘制是从Baseline处开始绘制的,Baseline往上至字符最高处的距离我们称之为ascent(上坡度),Baseline往下至字符最底处的距离我们称之为descent(下坡度),而leading(行间距)则表示上一行字符的descent到该行字符的ascent之间的距离。Baseline上方的值为负,下方的值为正。具体看图,两张图说明一切:
ViewGroup是一个View的容器,所谓容器,就是包含多个View,例如一个LinearLayout布局中包含了一个自定义的CustomView,一个Button,一个TextView,那我们就需要继承自ViewGroup了,继承此类必须实现一个方法,即onLayout(boolean changed, int l, int t, int r, int b)
,用来确定各个子View显示的位置,当然onMeasure()
方法也是必须有的,用来确定各个子View的大小。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
/*
* 如果有子元素
*/
if (getChildCount() > 0) {
// 那么对子元素进行测量
measureChildren(widthMeasureSpec, heightMeasureSpec);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
/*
* 如果有子元素
*/
if (getChildCount() > 0) {
// 声明一个临时变量存储高度倍增值
int mutilHeight = 0;
// 那么遍历子元素并对其进行定位布局
for (int i = 0; i < getChildCount(); i++) {
// 获取一个子元素
View child = getChildAt(i);
// 通知子元素进行布局
child.layout(0, mutilHeight, child.getMeasuredWidth(), child.getMeasuredHeight() + mutilHeight);
// 改变高度倍增值
mutilHeight += child.getMeasuredHeight();
}
}
}
至此,一个不完善的自定义View完成,能显示一个简单的纵向的LinearLayout布局。但是尚未考虑到子元素在定位时候受到父容器内边距的影响(即Padding)。改进方案如下面代码所示:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int parentPaddingLeft = getPaddingLeft();
int parentPaddingTop = getPaddingTop();
if (getChildCount() > 0) {
...
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
child.layout(parentPaddingLeft, parentPaddingTop, child.getMeasuredWidth() + parentPaddingLeft, child.getMeasuredHeight() + parentPaddingTop);
...
}
}
}