瞭解自定義控件的三大流程(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