一.什么是自定义View
自定义view可以分为三类:
1.把系统内置的控件组合起来生成一个新的控件;
2.继承系统现有的控件,然后加入新的功能;
3.自己绘制控件,继承系统的View类,通过View中的回调方法实现绘制。
本文所说的自定义View就是第三种方式。
二.为什么使用自定义View
自定义View可以实现系统View满足不了的需求,根据我们开发中不同的需求去实习我们自己的View。通过自定义View我们还可以实现一些炫酷的效果,提升产品的体验。
三.自定义View的2个关键方法
在学习自定义View之前我们先了解几个自定义View的函数,这样有助于我们更好的理解自定义View。
1.前言
在介绍两个方法之前我们先看一下MeasureSpec这个类
MeasureSpec从字面意思上理解为“测量规格”,它决定了一个View的尺寸。
MeasureSpec是View源码中的一个静态内部类。
getMode(int measureSpec)
和getSize(int measureSpec)
方法的参数mesaureSpec是一个32位的int值,前两位为View的测量模式值,后30位为View的测量尺寸。
getMode(int measureSpec)
方法是获取View的测量模式。
getSize(int measureSpec)
方法是获取View测量的尺寸。
源码如下:
public static class MeasureSpec {
//需要移动的位数
private static final int MODE_SHIFT = 30;
//相当于把0x3左移30位
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
//View的测量模式
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
//View的测量模式
public static final int EXACTLY = 1 << MODE_SHIFT;
//View的测量模式
public static final int AT_MOST = 2 << MODE_SHIFT;
//把size和mode合成一个32为的int
public static int makeMeasureSpec(int size, int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
//获取View的测量模式
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
//获取测量的尺寸
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
}
从源码中可以看出测量模式有三种类型
测量模式 | 表示的意思 |
---|---|
UNSPECIFIED | 父容器没有对当前View有任何限制,要多大就多大,这种情况一般用于系统内部,表示一种测量状态 |
EXACTLY | 父容器已经检查出View所需要的精确大小,这时候View的最终大小就是getSize 中返回的值 |
AT_MOST | 父容器制定了一个View可用的大小,但View大小不能大于这个值 |
测量模式跟布局时用到的
wrap_content
、match_parent
以及固定的尺寸的对应关系如下:
EXACTLY对应为:match_parent
和固定尺寸
AT_MOST对应为:wrap_content
前面说了这么多,下面我们看一下onMeasure
的用处吧:
2.onMeasure方法
该方法的作用是测量当前View的在屏幕上占用的尺寸,我们看一下View中的源码:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//此方法是设置View的测量值
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}```
其中```setMeasuredDimension ```方法是设置View测量的值获取View值的方法是```getDefaultSize ``` ,我们再看一下它的源码:
```java
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;
}
从
getDefaultSize
方法中可用看出View的宽高都由specSize决定,可以得出:
在View中使用wrap_content
就相当于是使用match_parent
,所以直接继承系统View的类的控件需要重写onMeasure
。
2.onDraw方法
这个方法就比较好理解了,它是把已经测量好的View画在屏幕上,开发者根据自己的需求绘制不同的功能。
onDraw
方法是通过View的draw
方法调用的,分析源码可以看出draw
方法绘制过程一般分为以下几步:
- 绘制背景
background.draw(canvas)
- 绘制自己,调用
onDraw
方法 - 绘制childern,调用
dispatchDrae
方法 - 绘制装饰 ,
onDrawScrollBars
方法
源码如下:
public void draw(Canvas canvas) {
if (ViewDebug.TRACE_HIERARCHY) {
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.DRAW);
}
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & DIRTY_MASK) == DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~DIRTY_MASK) | DRAWN;
// 第一步 绘制背景
int saveCount;
if (!dirtyOpaque) {
final Drawable background = mBGDrawable;
if (background != null) {
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if (mBackgroundSizeChanged) {
background.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
mBackgroundSizeChanged = false;
}
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
}
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
//第二步 绘制自己
if (!dirtyOpaque) onDraw(canvas);
//第三步 绘制childern
dispatchDraw(canvas);
//第四步 绘制装饰
onDrawScrollBars(canvas);
//以下省略...
return;
}
}
下面我们就来写一个自定义的View吧:
四.自定义View的流程
1.自定义View的属性(非必需)
2.在View的构造方法中获得我们自定义的属性(非必需)
3.重写onMesure
4.重写onDraw
以一个简单的圆形视图RoundView的控件为例吧。
第一步
给这个视图设置个自定义的属性,在values
目录下,创建一个名为attrs_round_view.xml
的文件,文件内容如下:
第二步
在自定义RoundView的构造函数中获取color属性:
//画圆形的画笔
private Paint mPaint;
//初始化画笔
private void initPaint() {
mPaint = new Paint();
//设置画笔的颜色 默认为黑色
mPaint.setColor(mColor);
}
public RoundView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//他们两个是相同的
//TypedArray array=context.getTheme().obtainStyledAttributes(attrs,R.styleable.RecyclerView,defStyleAttr,0);
//获取RoundView中的所有自定义属性
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.RoundView);
//获取颜色的属性
mColor = array.getColor(R.styleable.RoundView_round_color, Color.BLACK);
//初始化画笔
initPaint();
//释放TypedArray
array.recycle();
}
第三步
重写onMeasure,计算View的布局。设置布局的默认大小为200
//视图的默认大小
private final static int DEFAULT_SIZE = 200;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//获取测量之后的宽度
int width = measureDimension(widthMeasureSpec);
//获取测量之后的高度
int height = measureDimension(heightMeasureSpec);
//设置测量之后的大小
setMeasuredDimension(width, height);
}
/**
* 获取计算之后的值
* @param measureSpec
* @return
*/
private int measureDimension(int measureSpec) {
//获取测量方式
int specMode = MeasureSpec.getMode(measureSpec);
//获取测量大小
int specSize = MeasureSpec.getSize(measureSpec);
//设置默认大小
int result = DEFAULT_SIZE;
//如果是wrap_content就选取最小的值作为最后测量的大小
if (specMode == MeasureSpec.AT_MOST) {
result = Math.min(DEFAULT_SIZE, specSize);
//如果是match_parent或者是固定大小就返回测量的大小
} else if (specMode == MeasureSpec.EXACTLY) {
result = specSize;
}
return result;
}
第四步
通过onDraw
方法绘制圆形,首先获取布局中设置的padding属性,再根据宽高计算出实际圆形的半径,并绘制。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//获取布局的padding大小
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
//通过获取布局的宽高和padding大小计算实际的宽高
int width = getWidth() - paddingLeft - paddingRight;
int height = getHeight() - paddingTop - paddingBottom;
//计算圆的半径
int radius = Math.min(width, height) / 2;
//以圆形的原点坐标,画出圆形
canvas.drawCircle(paddingLeft + radius, paddingTop + radius, radius, mPaint);
}
最后的效果如下图所示:
附上:RoundVIew代码