Android自定义View
一、View绘制原理
View的绘制基本上由measure()、layout()、draw()三个函数完成。
1.1 测量onMeasure
测量过程中的相关方法主要有:
public final void measure(int widthMeasureSpec, int heightMeasureSpec)
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight)
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
其中,measure()中完成参数初始化后调用onMeasure(),onMeasure()中计算view的宽高,并调用setMeasuredDimension()方法保存测量结果;measure()与setMeasuredDimension()为final方法,onMeasure()在子类中需要重写。
1.1.1 关于MeasureSpec
MeasureSpec是一个View的内部类,它封装了一个view的尺寸,在onMeasure()中可以根据MeasureSpec的值具体确定view的尺寸;MeasureSpec的值保存在一个int值中,其32位中的前两位表示 MODE ,后30位表示 size 。
-
MeasureSpec中的MODE
共有三种:UNSPECIFIED、EXACTLY 和 AT_MOST;
– UPSPECIFIED :父容器对于子容器没有任何限制,子容器想要多大就多大,一般为系统内部使用;
– EXACTLY:精准模式,对应 match_parent ,父容器已经为子容器设置了尺寸,子容器应当服从这些边界,不论子容器想要多大的空间,根据MeasureSpec中size确定;
– AT_MOST:最大模式,对应 wrap_content ,子容器可以是MeasureSpec中size值大小内的任意大小。 -
MeasureSpec的使用
// 获取mode int specMode=MeasureSpec.getMode(measureSpec); // 获取size int specSize=MeasureSpec.getSize(measureSpec); //生成MeasureSpec int measureSpec1=MeasureSpec.makeMeasureSpec(size,mode);
-
根据MODE确定子view的MeasureSpec
public static int getChildMeasureSpec(int spec, int padding, int childDimension) { int specMode = MeasureSpec.getMode(spec); int specSize = MeasureSpec.getSize(spec); int size = Math.max(0, specSize - padding); int resultSize = 0; int resultMode = 0; switch (specMode) { //当父View要求一个精确值时,为子View赋值 case MeasureSpec.EXACTLY: //如果子view有自己的尺寸,则使用自己的尺寸 if (childDimension >= 0) { resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; //当子View是match_parent,将父View的大小赋值给子View } else if (childDimension == LayoutParams.MATCH_PARENT) { resultSize = size; resultMode = MeasureSpec.EXACTLY; //如果子View是wrap_content,设置子View的最大尺寸为父View } else if (childDimension == LayoutParams.WRAP_CONTENT) { resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // 父布局给子View了一个最大界限 case MeasureSpec.AT_MOST: if (childDimension >= 0) { //如果子view有自己的尺寸,则使用自己的尺寸 resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // 父View的尺寸为子View的最大尺寸 resultSize = size; resultMode = MeasureSpec.AT_MOST; } else if (childDimension == LayoutParams.WRAP_CONTENT) { //父View的尺寸为子View的最大尺寸 resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // 父布局对子View没有做任何限制 case MeasureSpec.UNSPECIFIED: if (childDimension >= 0) { //如果子view有自己的尺寸,则使用自己的尺寸 resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { //因父布局没有对子View做出限制,当子View为MATCH_PARENT时则大小为0 resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } else if (childDimension == LayoutParams.WRAP_CONTENT) { //因父布局没有对子View做出限制,当子View为WRAP_CONTENT时则大小为0 resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } break; } return MeasureSpec.makeMeasureSpec(resultSize, resultMode); }
这段代码只为子view设置了MeasureSpec参数而并未确定子view的实际大小,实际大小仍需在onMeasure()中具体设置。
- 子view的测量模式
可以看出,子view的测量模式由自身LayoutParam和父view的MeasureSpec决定:
– 父view测量模式为UNSPECIFIED:若子view有尺寸,则使用,否则为0;
– EXACTLY:父布局为精准模式,有确定大小,若子view有大小则直接使用,否则不能超出父view的大小;
– AT_MOST:父布局为最大模式,则子view若有大小则直接使用,没有大小则不得超出父view大小;
1.1.2 onMeasure()源码分析
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
-
setMeasuredDimension(int measuredWidth, int measuredHeight) :该方法用来设置View的宽高,在我们自定义View时也会经常用到;
-
getDefaultSize(int size, int measureSpec):该方法用来获取View默认的宽高,源码:
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; }
-
getSuggestedMinimumWidth():获取建议的尺寸,源码:
protected int getSuggestedMinimumWidth() { return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth()); // view无背景时默认大小为mMinWidth(通过android:minWidth设置,默认为0);有背景时默认大小为mMinWidth与背景的最小尺寸中的较大者。
}
1.2 布局layout()
设置view在屏幕中的显示位置,对于View来说用于计算其位置参数,而对于ViewGroup则需计算自身以及子view的位置。
相关方法主要有:
public void layout(int l, int t, int r, int b)
protected boolean setFrame(int left, int top, int right, int bottom)
protected void onLayout(boolean changed, int left, int top, int right, int bottom)
layout通过调用setFrame(l,t,r,b),l,t,r,b即子视图在父视图中的具体位置,onLayout()方法一般只会在自定义ViewGroup中才会使用。
1.2.1 layout()源码
//参数l、t、r、b为view边界相对父view的距离
public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
//这里通过setFrame或setOpticalFrame方法确定View在父容器当中的位置。
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
//调用onLayout方法。onLayout方法是一个空实现,不同的布局会有不同的实现。
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
}
}
- layout()中已经通过setOpticalFrame()或setFrame()方法对view自身位置进行设置,所以onLayout()一般用于ViewGroup对子view的位置进行计算。
1.3 绘制draw()
主要用前两步得到的参数完成视图的最终绘制,绘制流程的入口为draw()方法,主要步骤如下:
- draw background if needed
- save canvas if needed
- draw content of the view
- draw child views
- draw edges 、shadow if needed
- draw decorator such as scrollbars
绘制流程的主要方法:
public void draw(Canvas canvas)
protected void onDraw(Canvas canvas)
1.3.1 draw()源码
public void draw(Canvas canvas) {
int saveCount;
// 1. 如果需要,绘制背景
if (!dirtyOpaque) {
drawBackground(canvas);
}
// 2. 有过有必要,保存当前canvas。
final int viewFlags = mViewFlags;
if (!verticalEdges && !horizontalEdges) {
// 3. 绘制View的内容。
if (!dirtyOpaque) onDraw(canvas);
// 4. 绘制子View。
dispatchDraw(canvas);
drawAutofilledHighlight(canvas);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// 6. 绘制装饰,如滚动条等等。
onDrawForeground(canvas);
// we're done...
return;
}
}
/**
* 1.绘制View背景
*/
private void drawBackground(Canvas canvas) {
//获取背景
final Drawable background = mBackground;
if (background == null) {
return;
}
setBackgroundBounds();
//获取便宜值scrollX和scrollY,如果scrollX和scrollY都不等于0,则会在平移后的canvas上面绘制背景。
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
/**
* 3.绘制View的内容,该方法是一个空的实现,在各个业务当中自行处理。
*/
protected void onDraw(Canvas canvas) {
}
/**
* 4. 绘制子View。该方法在View当中是一个空的实现,在各个业务当中自行处理。
* 在ViewGroup当中对dispatchDraw方法做了实现,主要是遍历子View,并调用子类的draw方法,一般我们不需要自己重写该方法。
*/
protected void dispatchDraw(Canvas canvas) {
}
自定义view时一般重写onDraw(),而继承自ViewGroup时不需要。
1.3.2 绘制坐标系
如下图所示,在安卓系统中,一屏幕左上角为原点,向左为x轴,向下为y轴。
而对于控件View来说,其自身的坐标系及使用方法如下:
- 获取view坐标
可以使用如下方法获取view各边界到其父view的距离:
getTop()、getLeft()、getBottom()、getRight(); - 获取view尺寸:
width=getRight()-getLeft()
height=getBottom()-getTop()
也可以直接使用view提供的getWidth()和getHeight()方法来获取;
二、自定义控件属性
对于安卓定义的系统控件,使用 android: 命名空间进行配置,而对于自定义控件的参数配置,则可以使用自定义属性。
<declare-styleable name="CustumView">
<attr name="viewText" format="string" />
</declare-styleable>
2.1 属性值的类型
- reference:参考某一资源id
– 属性定义:
<attr name = "background" format = "reference" />
– 属性使用:
<ImageView android:background = "@drawable/图片ID"/>
- dimension:尺寸值,< Button android:layout_width = “42dip”/>
- color:颜色值,< TextView android:textColor = “#00FF00” />
- boolean:布尔值,< Button android:focusable = “true”/>
- float:浮点值,< alpha android:fromAlpha = “1.0”/>
- integer:整型,< animated-rotate android:framesCount = “12”/>
- string:字符串,< TextView android:text = “我是文本”/>
- fraction:百分数,< rotate android:pivotX = “200%”/>
- enum:枚举;使用中只能用其中的一个,不可android:orientation = “horizontal|vertical";
– 属性定义
<attr name="orientation">
<enum name="horizontal" value="0" />
<enum name="vertical" value="1" />
</attr>
– 属性使用
<LinearLayout
android:orientation = "vertical">
< /LinearLayout>
- flag:位或运算;使用时可以使用多个值;
– 定义
<attr name="gravity">
<flag name="top" value="0x01" />
<flag name="bottom" value="0x02" />
<flag name="left" value="0x04" />
<flag name="right" value="0x08" />
<flag name="center_vertical" value="0x16" />
...
</attr>
– 使用
<TextView android:gravity="bottom|left"/>
- 混合类型:定义属性时可以指定多个类型;
–定义
<attr name = "background" format = "reference|color" />
– 使用
<ImageView android:background = "@drawable/图片ID" />
或者:
<ImageView android:background = "#00FF00" />
三、自定义View开发步骤
3.1 新建属性集文件并添加自定义属性
在资源文件夹的 values 目录创建attrs.xml文件,在其中添加属性集:
<declare-styleable name="Dock">
<attr name="dockHeight" format="dimension"/>
<attr name="dockBacgroundColor" format="color"/>
<attr name="dockIconSize" format="dimension"/>
</declare-styleable>
3.2 创建自定义控件类集成View
创建控件类,其中一个参数的构造参数为通过new关键字新建时调用;修改构造函数使其相互调用;
public Dock(Context context) {
this(context,null);
}
public Dock(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public Dock(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
ctx=context;
initAttrs(context,attrs);
}
3.3 通过TypedArray获取属性集中各属性值并设置给控件
private void initAttrs(Context context, AttributeSet attrs){
TypedArray ta=context.obtainStyledAttributes(attrs,R.styleable.Dock);
iconSize=(int)ta.getDimension(R.styleable.Dock_dockIconSize,dp2px(context,60));
dockHeight=iconSize + 2*20; //(int)ta.getDimension(R.styleable.Dock_dockHeight,dp2px(context,100));
dockBgColor=ta.getColor(R.styleable.Dock_dockBacgroundColor, Color.parseColor("#f8f8f8"));
ta.recycle();
}
3.4 通过onMeasure()设置控件尺寸
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//去除wrap_content、Match_Parent对控件的影响
setMeasuredDimension(width, mHeight);
}
3.5 通过onDraw()方法完成控件绘制
通过onDraw()方法传递的canvas对象完成控件的最终绘制,可通过getWidth()或getRight()-getLeft()获取view的宽(以及高)。
@Override
protected void onDraw(Canvas canvas) {
// 准备画笔mPaint
canvas.drawCircle(100,100,10, mPaint);
}