一、自定义View的分类
1.继承View重写onDraw方法
这种方法主要用于实现一些不规则的效果,即这种效果不方便通过布局的组合方式来达到,往往需要静态或者动态德显示一些不规则的图形。很显然这需要通过绘制的方式来实现,即重写onDraw方法。采用这种方式需要自己支持wrap_content,并padding也需要自己处理。
2.继承ViewGroup派生特殊的Layout
这种方法主要用于实现自定义的布局,即除了LinearLayout、RelativeLayout、FrameLayout这几种系统的布局之外,我们重新定义一种新布局,当某种效果看起来像几种View组合在一起的时候,可以采用这种方法实现。采用这种方式稍微复杂一些,需要合适地处理ViewGroup的测量、布局这两个过程,并同时处理子元素的测量和布局过程。
3.继承特定的View(比如TextView)
这种方法比较常见,一般用于扩展某种已有的View的功能。这种方法不需要自己支持wrap_content和padding。
4.继承特定的ViewGroup(比如LinearLayout)
这种方法也比较常见,当某种效果看起来像几种View组合在一起的时候,可以采用这种方法来实现。采用这种方法不需要自己处理ViewGroup的测量和布局这两个过程。第二种方法更接近View的底层。
二、自定义View的实现
接下来只实现继承View和ViewGroup这两种情况
1.继承View重写onDraw方法
这种方式下要注意的是处理wrap_content和padding
public class CircleView extends View {
private int mColor = Color.RED;
private Paint mPaint;
public CircleView(Context context) {
this(context,null);
}
public CircleView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public CircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
mColor = a.getColor(R.styleable.CircleView_circle_color, Color.RED);
a.recycle();
init();
}
private void init() {
mPaint = new Paint();
mPaint.setColor(mColor);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setAntiAlias(true);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
//指定默认的宽高值
int mWidth = 200;
int mHeight = 200;
//处理宽、高或宽和高为wrap_content的情况
if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST){
setMeasuredDimension(mWidth,mHeight);
}else if (widthMode == MeasureSpec.AT_MOST){
setMeasuredDimension(mWidth,heightMode);
}else if (heightMode == MeasureSpec.AT_MOST){
setMeasuredDimension(widthSize,mHeight);
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//处理又padding值的情况
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
int width = getWidth()-paddingLeft-paddingRight;
int height = getHeight()-paddingTop-paddingBottom;
int radius = Math.min(width,height)/2;
canvas.drawCircle(paddingLeft+width/2,paddingTop+height/2,radius,mPaint);
}
}
除此之外,我们经常提供自定义的属性,接下来介绍一下自定义属性的使用过程。
第一步,在values目录下创建自定义属性的XML,比如attrs.xml,也可以选择类似于attrs_aaa_bbb.xml这样以attrs_开头的文件名称。当然这个名称不受限制,也可以随便取,但为了阅读性好,还是按一定规则取文件名。文件内容如下:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CircleView">
<attr name="circle_color" format="color"/>
</declare-styleable>
</resources>
在这个XML文件中声明了一个CircleView的自定义属性集合,在这个集合里可以有很多属性,这里只定义了一个格式为color的属性circle_color。除了color还有其他格式,比如reference是指资源id,dimension是指尺寸。
第二步,在View的构造方法中解析自定义属性的值,并做相应处理。
public CircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//加载自定义属性集合CircleView
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
//解析CircleView属性集合中的circle_color这个属性,它的id是R.styleable.CircleView_circle_color,并设置默认值为红色
mColor = a.getColor(R.styleable.CircleView_circle_color, Color.RED);
//通过recycle方法来实现资源
a.recycle();
init();
}
第三步,在布局文件中使用自定义属性。要注意的是在使用时要添加schemas声明:
xmlns:app="http://schemas.android.com/apk/res-auto"。使用时用app作为自定义属性的前缀。
2.继承ViewGroup派生特殊的Layout
这种方式一般用于实现一个自定义的布局,除了要处理ViewGroup的测量和布局过程以及子View的测量和布局过程,如果有滑动情况还要处理滑动冲突。这里我们重点了解一下measure和layout的过程,其他不作考虑。
public class HorizontalScrollViewEx extends ViewGroup {
public HorizontalScrollViewEx(Context context) {
this(context,null);
}
public HorizontalScrollViewEx(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public HorizontalScrollViewEx(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measureWidth = 0;
int measureHeight = 0;
int childCount = getChildCount();
// ViewGroup要先测量孩子
measureChildren(widthMeasureSpec,heightMeasureSpec);
//在测量和计算自己
int widthSpaceMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpaceMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec);
if (childCount == 0){
//如果没有孩子
setMeasuredDimension(0,0);
}else if (widthMeasureSpec == MeasureSpec.AT_MOST && heightMeasureSpec == MeasureSpec.AT_MOST){
//这里假设所有的子View的宽都是一致的
View childView = getChildAt(0);
measureWidth = childView.getMeasuredWidth()*childCount;
measureHeight = childView.getMeasuredHeight();
}else if (widthMeasureSpec == MeasureSpec.AT_MOST){
View childView = getChildAt(0);
measureWidth = childView.getMeasuredWidth()*childCount;
measureHeight = heightSpaceSize;
}else if (heightMeasureSpec == MeasureSpec.AT_MOST){
View childView = getChildAt(0);
measureHeight = childView.getMeasuredHeight();
measureWidth = widthSpaceSize;
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childLeft = 0;//以左侧为0为起点
int childCount = getChildCount();
for (int i = 0 ;i< childCount;i++){
View childView = getChildAt(i);
if (childView.getVisibility() != View.GONE){
int measuredWidth = childView.getMeasuredWidth();
childView.layout(childLeft,0,childLeft+measuredWidth,childView.getMeasuredHeight());
childLeft += measuredWidth;
}
}
}
}
上述代码主要是做一个示例,并没有着重处理细节。在onMeasure方法中,我们要先处理子View的测量,然后测量自身,同时还要考虑是否是wrap_content的情况。但是这里并没有处理padding和margin的问题。在onLayout中,主要是遍历子View,通过调用layout方法来给每一个View定位。
参考《Android开发艺术探索》