自定义View的原因
当Android系统内置的View无法实现我们的需求,我们需要针对我们的业务需求定制我们想要的View。
自定义View的分类
继承View
这种方法主要用于实现一些不规则的效果,采用这种方法需要自己支持wrap_content,并且padding也需要自己处理。这种方法通常需要重写两个方法:onMeasure()、onDraw()。
onMeasure()方法
onMeasure()方法负责对当前View的尺寸进行测量。onMeasure()方法函数原型如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
方法中的两个参数widthMeasureSpec和heightMeasureSpec是两个32位的int型数据,其中前两位表示测量模式,后30位表示具体数值,其具体的值可通过MeasureSpec类分别获取:
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
测量模式包含以下三种
测量模式 | 表达含义 |
---|---|
UNSPECIFIED | 父容器不对View有任何限制,要多大给多大 |
EXACTLY | 父容器已检测出View所需的精确大小,View的最终大小为SpecSize所指定的值 |
AT_MOST | 父容器指定了一个可用大小即SpecSize,View的大小不能大于这个值 |
EXACTLY模式即对应我们日常开发中的具体大小尺寸与match_parent尺寸,而AT_MOST尺寸对应的是wrap_content。
在重写onMeasure()方法时,要注意对wrap_content进行支持,即为View提供默认值大小,具体方法可如下编写:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = getMeasureSize(widthMeasureSpec, 200);
int height = getMeasureSize(heightMeasureSpec, 200);
setMeasuredDimension(width, height);
}
private int getMeasureSize(int measureSpec, int defaultSize) {
int mSize = 0;
int mode = MeasureSpec.getMode(measureSpec);
int size = MeasureSpec.getSize(measureSpec);
switch (mode) {
case MeasureSpec.AT_MOST:
mSize = Math.min(defaultSize, size);
break;
case MeasureSpec.EXACTLY:
mSize = size;
break;
default:
mSize = defaultSize;
break;
}
return mSize;
}
onDraw()方法
在这个方法中,我们需要把我们想要的View效果绘制出来,如下绘制一个简单的圆,在这个方法中记得对View的padding值进行处理,否则View的padding值不会生效:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
//处理View的padding值
int width = getWidth() - paddingLeft - paddingRight;
int height = getHeight() - paddingTop - paddingBottom;
int r = Math.min(width, height) / 2;
canvas.drawCircle(getWidth() / 2, getHeight() / 2, r, mPaint);
}
自定义View属性
当我们需要对我们自定义的View定义属性时,我们需要在资源文件里面添加自定义属性集合,如下:
<declare-styleable name="CircleView">
<!--声明自定义属性-->
<attr name="circle_color" format="color"/>
</declare-styleable>
然后,在自定义View的的构造方法中将属性值取出,代码如下所示:
public CircleView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
color = a.getColor(R.styleable.CircleView_circle_color, color);
a.recycle();
init();
}
完成这些工作后,我们就可以在Xml文件的自定义View中定义这些自定义属性了,代码如下:
<com.example.study.CircleView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="10dp"
app:circle_color="@color/colorAccent"/>
继承特定的View(如TextView)
这种方法一般是用于扩展某种已有的View的功能,这种方法比较容易实现,不需要自己支持wrap_content和padding等,具体操作应根据业务需求而进行。
自定义ViewGroup派生特殊的Layout
这种方法主要用于实现自定义的布局,即除了常用布局之外,我们重新定义一种新布局,当某种效果看起来像几种View组合在一起的时候,可以采用这种方法。采用这种方法稍微复杂一些,需要合适的处理ViewGroup的测量、布局这两个过程,同时处理子元素的测量与布局过程。
onMeasure()方法
这种方法需要重写onMeasure()方法测量子View的大小以及确定ViewGroup本身的大小,示例代码如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
measureChildren(widthMeasureSpec, heightMeasureSpec);
int childCount = getChildCount();
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (childCount == 0) {
//如果没有子View,则设置宽高为0
setMeasuredDimension(0, 0);
} else if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
//宽高都为包裹内容,则宽设置为所有子View的宽度之和,高设置为子View中的最大高度
setMeasuredDimension(getTotalWidth(), getMaxHeight());
} else if (widthMode == MeasureSpec.AT_MOST) {
//只有宽度为包裹内容,则设置宽度为所有子View宽度之和,高度为测量高度
setMeasuredDimension(getTotalWidth(), heightSize);
} else if (heightMode == MeasureSpec.AT_MOST) {
//只有高度为包裹内容,则设置宽度为测量宽度,高度设置为子View的最大高度
setMeasuredDimension(widthSize, getMaxHeight());
}
}
private int getTotalWidth() {
int mChildrenWidth = 0;
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View v = getChildAt(i);
mChildrenWidth += v.getMeasuredWidth();
}
return mChildrenWidth;
}
private int getMaxHeight() {
int maxHeight = 0;
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View v = getChildAt(i);
if (v.getMeasuredHeight() > maxHeight) {
maxHeight = v.getMeasuredHeight();
}
}
return maxHeight;
}
在上述方法中应该进一步的考虑自定义ViewGroup自身的padding值以及子View的margin值。
onLayout()方法
继承ViewGroup类后必须重写onLayout()抽象方法,在这个方法里面对ViewGroup内的子View按照自己的意愿进行摆放,下列代码例子是一个简单的横向排列摆放逻辑:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
int childLeft = 0;
for (int i = 0; i < childCount; i++) {
View v = getChildAt(i);
if (v.getVisibility() != View.GONE) {
//如果子view的显示状态不为GONE,则通过layout将其放置在合适的位置
int childWidth = v.getMeasuredWidth();
v.layout(childLeft, 0, childLeft + childWidth, v.getMeasuredHeight());
childLeft += childWidth;
}
}
}
onLayout()方法中也应该考虑到ViewGroup本身的padding值以及子View的margin值。
至此,自定义ViewGroup的基本逻辑就实现完善了,其余需要注意的内容,如滑动冲突、ViewGroup内部内容的滑动可自行完善。
继承特定的ViewGroup(比如LinearLayout)
这种方法也比较常见,当某种效果看起来像几种View组合在一起的时候,可以采用这种方式,采用这种方式不需要自己处理ViewGroup的测量和布局这两个过程。
参考资料:
- 《Android开发艺术探索》
- 自定义View,有这一篇就够了