1. 简介
前面分析了一大堆原理:
自定义View原理篇(1)- measure过程
自定义View原理篇(2)- layout过程
自定义View原理篇(3)- draw过程
现在来看看是如何实现自定义View:
2.自定义View的分类
自定义View
可以分为两大类,一种是自定义单一View
,另一种是自定义ViewGroup
。具体如下图所示:
类型 | 实现 | 目的 |
---|---|---|
自定义单一View | 继承系统已有View 如:TextView |
扩展已有View的功能 |
继承View | 实现一些不规则效果 | |
自定义ViewGroup | 继承系统已有ViewGroup 如:LinearLayout |
扩展已有ViewGroup的功能 组合View功能 |
继承ViewGroup | 实现自定义布局 |
本章主要介绍一下自定义单一View
,自定义ViewGroup
下一章来说明。
3.自定义单一View
自定义单一View
又分为两类,一类是继承系统已有View
,另一类是直接继承View
类,我们分开来看下。
3.1 继承系统已有View
这种方式可以去扩展系统已有View
的功能,比如要显示一个圆形的ImageView
等等,都可以通过这种方式去实现。
我们这里的例子就简单点,给一个ImageView
加一个水印:
public class WatermarkImageView extends ImageView {//继承ImageView
private Paint mPaint;
public WatermarkImageView(Context context) {
super(context);
init();
}
public WatermarkImageView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public WatermarkImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
//画笔初始化
private void init() {
mPaint = new Paint();//创建画笔
mPaint.setColor(Color.RED);// 设置画笔颜色
mPaint.setTextSize(100);//创建字体大小
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//画上水印
canvas.drawText("四月葡萄园博客", 20, getHeight() - 20, mPaint);
}
}
在布局中引用这个WatermarkImageView
,引用自定义的View
需要包名+View名:
运行程序看看效果:
简单总结一下:继承系统已有View
去扩展功能还是比较简单的,这种方法一般只需重写onDraw()
即可,同时也无需支持wrap_content
和padding
。
3.2 继承View类
继承View
类可以用来实现一些不规则效果,但是需要注意的是,这种方式不仅需要重写onDraw()
,还需要自己去支持wrap_content
和padding
,否则wrap_content
和padding
将不起效。另外,为了方便使用这个自定义View
,我们通常还会提供一些自定义的属性。
这里我们以画一个圆角矩形为例:
public class RoundRectView extends View {//继承View
private Paint mPaint;
private int mColor=Color.RED;
public RoundRectView(Context context) {
super(context);
init();
}
public RoundRectView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public RoundRectView(Context context, 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);
//获取View的宽高
int width = getWidth();
int height = getHeight();
//画圆角矩形
RectF rectF = new RectF(0, 0, width, height);
canvas.drawRoundRect(rectF, 50, 50, mPaint);
}
}
在布局中引用这个RoundRectView
:
需要注意的是,我们这里使用了wrap_content
和padding
,我们先来看看效果:
可以看到,wrap_content
没有起到效果,这里跟match_parent
一样了,至于原因,可以看下这篇文章的分析:自定义View原理篇(1)- measure过程。
同样,padding
也没有生效。
所以,我们需要自己去支持wrap_content
和padding
。
3.2.1 支持wrap_content
支持wrap_content
需重写onMeasure()
:
@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);
// 设置wrap_content的默认宽高值
// 默认宽高的设定并无固定依据,根据需要灵活设置
// 类似TextView,ImageView等针对wrap_content均在onMeasure()对设置默认宽高值有特殊处理,具体细节请自行查看
int mWidth = 400;
int mHeight = 400;
// 当测量模式是AT_MOST(即wrap_content)时设置默认值
if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, mHeight);
// 宽或高其中一个模式为AT_MOST(即wrap_content)时,设置为默认值
} else if (widthMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, heightSize);
} else if (heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSize, mHeight);
}
}
再来看看效果:
wrap_content
生效了。
3.2.2 支持padding
支持padding
需要在onDraw()
方法作相应的修改:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//获取左右上下的padding值
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
//View的宽高需要减去padding
int width = getWidth() - paddingLeft - paddingRight;
int height = getHeight() - paddingTop - paddingBottom;
//画圆角矩形
RectF rectF = new RectF(0 + paddingLeft, 0 + paddingTop, width + paddingRight, height + paddingBottom);
canvas.drawRoundRect(rectF, 50, 50, mPaint);
}
再来看看效果:
padding
也生效了。
3.2.3 自定义View属性
Android
系统的控件以android
开头的比如android:layout_width
等等,这些都是系统自带的属性。
为了方便使用自定义View
,通常我们都会加上一些自定义属性,比如:为RoundRectView
增加一个设置颜色的属性。
自定义View
属性可以分为三个步骤:
- 在
values
目录下创建自定义属性的xml
文件- 在自定义
View
的构造方法中解析自定义属性的值- 在布局文件中使用自定义属性
下面对每个步骤来进行详细的讲解:
3.2.3.1 在values目录下创建自定义属性的xml文件
在values
目录下创建attrs_round_rect_view.xml
3.2.3.2 在自定义View的构造方法中解析自定义属性的值
我们修改一下RoundRectView
的构造方法:
public RoundRectView(Context context, AttributeSet attrs) {
super(context, attrs);
// 加载自定义属性集合RoundRectView
TypedArray typedArray=context.obtainStyledAttributes(attrs,R.styleable.RoundRectView);
//解析RoundRectView属性集合的round_rect_color属性,如果没设置默认值为Color.RED
mColor=typedArray.getColor(R.styleable.RoundRectView_round_rect_color,Color.RED);
//获取资源后要及时释放
typedArray.recycle();
init();
}
3.2.3.3 在布局文件中使用自定义属性
最后,在布局中用上:
需要注意的是:使用自定义属性需要添加schemas: xmlns:app=”http://schemas.android.com/apk/res-auto”
,其中app
是我们自定义的名字,也可以使用其他名字。使用时需要加上这个名字作为额前缀,如:app:round_rect_color="#00ffff"
。
我们再来看看效果:
至此,一个功能比较完善的自定义
View
就完成了。
3.2.4 完整代码
最后贴一下完整的代码:
RoundRectView.java
:
public class RoundRectView extends View {//继承View
private Paint mPaint;
private int mColor = Color.RED;
public RoundRectView(Context context) {
super(context);
init();
}
public RoundRectView(Context context, AttributeSet attrs) {
super(context, attrs);
// 加载自定义属性集合RoundRectView
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.RoundRectView);
//解析RoundRectView属性集合的round_rect_color属性,如果没设置默认值为Color.RED
mColor = typedArray.getColor(R.styleable.RoundRectView_round_rect_color, Color.RED);
//获取资源后要及时释放
typedArray.recycle();
init();
}
public RoundRectView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mPaint = new Paint();//创建画笔
mPaint.setColor(mColor);//设置画笔颜色
}
@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);
// 设置wrap_content的默认宽高值
// 默认宽高的设定并无固定依据,根据需要灵活设置
// 类似TextView,ImageView等针对wrap_content均在onMeasure()对设置默认宽高值有特殊处理,具体细节请自行查看
int mWidth = 400;
int mHeight = 400;
// 当测量模式是AT_MOST(即wrap_content)时设置默认值
if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, mHeight);
// 宽或高其中一个模式为AT_MOST(即wrap_content)时,设置为默认值
} else if (widthMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(mWidth, heightSize);
} 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();
//View的宽高需要减去padding
int width = getWidth() - paddingLeft - paddingRight;
int height = getHeight() - paddingTop - paddingBottom;
//画圆角矩形
RectF rectF = new RectF(0 + paddingLeft, 0 + paddingTop, width + paddingRight, height + paddingBottom);
canvas.drawRoundRect(rectF, 50, 50, mPaint);
}
}
values/attrs_round_rect_view.xml
:
activity_main.xml
: