了解自定义控件的三大流程(measure、layout、draw)
在上一篇博客中我们大致介绍了一下View和ViewGroup,接下来我们就学习一下自定义控件的三大流程,为我们打下夯实的基础。(本博客主要参考《Android群英传》和《Android开发艺术探索》,大家也可以去阅读这两本书籍)
自定义控件三大流程简介
什么是自定义控件的三大流程,相信正在阅读这篇博客的你肯定接触过自定义控件,也见过onMeasure()、onLayout()和onDraw()这三个方法,自定义控件的三大流程就是这三个方法了,下面就让我们循序渐进的了解一下这三个方法。
measure
onMeasure方法的作用通俗点讲就是确认View位置,而对于ViewGroup来说,除了完成自己的measure过程以外,还会遍历去调用所有子元素的measure方法,各子元素再递归去执行这个过程。下面就依次来讲解:
1、View的测量过程
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
我们在onMeasure上按住ctrl后鼠标左击,进入 super.onMeasure的源码,如下所示:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
这里我们大致的可以明白onMeasure方法是通过setMeasuredDimension来控制控件大小的,我们不需要深入的去了解,我们在看一下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这个类,MeasureSpec其实是一个32位的int值,高2位为测量模式,低30位为测量的大小,内部封装了一些获取测量模式和测量大小的位运算。测量模式分一下三种:
UNSPECIFIED
中文翻译为未特别指定(规定)的,既父容器不对View有任何限制,View想要多大就多大
EXACTLY
中文翻译为精确地,即精确值模式,但我们将控件的layout_width或者layout_height指定为固定值,如“10dp”,或者为match_parent时使用该模式
AT_MOST
最大值模式,当控件宽高指定为wrap_content时,使用改模式
通过上面的分析我们可以很轻松的写出我们自己想要的测量方法,下面给出一个示例:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getMeasureSize(widthMeasureSpec), getMeasureSize(heightMeasureSpec));
}
public static int getMeasureSize(int measureSpec) {
int size = 200;
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
result = Math.min(size, specSize);
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
2、ViewGroup的measure过程
ViewGroup是一个抽象类,因此它没有重写View的onMeasure方法,但它提供了一个measureChildren的方法,同样的我们依次地大致阅读以下源码:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
相信大家也能很容易的读懂,原理就是遍历ViewGroup内所有的View去调用View的measure方法。
layout
Layout的作用是ViewGroup用来确定子元素的位置,当ViewGroup的位置被确定后,它在onLayout中会遍历所有的子元素并调用其layout方法,在layout方法中子view的onLayout方法有会被调用,这段话大家可能看的云里雾里,下面给出LinearLayout的onLayout的源码,相信大家就一目了然了:
1、写一个继承LinearLayout的类,重写onLayout方法
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
}
2、进入super.onLayout方法
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical(l, t, r, b);
} else {
layoutHorizontal(l, t, r, b);
}
}
3、从源码很明显可以看出线性布局分垂直和水平方向,我们以垂直方向为例,进入layoutVertical方法:
这里代码量很大,大家没必要看的非常仔细,首先找到for (int i = 0; i < count; i++)这个for循环,大家肯定明白这是遍历LinearLayout中的子View,之后调用setChildFrame这个方法确定子View的位置
void layoutVertical(int left, int top, int right, int bottom) {
final int paddingLeft = mPaddingLeft;
int childTop;
int childLeft;
// Where right end of child should go
final int width = right - left;
int childRight = width - mPaddingRight;
// Space available for child
int childSpace = width - paddingLeft - mPaddingRight;
final int count = getVirtualChildCount();
final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
switch (majorGravity) {
case Gravity.BOTTOM:
// mTotalLength contains the padding already
childTop = mPaddingTop + bottom - top - mTotalLength;
break;
// mTotalLength contains the padding already
case Gravity.CENTER_VERTICAL:
childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
break;
case Gravity.TOP:
default:
childTop = mPaddingTop;
break;
}
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);
} else if (child.getVisibility() != GONE) {
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams();
int gravity = lp.gravity;
if (gravity < 0) {
gravity = minorGravity;
}
final int layoutDirection = getLayoutDirection();
final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.CENTER_HORIZONTAL:
childLeft = paddingLeft + ((childSpace - childWidth) / 2)
+ lp.leftMargin - lp.rightMargin;
break;
case Gravity.RIGHT:
childLeft = childRight - childWidth - lp.rightMargin;
break;
case Gravity.LEFT:
default:
childLeft = paddingLeft + lp.leftMargin;
break;
}
if (hasDividerBeforeChildAt(i)) {
childTop += mDividerHeight;
}
childTop += lp.topMargin;
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
}
4、接下来进入setChildFrame方法看如何实现确定子View的位置
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
到这里大家就稍微明白了,原来是调用子View的layout方法来确定子View的位置
5、进入layout方法一探究竟
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;
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
if (shouldDrawRoundScrollbar()) {
if(mRoundScrollbarRenderer == null) {
mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
}
} else {
mRoundScrollbarRenderer = null;
}
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}
这里代码量也有些多,相信仔细看了的肯定也能读懂,没懂也没关系,不用看得那么仔细,首先找到boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
setOpticalFrame和setFrame从参数我们就大致能猜到是确定子View的位置的,之后再找到onLayout(changed, l, t, r, b);这个方法,这样就到了子View的onLayout方法中,若子View是一个ViewGroup的话又可以确定子View的位置了,这样就可以确定一个View树上所有View的位置。
相信看到这里大家都恍然大悟了,希望大家在看博客的时候也打开eclipse或者as简单的阅读以下源码,参考源码我相信大家聪慧的大脑,肯定能玩转自定义View的layout。
draw
当View的位置确定好之后我们就要开始绘制View了,这里我们就需要了解一下Paint和Canvas这两个对象了,相信大家已经非常熟悉了,这里稍微做一下总结:
1.Paint(画笔)类
要绘制图形,首先得调整画笔,按照自己的开发需要设置画笔的相关属性。Pain类的常用属性设置方法如下:
setAntiAlias(); //设置画笔的锯齿效果
setColor(); //设置画笔的颜色
setARGB(); //设置画笔的A、R、G、B值
setAlpha(); //设置画笔的Alpha值
setTextSize(); //设置字体的尺寸
setStyle(); //设置画笔的风格(空心或实心)
setStrokeWidth(); //设置空心边框的宽度
getColor(); //获取画笔的颜色
2.Canvas(画布)类
画笔属性设置好之后,还需要将图像绘制到画布上。Canvas类可以用来实现各种图形的绘制工作,如绘制直线、矩形、圆等等。Canvas绘制常用图形的方法如下:
绘制直线:canvas.drawLine(float startX, float startY, float stopX, float stopY, Paint paint);
绘制矩形:canvas.drawRect(float left, float top, float right, float bottom, Paint paint);
绘制圆形:canvas.drawCircle(float cx, float cy, float radius, Paint paint);
绘制字符:canvas.drawText(String text, float x, float y, Paint paint);
绘制图形:canvas.drawBirmap(Bitmap bitmap, float left, float top, Paint paint);
示例:简单的实现一下音频条效果
public class CustomView extends View {
private Paint mPaint = null;
private int count = 80;
private int mRectWidth = 10;
private int offset = 2;
private float mRectHight = 400;
private float mCurrentHight = 400;
public customView(Context context) {
super(context);
init();
}
public customView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public customView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(Color.RED);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getMeasureSize(widthMeasureSpec), getMeasureSize(heightMeasureSpec));
}
public static int getMeasureSize(int measureSpec) {
int size = 200;
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
result = Math.min(size, specSize);
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
LinearGradient mLinearGradient = new LinearGradient(
0,
0,
mRectWidth,
mRectHight,
Color.YELLOW,
Color.BLUE,
Shader.TileMode.CLAMP
);
mPaint.setShader(mLinearGradient);
}
@Override
protected void onDraw(Canvas canvas) {
for (int i = 0; i < count; i++){
double mRandom = Math.random();
mCurrentHight = (float) (mRectHight * mRandom);
canvas.drawRect((float)(mRectWidth * i + offset),
mRectHight-mCurrentHight,
(float)(mRectWidth * (i+1)),
mRectHight,
mPaint);
}
postInvalidateDelayed(300);
}
}
这个示例博主这里就不做解释了,实现方法有很多种,我只是提供一种思路,更多的还是希望读者亲自去敲一遍试试效果,遇到不懂的baidu或者google,这样收获会更多,感谢您的阅读,下一篇将继续讨论自定义View的滑动和事件分发机制,欢迎大家进一步学习!自定义控件的时间分发、拦截、处理http://blog.csdn.net/u010083327/article/details/60874681