很多时候,android系统提供的控件不能满足我们的业务需求,此时,需要我们自定义一个View,来展示我们的内容。
我们先来实现一个最简单的自定义View,画一个圆
package com.test.customview;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
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);
init();
}
private void init() {
mPaint = new Paint();
mPaint.setColor(mColor);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
int radius = Math.min(width, height) / 2;
canvas.drawCircle(width / 2, height / 2, radius, mPaint);
}
}
在布局中使用它
<com.test.customview.CircleView
android:layout_width="match_parent"
android:layout_height="match_parent" />
看下效果
修改下布局,再看下效果,为了更明显看到控件大小,我们设置一个背景色
<com.test.customview.CircleView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#afafaf"/>
发现wrap_content并没有起作用,控件的高度依然是和父容器一样,这相当于match_parent
我们看下View中onMeasure的源码
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
看下getDefaultSize的实现:
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
MeasureSpec代表一个32位的int值,高两位代表SpecMode ,是测量模式,低30位代表SpecSize ,是在某种测量模式下的规格大小。
SpecMode 有三种:
- UNSPECIFIED 父容器不对View做任何限制,一般用于系统内部
- AT_MOST 父容器指定一个可用大小SpecSize ,View不能大于这个值,对应LayoutParams中的wrap_content
- EXACTLY 父容器已经测出View所需的精确大小,View的大小就是SpecSize ,对应LayoutParams的match_parent和具体数值这两中模式
可以看出,wrap_content和match_parent采用了同样的处理方式,会直接占满父容器的剩余空间。解决办法是重写onMeasure方法,并指定一个AT_MOST时的默认值
private int defaultWidth = 100;
private int defaultHeight = 100;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpaceMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpaceMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthSpaceMode == MeasureSpec.AT_MOST && heightSpaceMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(defaultWidth, defaultHeight);
} else if (widthSpaceMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(defaultWidth, heightSpaceSize);
} else if (heightSpaceMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpaceSize, defaultHeight);
}
}
看下效果
wrap_content已经生效了
现在我们设置一个padding
<com.test.customview.CircleView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#afafaf"
android:padding="10dp"/>
没有生效,处理也简单,在onDraw方法中,考虑到padding并进行相应处理即可
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
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);
}
我们现在只有红色,如果想要画其他颜色的圆需要怎么办呢,Android中允许自定义属性来达到我们的目的
首先,在res/valus目录下新建attrs.xml值文件
新建一条自定义属性,比如说圆的颜色
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CircleView">
<attr name="circle_color" format="color"/>
</declare-styleable>
</resources>
在CircleView中获取属性,在构造方法中调用
private void initAttrs(AttributeSet attrs) {
TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.CircleView);
mColor = a.getColor(R.styleable.CircleView_circle_color, Color.RED);
a.recycle();
}
在布局文件中使用
<com.test.customview.CircleView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#afafaf"
android:padding="10dp"
app:circle_color="@color/colorPrimary"/>
成功
关于自定义属性中的format可以
package com.test.customview;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
public class CircleView extends View {
private Context mContext;
private int mColor;
private Paint mPaint;
private int defaultWidth = 100;
private int defaultHeight = 100;
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);
mContext = context;
initAttrs(attrs);
init();
}
private void initAttrs(AttributeSet attrs) {
TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.CircleView);
mColor = a.getColor(R.styleable.CircleView_circle_color, Color.RED);
a.recycle();
}
private void init() {
mPaint = new Paint();
mPaint.setColor(mColor);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
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);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpaceMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpaceMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthSpaceMode == MeasureSpec.AT_MOST && heightSpaceMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(defaultWidth, defaultHeight);
} else if (widthSpaceMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(defaultWidth, heightSpaceSize);
} else if (heightSpaceMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpaceSize, defaultHeight);
}
}
}