上次我們研究了View的構造函數(shù),自定義View最重要的步驟就是完成我們View的繪制.我們本篇就來好好的研究一下它.
我們都知道重寫onDraw()方法便可對View進行各種繪制操作,但是在繪制之前我們還需要對View的布局進行處理,因為我們的自定義View可能需要在多種不同的布局環(huán)境下展現(xiàn),如果不對其進行布局的處理,可能會使View的繪制得不到想要的結果.
onSizeChanged()
處理布局無非就是要知道View在屏幕展現(xiàn)時的大小和位置,在繪制前計算得到這些值以及其它與View的大小相關的值,我們便能將View正確的繪制到屏幕上.
而最簡單的方法就是重寫onSizeChanged()函數(shù),這個函數(shù)會在View第一次分配大小時,以及當View的大小發(fā)生變化時被調用.因為當View的大小發(fā)生改變時才會對其布局造成影響,所以我們只需在onSizeChanged()函數(shù)中重新計算View的大小,位置,以及其他與View大小相關的值即可.這樣我們可以避免在每次進行繪制時都在onDraw()函數(shù)中進行計算,可以提高效率.
下面是一段示例代碼,使用的還是上次的相關代碼:
private float mWidth, mHeight;
/**
* 在View大小發(fā)生變化是會被調用.可以在此函數(shù)進行View的大小,位置以及其他相關值的計算
*
* @param w 當前View的寬度
* @param h 當前View的高度
* @param oldw View改變前的寬度
* @param oldh View改變前的高度
*/
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
float xpad = (float)(getPaddingLeft()+getPaddingRight());
float ypad = (float)(getPaddingTop()+getPaddingBottom());
mWidth = w-xpad;
mHeight = h-ypad;
}
上面代碼中我們定義了mWidth,mHeight兩個成員屬性, 我們的自定義View是繪制一個矩形.所以在onSizeChanged()中需要計算矩形展現(xiàn)的大小.這里需要將padding的值計算在內,因為padding是需要View在布局時處理的.
onMeasure()
上面的onSizeChanged()函數(shù)可以簡單進行布局的處理,如果想要對View的布局參數(shù)進行細致的處理,這就需要重寫onMeasure()函數(shù).onMeasure()函數(shù)是由measure(int widthMeasureSpec, int heightMeasureSpec)函數(shù)所調用的,measure()用于獲取一個View應該在布局中的大小,其中兩個參數(shù)int widthMeasureSpec, int heightMeasureSpec由父布局提供,表示View的寬高的限制.但是具體實際的測量工作是在onMeasure()中完成的.
所以void onMeasure (int widthMeasureSpec,int heightMeasureSpec)需要對View和它的內容進行測量,為View的內容提供準確有效的測量.先來研究一下傳進來的兩個參數(shù)int widthMeasureSpec,int heightMeasureSpec.這兩個參數(shù)是表示當前View在布局時測量的寬高的限制,它們是經過View.MeasureSpec編碼處理的32位int值.它表示布局寬度或高度,并包含著size和mode兩個信息.高兩位表示mode,其余的30位表示size,它是通過位運算將它們結合在一起的.這兩個參數(shù)是由View的父控件傳遞過來的.我們在xml布局文件或在代碼中為View設置的寬高具體值或寬高的填充模式就對應著這兩個參數(shù)的mode的3種形式:
-
UNSPECIFIED
表明parent沒有對child強加任何限制,child可以是它想要的任何尺寸 -
EXACTLY
parent給child一個具體的值,child需要設置為該大小
對應具體的寬高值或match_parent -
AT_MOST
child可以是任意的大小,但不能超過限制的值
對應wrap_content
這些mode都表示View在測量時的限制.清楚了兩個參數(shù)的意義就可以對View進行測量了.但需要注意的是,View的寬高尺寸,只有在測量之后(measure()被調用后)才能得到.下面是onMeasure()的代碼:
/**
* 對View的內容進行測量
*
* @param widthMeasureSpec parent對View寬度的限制信息
* @param heightMeasureSpec parent對View高度的限制信息
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
float xpad = (float) (getPaddingLeft() + getPaddingRight());
float ypad = (float) (getPaddingTop() + getPaddingBottom());
mWidth = MeasureSpec.getSize(widthMeasureSpec) - xpad;
mHeight = MeasureSpec.getSize(heightMeasureSpec) - ypad;
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
之前已經提到過了,我們需要對View的padding進行處理,這是View的職責.當重寫onMeasure()方法時必須調用void setMeasuredDimension(int measuredWidth, int measuredHeight)這個函數(shù)來存儲經過測量的寬高,否則會觸發(fā)IllegalStateException類型的異常,調用父類的onMeasure()函數(shù)也是一種方法,因為super.onMeasure()函數(shù)中調用了setMeasuredDimension(),上面的代碼我就直接調用了父類的onMeasure()方法.
我們可以進入super.onMeasure()當中去查看一下源代碼,它的實現(xiàn)如下:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
果然如我們所說的,它調用了setMeasuredDimension(),代碼中還有調用了另外三個函數(shù)getDefaultSize(),getSuggestedMinimumWidth(),getSuggestedMinimumHeight().
我們先跳到getDefaultSize(),看一下這函數(shù)是干什么的.
/**
* Utility to return a default size. Uses the supplied size if the
* MeasureSpec imposed no constraints. Will get larger if allowed
* by the MeasureSpec.
*
* @param size Default size for this view
* @param measureSpec Constraints imposed by the parent
* @return The size this view should be.
*/
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;
}
從注釋中可以看出,這個函數(shù)是根據傳進的參數(shù)獲取View的布局大小,具體過程就是根據測量模式的不同,返回不同的大小.getSuggestedMinimumHeight()和getSuggestedMinimumWidth()兩個函數(shù)就是獲取View的最小建議值,這就可以保證View的測量值是最少是建議的值(View的最小值或(View的最小值和背景最小值中的較大值)),下面就是具體的源碼.
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
onDraw()
測量完畢后,我們就可以進行View的繪制了.我們通過Canvas和Paint進行繪制,Canvas決定繪制什么,而Paint決定怎么繪制.由于Canvas是onDraw(Canvas canvas)傳入的參數(shù),但Paint卻需要我們自己創(chuàng)建和初始化,如果選擇在onDraw()里執(zhí)行這項工作,則在每次的View繪制時都會進行.所以為了提高效率,我們可以將Paint的創(chuàng)建和初始化放在View的構造函數(shù)中執(zhí)行.
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
mPaint = new Paint();
mPaint.setColor(Color.BLUE);
mPaint.setStrokeWidth(5);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRect(getPaddingLeft(), getPaddingTop(), getPaddingLeft() + mWidth, getPaddingTop() + mHeight, mPaint);
}
這里我們只簡單的繪制一個矩形.一個自定義的View基本上就完成了.