===========================第一階段============================
任何一個視圖都不可能憑空突然出現在屏幕上,它們都是要經過非常科學的繪製流程後才能顯示出來的。每一個視圖的繪製過程都必須經歷三個最主要的階段,即onMeasure()、onLayout()和onDraw(),下面我們逐個對這三個階段展開進行探討。
一. onMeasure()
measure是測量的意思,那麼onMeasure()方法顧名思義就是用於測量視圖的大小的。View系統的繪製流程會從ViewRoot的performTraversals()方法中開始,在其內部調用View的measure()方法。measure()方法接收兩個參數,widthMeasureSpec和heightMeasureSpec,這兩個值分別用於確定視圖的寬度和高度的規格和大小。
MeasureSpec的值由specSize和specMode共同組成的,其中specSize記錄的是大小,specMode記錄的是規格。specMode一共有三種類型,如下所示:
1. EXACTLY
表示父視圖希望子視圖的大小應該是由specSize的值來決定的,系統默認會按照這個規則來設置子視圖的大小,開發人員當然也可以按照自己的意願設置成任意的大小。
2. AT_MOST
表示子視圖最多隻能是specSize中指定的大小,開發人員應該儘可能小得去設置這個視圖,並且保證不會超過specSize。系統默認會按照這個規則來設置子視圖的大小,開發人員當然也可以按照自己的意願設置成任意的大小。
3. UNSPECIFIED
表示開發人員可以將視圖按照自己的意願設置成任意的大小,沒有任何限制。這種情況比較少見,不太會用到。
那麼你可能會有疑問了,widthMeasureSpec和heightMeasureSpec這兩個值又是從哪裏得到的呢?通常情況下,這兩個值都是由父視圖經過計算後傳遞給子視圖的,說明父視圖會在一定程度上決定子視圖的大小。但是最外層的根視圖,它的widthMeasureSpec和heightMeasureSpec又是從哪裏得到的呢?這就需要去分析ViewRoot中的源碼了,觀察performTraversals()方法可以發現如下代碼:
-
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
-
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
可以看到,這裏調用了getRootMeasureSpec()方法去獲取widthMeasureSpec和heightMeasureSpec的值,注意方法中傳入的參數,其中lp.width和lp.height在創建ViewGroup實例的時候就被賦值了,它們都等於MATCH_PARENT。然後看下getRootMeasureSpec()方法中的代碼,如下所示:
-
private int getRootMeasureSpec(int windowSize, int rootDimension) {
-
int measureSpec;
-
switch (rootDimension) {
-
case ViewGroup.LayoutParams.MATCH_PARENT:
-
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
-
break;
-
case ViewGroup.LayoutParams.WRAP_CONTENT:
-
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
-
break;
-
default:
-
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
-
break;
-
}
-
return measureSpec;
-
}
可以看到,這裏使用了MeasureSpec.makeMeasureSpec()方法來組裝一個MeasureSpec,當rootDimension參數等於MATCH_PARENT的時候,MeasureSpec的specMode就等於EXACTLY,當rootDimension等於WRAP_CONTENT的時候,MeasureSpec的specMode就等於AT_MOST。並且MATCH_PARENT和WRAP_CONTENT時的specSize都是等於windowSize的,也就意味着根視圖總是會充滿全屏的。
介紹了這麼多MeasureSpec相關的內容,接下來我們看下View的measure()方法裏面的代碼吧,如下所示:
-
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
-
if ((mPrivateFlags & FORCE_LAYOUT) == FORCE_LAYOUT ||
-
widthMeasureSpec != mOldWidthMeasureSpec ||
-
heightMeasureSpec != mOldHeightMeasureSpec) {
-
mPrivateFlags &= ~MEASURED_DIMENSION_SET;
-
if (ViewDebug.TRACE_HIERARCHY) {
-
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_MEASURE);
-
}
-
onMeasure(widthMeasureSpec, heightMeasureSpec);
-
if ((mPrivateFlags & MEASURED_DIMENSION_SET) != MEASURED_DIMENSION_SET) {
-
throw new IllegalStateException("onMeasure() did not set the"
-
+ " measured dimension by calling"
-
+ " setMeasuredDimension()");
-
}
-
mPrivateFlags |= LAYOUT_REQUIRED;
-
}
-
mOldWidthMeasureSpec = widthMeasureSpec;
-
mOldHeightMeasureSpec = heightMeasureSpec;
-
}
注意觀察,measure()這個方法是final的,因此我們無法在子類中去重寫這個方法,說明Android是不允許我們改變View的measure框架的。然後在第9行調用了onMeasure()方法,這裏纔是真正去測量並設置View大小的地方,默認會調用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是一直從measure()方法中傳遞過來的。然後調用MeasureSpec.getMode()方法可以解析出specMode,調用MeasureSpec.getSize()方法可以解析出specSize。接下來進行判斷,如果specMode等於AT_MOST或EXACTLY就返回specSize,這也是系統默認的行爲。之後會在onMeasure()方法中調用setMeasuredDimension()方法來設定測量出的大小,這樣一次measure過程就結束了。
當然,一個界面的展示可能會涉及到很多次的measure,因爲一個佈局中一般都會包含多個子視圖,每個視圖都需要經歷一次measure過程。ViewGroup中定義了一個measureChildren()方法來去測量子視圖的大小,如下所示:
-
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);
-
}
-
}
-
}
這裏首先會去遍歷當前佈局下的所有子視圖,然後逐個調用measureChild()方法來測量相應子視圖的大小,如下所示:
-
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);
-
}
可以看到,在第4行和第6行分別調用了getChildMeasureSpec()方法來去計算子視圖的MeasureSpec,計算的依據就是佈局文件中定義的MATCH_PARENT、WRAP_CONTENT等值,這個方法的內部細節就不再貼出。然後在第8行調用子視圖的measure()方法,並把計算出的MeasureSpec傳遞進去,之後的流程就和前面所介紹的一樣了。
當然,onMeasure()方法是可以重寫的,也就是說,如果你不想使用系統默認的測量方式,可以按照自己的意願進行定製,比如:
-
public class MyView extends View {
-
-
......
-
-
@Override
-
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-
setMeasuredDimension(200, 200);
-
}
-
-
}
這樣的話就把View默認的測量流程覆蓋掉了,不管在佈局文件中定義MyView這個視圖的大小是多少,最終在界面上顯示的大小都將會是200*200。
需要注意的是,在setMeasuredDimension()方法調用之後,我們才能使用getMeasuredWidth()和getMeasuredHeight()來獲取視圖測量出的寬高,以此之前調用這兩個方法得到的值都會是0。
由此可見,視圖大小的控制是由父視圖、佈局文件、以及視圖本身共同完成的,父視圖會提供給子視圖參考的大小,而開發人員可以在XML文件中指定視圖的大小,然後視圖本身會對最終的大小進行拍板。
到此爲止,我們就把視圖繪製流程的第一階段分析完了。
二. onLayout()
measure過程結束後,視圖的大小就已經測量好了,接下來就是layout的過程了。正如其名字所描述的一樣,這個方法是用於給視圖進行佈局的,也就是確定視圖的位置。ViewRoot的performTraversals()方法會在measure結束後繼續執行,並調用View的layout()方法來執行此過程,如下所示:
-
host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight);
layout()方法接收四個參數,分別代表着左、上、右、下的座標,當然這個座標是相對於當前視圖的父視圖而言的。可以看到,這裏還把剛纔測量出的寬度和高度傳到了layout()方法中。那麼我們來看下layout()方法中的代碼是什麼樣的吧,如下所示:
-
public void layout(int l, int t, int r, int b) {
-
int oldL = mLeft;
-
int oldT = mTop;
-
int oldB = mBottom;
-
int oldR = mRight;
-
boolean changed = setFrame(l, t, r, b);
-
if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {
-
if (ViewDebug.TRACE_HIERARCHY) {
-
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);
-
}
-
onLayout(changed, l, t, r, b);
-
mPrivateFlags &= ~LAYOUT_REQUIRED;
-
if (mOnLayoutChangeListeners != null) {
-
ArrayList<OnLayoutChangeListener> listenersCopy =
-
(ArrayList<OnLayoutChangeListener>) 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 &= ~FORCE_LAYOUT;
-
}
在layout()方法中,首先會調用setFrame()方法來判斷視圖的大小是否發生過變化,以確定有沒有必要對當前的視圖進行重繪,同時還會在這裏把傳遞過來的四個參數分別賦值給mLeft、mTop、mRight和mBottom這幾個變量。接下來會在第11行調用onLayout()方法,正如onMeasure()方法中的默認行爲一樣,也許你已經迫不及待地想知道onLayout()方法中的默認行爲是什麼樣的了。進入onLayout()方法,咦?怎麼這是個空方法,一行代碼都沒有?!
沒錯,View中的onLayout()方法就是一個空方法,因爲onLayout()過程是爲了確定視圖在佈局中所在的位置,而這個操作應該是由佈局來完成的,即父視圖決定子視圖的顯示位置。既然如此,我們來看下ViewGroup中的onLayout()方法是怎麼寫的吧,代碼如下:
-
@Override
-
protected abstract void onLayout(boolean changed, int l, int t, int r, int b);
可以看到,ViewGroup中的onLayout()方法竟然是一個抽象方法,這就意味着所有ViewGroup的子類都必須重寫這個方法。沒錯,像LinearLayout、RelativeLayout等佈局,都是重寫了這個方法,然後在內部按照各自的規則對子視圖進行佈局的。由於LinearLayout和RelativeLayout的佈局規則都比較複雜,就不單獨拿出來進行分析了,這裏我們嘗試自定義一個佈局,藉此來更深刻地理解onLayout()的過程。
自定義的這個佈局目標很簡單,只要能夠包含一個子視圖,並且讓子視圖正常顯示出來就可以了。那麼就給這個佈局起名叫做SimpleLayout吧,代碼如下所示:
-
public class SimpleLayout extends ViewGroup {
-
-
public SimpleLayout(Context context, AttributeSet attrs) {
-
super(context, attrs);
-
}
-
-
@Override
-
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-
if (getChildCount() > 0) {
-
View childView = getChildAt(0);
-
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
-
}
-
}
-
-
@Override
-
protected void onLayout(boolean changed, int l, int t, int r, int b) {
-
if (getChildCount() > 0) {
-
View childView = getChildAt(0);
-
childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight());
-
}
-
}
-
-
}
代碼非常的簡單,我們來看下具體的邏輯吧。你已經知道,onMeasure()方法會在onLayout()方法之前調用,因此這裏在onMeasure()方法中判斷SimpleLayout中是否有包含一個子視圖,如果有的話就調用measureChild()方法來測量出子視圖的大小。
接着在onLayout()方法中同樣判斷SimpleLayout是否有包含一個子視圖,然後調用這個子視圖的layout()方法來確定它在SimpleLayout佈局中的位置,這裏傳入的四個參數依次是0、0、childView.getMeasuredWidth()和childView.getMeasuredHeight(),分別代表着子視圖在SimpleLayout中左上右下四個點的座標。其中,調用childView.getMeasuredWidth()和childView.getMeasuredHeight()方法得到的值就是在onMeasure()方法中測量出的寬和高。
這樣就已經把SimpleLayout這個佈局定義好了,下面就是在XML文件中使用它了,如下所示:
-
<com.example.viewtest.SimpleLayout xmlns:android="http://schemas.android.com/apk/res/android"
-
android:layout_width="match_parent"
-
android:layout_height="match_parent" >
-
-
<ImageView
-
android:layout_width="wrap_content"
-
android:layout_height="wrap_content"
-
android:src="@drawable/ic_launcher"
-
/>
-
-
</com.example.viewtest.SimpleLayout>
可以看到,我們能夠像使用普通的佈局文件一樣使用SimpleLayout,只是注意它只能包含一個子視圖,多餘的子視圖會被捨棄掉。這裏SimpleLayout中包含了一個ImageView,並且ImageView的寬高都是wrap_content。現在運行一下程序,結果如下圖所示:
OK!ImageView成功已經顯示出來了,並且顯示的位置也正是我們所期望的。如果你想改變ImageView顯示的位置,只需要改變childView.layout()方法的四個參數就行了。
在onLayout()過程結束後,我們就可以調用getWidth()方法和getHeight()方法來獲取視圖的寬高了。說到這裏,我相信很多朋友長久以來都會有一個疑問,getWidth()方法和getMeasureWidth()方法到底有什麼區別呢?它們的值好像永遠都是相同的。其實它們的值之所以會相同基本都是因爲佈局設計者的編碼習慣非常好,實際上它們之間的差別還是挺大的。
首先getMeasureWidth()方法在measure()過程結束後就可以獲取到了,而getWidth()方法要在layout()過程結束後才能獲取到。另外,getMeasureWidth()方法中的值是通過setMeasuredDimension()方法來進行設置的,而getWidth()方法中的值則是通過視圖右邊的座標減去左邊的座標計算出來的。
觀察SimpleLayout中onLayout()方法的代碼,這裏給子視圖的layout()方法傳入的四個參數分別是0、0、childView.getMeasuredWidth()和childView.getMeasuredHeight(),因此getWidth()方法得到的值就是childView.getMeasuredWidth() - 0 = childView.getMeasuredWidth() ,所以此時getWidth()方法和getMeasuredWidth() 得到的值就是相同的,但如果你將onLayout()方法中的代碼進行如下修改:
-
@Override
-
protected void onLayout(boolean changed, int l, int t, int r, int b) {
-
if (getChildCount() > 0) {
-
View childView = getChildAt(0);
-
childView.layout(0, 0, 200, 200);
-
}
-
}
這樣getWidth()方法得到的值就是200 - 0 = 200,不會再和getMeasuredWidth()的值相同了。當然這種做法充分不尊重measure()過程計算出的結果,通常情況下是不推薦這麼寫的。getHeight()與getMeasureHeight()方法之間的關係同上,就不再重複分析了。
到此爲止,我們把視圖繪製流程的第二階段也分析完了。
三. onDraw()
measure和layout的過程都結束後,接下來就進入到draw的過程了。同樣,根據名字你就能夠判斷出,在這裏才真正地開始對視圖進行繪製。ViewRoot中的代碼會繼續執行並創建出一個Canvas對象,然後調用View的draw()方法來執行具體的繪製工作。draw()方法內部的繪製過程總共可以分爲六步,其中第二步和第五步在一般情況下很少用到,因此這裏我們只分析簡化後的繪製過程。代碼如下所示:
-
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);
-
-
dispatchDraw(canvas);
-
-
onDrawScrollBars(canvas);
-
-
return;
-
}
-
}
可以看到,第一步是從第9行代碼開始的,這一步的作用是對視圖的背景進行繪製。這裏會先得到一個mBGDrawable對象,然後根據layout過程確定的視圖位置來設置背景的繪製區域,之後再調用Drawable的draw()方法來完成背景的繪製工作。那麼這個mBGDrawable對象是從哪裏來的呢?其實就是在XML中通過android:background屬性設置的圖片或顏色。當然你也可以在代碼中通過setBackgroundColor()、setBackgroundResource()等方法進行賦值。
接下來的第三步是在第34行執行的,這一步的作用是對視圖的內容進行繪製。可以看到,這裏去調用了一下onDraw()方法,那麼onDraw()方法裏又寫了什麼代碼呢?進去一看你會發現,原來又是個空方法啊。其實也可以理解,因爲每個視圖的內容部分肯定都是各不相同的,這部分的功能交給子類來去實現也是理所當然的。
第三步完成之後緊接着會執行第四步,這一步的作用是對當前視圖的所有子視圖進行繪製。但如果當前的視圖沒有子視圖,那麼也就不需要進行繪製了。因此你會發現View中的dispatchDraw()方法又是一個空方法,而ViewGroup的dispatchDraw()方法中就會有具體的繪製代碼。
以上都執行完後就會進入到第六步,也是最後一步,這一步的作用是對視圖的滾動條進行繪製。那麼你可能會奇怪,當前的視圖又不一定是ListView或者ScrollView,爲什麼要繪製滾動條呢?其實不管是Button也好,TextView也好,任何一個視圖都是有滾動條的,只是一般情況下我們都沒有讓它顯示出來而已。繪製滾動條的代碼邏輯也比較複雜,這裏就不再貼出來了,因爲我們的重點是第三步過程。
通過以上流程分析,相信大家已經知道,View是不會幫我們繪製內容部分的,因此需要每個視圖根據想要展示的內容來自行繪製。如果你去觀察TextView、ImageView等類的源碼,你會發現它們都有重寫onDraw()這個方法,並且在裏面執行了相當不少的繪製邏輯。繪製的方式主要是藉助Canvas這個類,它會作爲參數傳入到onDraw()方法中,供給每個視圖使用。Canvas這個類的用法非常豐富,基本可以把它當成一塊畫布,在上面繪製任意的東西,那麼我們就來嘗試一下吧。
這裏簡單起見,我只是創建一個非常簡單的視圖,並且用Canvas隨便繪製了一點東西,代碼如下所示:
-
public class MyView extends View {
-
-
private Paint mPaint;
-
-
public MyView(Context context, AttributeSet attrs) {
-
super(context, attrs);
-
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
-
}
-
-
@Override
-
protected void onDraw(Canvas canvas) {
-
mPaint.setColor(Color.YELLOW);
-
canvas.drawRect(0, 0, getWidth(), getHeight(), mPaint);
-
mPaint.setColor(Color.BLUE);
-
mPaint.setTextSize(20);
-
String text = "Hello View";
-
canvas.drawText(text, 0, getHeight() / 2, mPaint);
-
}
-
}
可以看到,我們創建了一個自定義的MyView繼承自View,並在MyView的構造函數中創建了一個Paint對象。Paint就像是一個畫筆一樣,配合着Canvas就可以進行繪製了。這裏我們的繪製邏輯比較簡單,在onDraw()方法中先是把畫筆設置成黃色,然後調用Canvas的drawRect()方法繪製一個矩形。然後在把畫筆設置成藍色,並調整了一下文字的大小,然後調用drawText()方法繪製了一段文字。
就這麼簡單,一個自定義的視圖就已經寫好了,現在可以在XML中加入這個視圖,如下所示:
-
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-
android:layout_width="match_parent"
-
android:layout_height="match_parent" >
-
-
<com.example.viewtest.MyView
-
android:layout_width="200dp"
-
android:layout_height="100dp"
-
/>
-
-
</LinearLayout>
將MyView的寬度設置成200dp,高度設置成100dp,然後運行一下程序,結果如下圖所示:
圖中顯示的內容也正是MyView這個視圖的內容部分了。由於我們沒給MyView設置背景,因此這裏看不出來View自動繪製的背景效果。
當然了Canvas的用法還有很多很多,這裏我不可能把Canvas的所有用法都列舉出來,剩下的就要靠大家自行去研究和學習了。
===========================第二階段============================
相信大家在平時使用View的時候都會發現它是有狀態的,比如說有一個按鈕,普通狀態下是一種效果,但是當手指按下的時候就會變成另外一種效果,這樣纔會給人產生一種點擊了按鈕的感覺。當然了,這種效果相信幾乎所有的Android程序員都知道該如何實現,但是我們既然是深入瞭解View,那麼自然也應該知道它背後的實現原理應該是什麼樣的,今天就讓我們來一起探究一下吧。
一、視圖狀態
視圖狀態的種類非常多,一共有十幾種類型,不過多數情況下我們只會使用到其中的幾種,因此這裏我們也就只去分析最常用的幾種視圖狀態。
1. enabled
表示當前視圖是否可用。可以調用setEnable()方法來改變視圖的可用狀態,傳入true表示可用,傳入false表示不可用。它們之間最大的區別在於,不可用的視圖是無法響應onTouch事件的。
2. focused
表示當前視圖是否獲得到焦點。通常情況下有兩種方法可以讓視圖獲得焦點,即通過鍵盤的上下左右鍵切換視圖,以及調用requestFocus()方法。而現在的Android手機幾乎都沒有鍵盤了,因此基本上只可以使用requestFocus()這個辦法來讓視圖獲得焦點了。而requestFocus()方法也不能保證一定可以讓視圖獲得焦點,它會有一個布爾值的返回值,如果返回true說明獲得焦點成功,返回false說明獲得焦點失敗。一般只有視圖在focusable和focusable
in touch mode同時成立的情況下才能成功獲取焦點,比如說EditText。
3. window_focused
表示當前視圖是否處於正在交互的窗口中,這個值由系統自動決定,應用程序不能進行改變。
4. selected
表示當前視圖是否處於選中狀態。一個界面當中可以有多個視圖處於選中狀態,調用setSelected()方法能夠改變視圖的選中狀態,傳入true表示選中,傳入false表示未選中。
5. pressed
表示當前視圖是否處於按下狀態。可以調用setPressed()方法來對這一狀態進行改變,傳入true表示按下,傳入false表示未按下。通常情況下這個狀態都是由系統自動賦值的,但開發者也可以自己調用這個方法來進行改變。
我們可以在項目的drawable目錄下創建一個selector文件,在這裏配置每種狀態下視圖對應的背景圖片。比如創建一個compose_bg.xml文件,在裏面編寫如下代碼:
-
<selector xmlns:android="http://schemas.android.com/apk/res/android">
-
-
<item android:drawable="@drawable/compose_pressed" android:state_pressed="true"></item>
-
<item android:drawable="@drawable/compose_pressed" android:state_focused="true"></item>
-
<item android:drawable="@drawable/compose_normal"></item>
-
-
</selector>
這段代碼就表示,當視圖處於正常狀態的時候就顯示compose_normal這張背景圖,當視圖獲得到焦點或者被按下的時候就顯示compose_pressed這張背景圖。
創建好了這個selector文件後,我們就可以在佈局或代碼中使用它了,比如將它設置爲某個按鈕的背景圖,如下所示:
-
<?xml version="1.0" encoding="utf-8"?>
-
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-
android:layout_width="match_parent"
-
android:layout_height="match_parent"
-
android:orientation="vertical" >
-
-
<Button
-
android:id="@+id/compose"
-
android:layout_width="60dp"
-
android:layout_height="40dp"
-
android:layout_gravity="center_horizontal"
-
android:background="@drawable/compose_bg"
-
/>
-
-
</LinearLayout>
現在運行一下程序,這個按鈕在普通狀態和按下狀態的時候就會顯示不同的背景圖片。
這樣我們就用一個非常簡單的方法實現了按鈕按下的效果,但是它的背景原理到底是怎樣的呢?這就又要從源碼的層次上進行分析了。
我們都知道,當手指按在視圖上的時候,視圖的狀態就已經發生了變化,此時視圖的pressed狀態是true。每當視圖的狀態有發生改變的時候,就會回調View的drawableStateChanged()方法,代碼如下所示:
-
protected void drawableStateChanged() {
-
Drawable d = mBGDrawable;
-
if (d != null && d.isStateful()) {
-
d.setState(getDrawableState());
-
}
-
}
在這裏的第一步,首先是將mBGDrawable賦值給一個Drawable對象,那麼這個mBGDrawable是什麼呢?觀察setBackgroundResource()方法中的代碼,如下所示:
-
public void setBackgroundResource(int resid) {
-
if (resid != 0 && resid == mBackgroundResource) {
-
return;
-
}
-
Drawable d= null;
-
if (resid != 0) {
-
d = mResources.getDrawable(resid);
-
}
-
setBackgroundDrawable(d);
-
mBackgroundResource = resid;
-
}
可以看到,在第7行調用了Resource的getDrawable()方法將resid轉換成了一個Drawable對象,然後調用了setBackgroundDrawable()方法並將這個Drawable對象傳入,在setBackgroundDrawable()方法中會將傳入的Drawable對象賦值給mBGDrawable。
而我們在佈局文件中通過android:background屬性指定的selector文件,效果等同於調用setBackgroundResource()方法。也就是說drawableStateChanged()方法中的mBGDrawable對象其實就是我們指定的selector文件。
接下來在drawableStateChanged()方法的第4行調用了getDrawableState()方法來獲取視圖狀態,代碼如下所示:
-
public final int[] getDrawableState() {
-
if ((mDrawableState != null) && ((mPrivateFlags & DRAWABLE_STATE_DIRTY) == 0)) {
-
return mDrawableState;
-
} else {
-
mDrawableState = onCreateDrawableState(0);
-
mPrivateFlags &= ~DRAWABLE_STATE_DIRTY;
-
return mDrawableState;
-
}
-
}
在這裏首先會判斷當前視圖的狀態是否發生了改變,如果沒有改變就直接返回當前的視圖狀態,如果發生了改變就調用onCreateDrawableState()方法來獲取最新的視圖狀態。視圖的所有狀態會以一個整型數組的形式返回。
在得到了視圖狀態的數組之後,就會調用Drawable的setState()方法來對狀態進行更新,代碼如下所示:
-
public boolean setState(final int[] stateSet) {
-
if (!Arrays.equals(mStateSet, stateSet)) {
-
mStateSet = stateSet;
-
return onStateChange(stateSet);
-
}
-
return false;
-
}
這裏會調用Arrays.equals()方法來判斷視圖狀態的數組是否發生了變化,如果發生了變化則調用onStateChange()方法,否則就直接返回false。但你會發現,Drawable的onStateChange()方法中其實就只是簡單返回了一個false,並沒有任何的邏輯處理,這是爲什麼呢?這主要是因爲mBGDrawable對象是通過一個selector文件創建出來的,而通過這種文件創建出來的Drawable對象其實都是一個StateListDrawable實例,因此這裏調用的onStateChange()方法實際上調用的是StateListDrawable中的onStateChange()方法,那麼我們趕快看一下吧:
-
@Override
-
protected boolean onStateChange(int[] stateSet) {
-
int idx = mStateListState.indexOfStateSet(stateSet);
-
if (DEBUG) android.util.Log.i(TAG, "onStateChange " + this + " states "
-
+ Arrays.toString(stateSet) + " found " + idx);
-
if (idx < 0) {
-
idx = mStateListState.indexOfStateSet(StateSet.WILD_CARD);
-
}
-
if (selectDrawable(idx)) {
-
return true;
-
}
-
return super.onStateChange(stateSet);
-
}
可以看到,這裏會先調用indexOfStateSet()方法來找到當前視圖狀態所對應的Drawable資源下標,然後在第9行調用selectDrawable()方法並將下標傳入,在這個方法中就會將視圖的背景圖設置爲當前視圖狀態所對應的那張圖片了。
那你可能會有疑問,在前面一篇文章中我們說到,任何一個視圖的顯示都要經過非常科學的繪製流程的,很顯然,背景圖的繪製是在draw()方法中完成的,那麼爲什麼selectDrawable()方法能夠控制背景圖的改變呢?這就要研究一下視圖重繪的流程了。
二、視圖重繪
雖然視圖會在Activity加載完成之後自動繪製到屏幕上,但是我們完全有理由在與Activity進行交互的時候要求動態更新視圖,比如改變視圖的狀態、以及顯示或隱藏某個控件等。那在這個時候,之前繪製出的視圖其實就已經過期了,此時我們就應該對視圖進行重繪。
調用視圖的setVisibility()、setEnabled()、setSelected()等方法時都會導致視圖重繪,而如果我們想要手動地強制讓視圖進行重繪,可以調用invalidate()方法來實現。當然了,setVisibility()、setEnabled()、setSelected()等方法的內部其實也是通過調用invalidate()方法來實現的,那麼就讓我們來看一看invalidate()方法的代碼是什麼樣的吧。
View的源碼中會有數個invalidate()方法的重載和一個invalidateDrawable()方法,當然它們的原理都是相同的,因此我們只分析其中一種,代碼如下所示:
-
void invalidate(boolean invalidateCache) {
-
if (ViewDebug.TRACE_HIERARCHY) {
-
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.INVALIDATE);
-
}
-
if (skipInvalidate()) {
-
return;
-
}
-
if ((mPrivateFlags & (DRAWN | HAS_BOUNDS)) == (DRAWN | HAS_BOUNDS) ||
-
(invalidateCache && (mPrivateFlags & DRAWING_CACHE_VALID) == DRAWING_CACHE_VALID) ||
-
(mPrivateFlags & INVALIDATED) != INVALIDATED || isOpaque() != mLastIsOpaque) {
-
mLastIsOpaque = isOpaque();
-
mPrivateFlags &= ~DRAWN;
-
mPrivateFlags |= DIRTY;
-
if (invalidateCache) {
-
mPrivateFlags |= INVALIDATED;
-
mPrivateFlags &= ~DRAWING_CACHE_VALID;
-
}
-
final AttachInfo ai = mAttachInfo;
-
final ViewParent p = mParent;
-
if (!HardwareRenderer.RENDER_DIRTY_REGIONS) {
-
if (p != null && ai != null && ai.mHardwareAccelerated) {
-
p.invalidateChild(this, null);
-
return;
-
}
-
}
-
if (p != null && ai != null) {
-
final Rect r = ai.mTmpInvalRect;
-
r.set(0, 0, mRight - mLeft, mBottom - mTop);
-
p.invalidateChild(this, r);
-
}
-
}
-
}
在這個方法中首先會調用skipInvalidate()方法來判斷當前View是否需要重繪,判斷的邏輯也比較簡單,如果View是不可見的且沒有執行任何動畫,就認爲不需要重繪了。之後會進行透明度的判斷,並給View添加一些標記位,然後在第22和29行調用ViewParent的invalidateChild()方法,這裏的ViewParent其實就是當前視圖的父視圖,因此會調用到ViewGroup的invalidateChild()方法中,代碼如下所示:
-
public final void invalidateChild(View child, final Rect dirty) {
-
ViewParent parent = this;
-
final AttachInfo attachInfo = mAttachInfo;
-
if (attachInfo != null) {
-
final boolean drawAnimation = (child.mPrivateFlags & DRAW_ANIMATION) == DRAW_ANIMATION;
-
if (dirty == null) {
-
......
-
} else {
-
......
-
do {
-
View view = null;
-
if (parent instanceof View) {
-
view = (View) parent;
-
if (view.mLayerType != LAYER_TYPE_NONE &&
-
view.getParent() instanceof View) {
-
final View grandParent = (View) view.getParent();
-
grandParent.mPrivateFlags |= INVALIDATED;
-
grandParent.mPrivateFlags &= ~DRAWING_CACHE_VALID;
-
}
-
}
-
if (drawAnimation) {
-
if (view != null) {
-
view.mPrivateFlags |= DRAW_ANIMATION;
-
} else if (parent instanceof ViewRootImpl) {
-
((ViewRootImpl) parent).mIsAnimating = true;
-
}
-
}
-
if (view != null) {
-
if ((view.mViewFlags & FADING_EDGE_MASK) != 0 &&
-
view.getSolidColor() == 0) {
-
opaqueFlag = DIRTY;
-
}
-
if ((view.mPrivateFlags & DIRTY_MASK) != DIRTY) {
-
view.mPrivateFlags = (view.mPrivateFlags & ~DIRTY_MASK) | opaqueFlag;
-
}
-
}
-
parent = parent.invalidateChildInParent(location, dirty);
-
if (view != null) {
-
Matrix m = view.getMatrix();
-
if (!m.isIdentity()) {
-
RectF boundingRect = attachInfo.mTmpTransformRect;
-
boundingRect.set(dirty);
-
m.mapRect(boundingRect);
-
dirty.set((int) boundingRect.left, (int) boundingRect.top,
-
(int) (boundingRect.right + 0.5f),
-
(int) (boundingRect.bottom + 0.5f));
-
}
-
}
-
} while (parent != null);
-
}
-
}
-
}
可以看到,這裏在第10行進入了一個while循環,當ViewParent不等於空的時候就會一直循環下去。在這個while循環當中會不斷地獲取當前佈局的父佈局,並調用它的invalidateChildInParent()方法,在ViewGroup的invalidateChildInParent()方法中主要是來計算需要重繪的矩形區域,這裏我們先不管它,當循環到最外層的根佈局後,就會調用ViewRoot的invalidateChildInParent()方法了,代碼如下所示:
-
public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
-
invalidateChild(null, dirty);
-
return null;
-
}
這裏的代碼非常簡單,僅僅是去調用了invalidateChild()方法而已,那我們再跟進去瞧一瞧吧:
-
public void invalidateChild(View child, Rect dirty) {
-
checkThread();
-
if (LOCAL_LOGV) Log.v(TAG, "Invalidate child: " + dirty);
-
mDirty.union(dirty);
-
if (!mWillDrawSoon) {
-
scheduleTraversals();
-
}
-
}
這個方法也不長,它在第6行又調用了scheduleTraversals()這個方法,那麼我們繼續跟進:
-
public void scheduleTraversals() {
-
if (!mTraversalScheduled) {
-
mTraversalScheduled = true;
-
sendEmptyMessage(DO_TRAVERSAL);
-
}
-
}
可以看到,這裏調用了sendEmptyMessage()方法,並傳入了一個DO_TRAVERSAL參數。瞭解Android異步消息處理機制的朋友們都會知道,任何一個Handler都可以調用sendEmptyMessage()方法來發送消息,並且在handleMessage()方法中接收消息,而如果你看一下ViewRoot的類定義就會發現,它是繼承自Handler的,也就是說這裏調用sendEmptyMessage()方法出的消息,會在ViewRoot的handleMessage()方法中接收到。那麼趕快看一下handleMessage()方法的代碼吧,如下所示:
-
public void handleMessage(Message msg) {
-
switch (msg.what) {
-
case DO_TRAVERSAL:
-
if (mProfile) {
-
Debug.startMethodTracing("ViewRoot");
-
}
-
performTraversals();
-
if (mProfile) {
-
Debug.stopMethodTracing();
-
mProfile = false;
-
}
-
break;
-
......
-
}
熟悉的代碼出現了!這裏在第7行調用了performTraversals()方法,這不就是我們在前面一篇文章中學到的視圖繪製的入口嗎?雖然經過了很多輾轉的調用,但是可以確定的是,調用視圖的invalidate()方法後確實會走到performTraversals()方法中,然後重新執行繪製流程。
瞭解了這些之後,我們再回過頭來看看剛纔的selectDrawable()方法中到底做了什麼才能夠控制背景圖的改變,代碼如下所示:
-
public boolean selectDrawable(int idx) {
-
if (idx == mCurIndex) {
-
return false;
-
}
-
final long now = SystemClock.uptimeMillis();
-
if (mDrawableContainerState.mExitFadeDuration > 0) {
-
if (mLastDrawable != null) {
-
mLastDrawable.setVisible(false, false);
-
}
-
if (mCurrDrawable != null) {
-
mLastDrawable = mCurrDrawable;
-
mExitAnimationEnd = now + mDrawableContainerState.mExitFadeDuration;
-
} else {
-
mLastDrawable = null;
-
mExitAnimationEnd = 0;
-
}
-
} else if (mCurrDrawable != null) {
-
mCurrDrawable.setVisible(false, false);
-
}
-
if (idx >= 0 && idx < mDrawableContainerState.mNumChildren) {
-
Drawable d = mDrawableContainerState.mDrawables[idx];
-
mCurrDrawable = d;
-
mCurIndex = idx;
-
if (d != null) {
-
if (mDrawableContainerState.mEnterFadeDuration > 0) {
-
mEnterAnimationEnd = now + mDrawableContainerState.mEnterFadeDuration;
-
} else {
-
d.setAlpha(mAlpha);
-
}
-
d.setVisible(isVisible(), true);
-
d.setDither(mDrawableContainerState.mDither);
-
d.setColorFilter(mColorFilter);
-
d.setState(getState());
-
d.setLevel(getLevel());
-
d.setBounds(getBounds());
-
}
-
} else {
-
mCurrDrawable = null;
-
mCurIndex = -1;
-
}
-
if (mEnterAnimationEnd != 0 || mExitAnimationEnd != 0) {
-
if (mAnimationRunnable == null) {
-
mAnimationRunnable = new Runnable() {
-
@Override public void run() {
-
animate(true);
-
invalidateSelf();
-
}
-
};
-
} else {
-
unscheduleSelf(mAnimationRunnable);
-
}
-
animate(true);
-
}
-
invalidateSelf();
-
return true;
-
}
這裏前面的代碼我們可以都不管,關鍵是要看到在第54行一定會調用invalidateSelf()方法,這個方法中的代碼如下所示:
-
public void invalidateSelf() {
-
final Callback callback = getCallback();
-
if (callback != null) {
-
callback.invalidateDrawable(this);
-
}
-
}
可以看到,這裏會先調用getCallback()方法獲取Callback接口的回調實例,然後再去調用回調實例的invalidateDrawable()方法。那麼這裏的回調實例又是什麼呢?觀察一下View的類定義其實你就知道了,如下所示:
-
public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Callback,
-
AccessibilityEventSource {
-
......
-
}
View類正是實現了Callback接口,所以剛纔其實調用的就是View中的invalidateDrawable()方法,之後就會按照我們前面分析的流程執行重繪邏輯,所以視圖的背景圖才能夠得到改變的。
另外需要注意的是,invalidate()方法雖然最終會調用到performTraversals()方法中,但這時measure和layout流程是不會重新執行的,因爲視圖沒有強制重新測量的標誌位,而且大小也沒有發生過變化,所以這時只有draw流程可以得到執行。而如果你希望視圖的繪製流程可以完完整整地重新走一遍,就不能使用invalidate()方法,而應該調用requestLayout()了。這個方法中的流程比invalidate()方法要簡單一些,但中心思想是差不多的,這裏也就不再詳細進行分析了。
===========================第三階段============================
自定義View的實現方法。
一些接觸Android不久的朋友對自定義View都有一絲畏懼感,總感覺這是一個比較高級的技術,但其實自定義View並不複雜,有時候只需要簡單幾行代碼就可以完成了。
如果說要按類型來劃分的話,自定義View的實現方式大概可以分爲三種,自繪控件、組合控件、以及繼承控件。那麼下面我們就來依次學習一下,每種方式分別是如何自定義View的。
一、自繪控件
自繪控件的意思就是,這個View上所展現的內容全部都是我們自己繪製出來的。繪製的代碼是寫在onDraw()方法中的。
下面我們準備來自定義一個計數器View,這個View可以響應用戶的點擊事件,並自動記錄一共點擊了多少次。新建一個CounterView繼承自View,代碼如下所示:
-
public class CounterView extends View implements OnClickListener {
-
-
private Paint mPaint;
-
-
private Rect mBounds;
-
-
private int mCount;
-
-
public CounterView(Context context, AttributeSet attrs) {
-
super(context, attrs);
-
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
-
mBounds = new Rect();
-
setOnClickListener(this);
-
}
-
-
@Override
-
protected void onDraw(Canvas canvas) {
-
super.onDraw(canvas);
-
mPaint.setColor(Color.BLUE);
-
canvas.drawRect(0, 0, getWidth(), getHeight(), mPaint);
-
mPaint.setColor(Color.YELLOW);
-
mPaint.setTextSize(30);
-
String text = String.valueOf(mCount);
-
mPaint.getTextBounds(text, 0, text.length(), mBounds);
-
float textWidth = mBounds.width();
-
float textHeight = mBounds.height();
-
canvas.drawText(text, getWidth() / 2 - textWidth / 2, getHeight() / 2
-
+ textHeight / 2, mPaint);
-
}
-
-
@Override
-
public void onClick(View v) {
-
mCount++;
-
invalidate();
-
}
-
-
}
可以看到,首先我們在CounterView的構造函數中初始化了一些數據,並給這個View的本身註冊了點擊事件,這樣當CounterView被點擊的時候,onClick()方法就會得到調用。而onClick()方法中的邏輯就更加簡單了,只是對mCount這個計數器加1,然後調用invalidate()方法。調用invalidate()方法會導致視圖進行重繪,因此onDraw()方法在稍後就將會得到調用。
既然CounterView是一個自繪視圖,那麼最主要的邏輯當然就是寫在onDraw()方法裏的了,下面我們就來仔細看一下。這裏首先是將Paint畫筆設置爲藍色,然後調用Canvas的drawRect()方法繪製了一個矩形,這個矩形也就可以當作是CounterView的背景圖吧。接着將畫筆設置爲黃色,準備在背景上面繪製當前的計數,注意這裏先是調用了getTextBounds()方法來獲取到文字的寬度和高度,然後調用了drawText()方法去進行繪製就可以了。
這樣,一個自定義的View就已經完成了,並且目前這個CounterView是具備自動計數功能的。那麼剩下的問題就是如何讓這個View在界面上顯示出來了,其實這也非常簡單,我們只需要像使用普通的控件一樣來使用CounterView就可以了。比如在佈局文件中加入如下代碼:
-
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
-
android:layout_width="match_parent"
-
android:layout_height="match_parent" >
-
-
<com.example.customview.CounterView
-
android:layout_width="100dp"
-
android:layout_height="100dp"
-
android:layout_centerInParent="true" />
-
-
</RelativeLayout>
可以看到,這裏我們將CounterView放入了一個RelativeLayout中,然後可以像使用普通控件來給CounterView指定各種屬性,比如通過layout_width和layout_height來指定CounterView的寬高,通過android:layout_centerInParent來指定它在佈局里居中顯示。只不過需要注意,自定義的View在使用的時候一定要寫出完整的包名,不然系統將無法找到這個View。
好了,就是這麼簡單,接下來我們可以運行一下程序,並不停地點擊CounterView,效果如下圖所示。
怎麼樣?是不是感覺自定義View也並不是什麼高級的技術,簡單幾行代碼就可以實現了。當然了,這個CounterView功能非常簡陋,只有一個計數功能,因此只需幾行代碼就足夠了,當你需要繪製比較複雜的View時,還是需要很多技巧的。
二、組合控件
組合控件的意思就是,我們並不需要自己去繪製視圖上顯示的內容,而只是用系統原生的控件就好了,但我們可以將幾個系統原生的控件組合到一起,這樣創建出的控件就被稱爲組合控件。
舉個例子來說,標題欄就是個很常見的組合控件,很多界面的頭部都會放置一個標題欄,標題欄上會有個返回按鈕和標題,點擊按鈕後就可以返回到上一個界面。那麼下面我們就來嘗試去實現這樣一個標題欄控件。
新建一個title.xml佈局文件,代碼如下所示:
-
<?xml version="1.0" encoding="utf-8"?>
-
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
-
android:layout_width="match_parent"
-
android:layout_height="50dp"
-
android:background="#ffcb05" >
-
-
<Button
-
android:id="@+id/button_left"
-
android:layout_width="60dp"
-
android:layout_height="40dp"
-
android:layout_centerVertical="true"
-
android:layout_marginLeft="5dp"
-
android:background="@drawable/back_button"
-
android:text="Back"
-
android:textColor="#fff" />
-
-
<TextView
-
android:id="@+id/title_text"
-
android:layout_width="wrap_content"
-
android:layout_height="wrap_content"
-
android:layout_centerInParent="true"
-
android:text="This is Title"
-
android:textColor="#fff"
-
android:textSize="20sp" />
-
-
</RelativeLayout>
在這個佈局文件中,我們首先定義了一個RelativeLayout作爲背景佈局,然後在這個佈局裏定義了一個Button和一個TextView,Button就是標題欄中的返回按鈕,TextView就是標題欄中的顯示的文字。
接下來創建一個TitleView繼承自FrameLayout,代碼如下所示:
-
public class TitleView extends FrameLayout {
-
-
private Button leftButton;
-
-
private TextView titleText;
-
-
public TitleView(Context context, AttributeSet attrs) {
-
super(context, attrs);
-
LayoutInflater.from(context).inflate(R.layout.title, this);
-
titleText = (TextView) findViewById(R.id.title_text);
-
leftButton = (Button) findViewById(R.id.button_left);
-
leftButton.setOnClickListener(new OnClickListener() {
-
@Override
-
public void onClick(View v) {
-
((Activity) getContext()).finish();
-
}
-
});
-
}
-
-
public void setTitleText(String text) {
-
titleText.setText(text);
-
}
-
-
public void setLeftButtonText(String text) {
-
leftButton.setText(text);
-
}
-
-
public void setLeftButtonListener(OnClickListener l) {
-
leftButton.setOnClickListener(l);
-
}
-
-
}
TitleView中的代碼非常簡單,在TitleView的構建方法中,我們調用了LayoutInflater的inflate()方法來加載剛剛定義的title.xml佈局。
接下來調用findViewById()方法獲取到了返回按鈕的實例,然後在它的onClick事件中調用finish()方法來關閉當前的Activity,也就相當於實現返回功能了。
另外,爲了讓TitleView有更強地擴展性,我們還提供了setTitleText()、setLeftButtonText()、setLeftButtonListener()等方法,分別用於設置標題欄上的文字、返回按鈕上的文字、以及返回按鈕的點擊事件。
到了這裏,一個自定義的標題欄就完成了,那麼下面又到了如何引用這個自定義View的部分,其實方法基本都是相同的,在佈局文件中添加如下代碼:
-
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
-
xmlns:tools="http://schemas.android.com/tools"
-
android:layout_width="match_parent"
-
android:layout_height="match_parent" >
-
-
<com.example.customview.TitleView
-
android:id="@+id/title_view"
-
android:layout_width="match_parent"
-
android:layout_height="wrap_content" >
-
</com.example.customview.TitleView>
-
-
</RelativeLayout>
這樣就成功將一個標題欄控件引入到佈局文件中了,運行一下程序,效果如下圖所示:
現在點擊一下Back按鈕,就可以關閉當前的Activity了。如果你想要修改標題欄上顯示的內容,或者返回按鈕的默認事件,只需要在Activity中通過findViewById()方法得到TitleView的實例,然後調用setTitleText()、setLeftButtonText()、setLeftButtonListener()等方法進行設置就OK了。
三、繼承控件
繼承控件的意思就是,我們並不需要自己重頭去實現一個控件,只需要去繼承一個現有的控件,然後在這個控件上增加一些新的功能,就可以形成一個自定義的控件了。這種自定義控件的特點就是不僅能夠按照我們的需求加入相應的功能,還可以保留原生控件的所有功能。
爲了能夠加深大家對這種自定義View方式的理解,下面我們再來編寫一個新的繼承控件。ListView相信每一個Android程序員都一定使用過,這次我們準備對ListView進行擴展,加入在ListView上滑動就可以顯示出一個刪除按鈕,點擊按鈕就會刪除相應數據的功能。
首先需要準備一個刪除按鈕的佈局,新建delete_button.xml文件,代碼如下所示:
-
<?xml version="1.0" encoding="utf-8"?>
-
<Button xmlns:android="http://schemas.android.com/apk/res/android"
-
android:id="@+id/delete_button"
-
android:layout_width="wrap_content"
-
android:layout_height="wrap_content"
-
android:background="@drawable/delete_button" >
-
-
</Button>
這個佈局文件很簡單,只有一個按鈕而已,並且我們給這個按鈕指定了一張刪除背景圖。
接着創建MyListView繼承自ListView,這就是我們自定義的View了,代碼如下所示:
-
public class MyListView extends ListView implements OnTouchListener,
-
OnGestureListener {
-
-
private GestureDetector gestureDetector;
-
-
private OnDeleteListener listener;
-
-
private View deleteButton;
-
-
private ViewGroup itemLayout;
-
-
private int selectedItem;
-
-
private boolean isDeleteShown;
-
-
public MyListView(Context context, AttributeSet attrs) {
-
super(context, attrs);
-
gestureDetector = new GestureDetector(getContext(), this);
-
setOnTouchListener(this);
-
}
-
-
public void setOnDeleteListener(OnDeleteListener l) {
-
listener = l;
-
}
-
-
@Override
-
public boolean onTouch(View v, MotionEvent event) {
-
if (isDeleteShown) {
-
itemLayout.removeView(deleteButton);
-
deleteButton = null;
-
isDeleteShown = false;
-
return false;
-
} else {
-
return gestureDetector.onTouchEvent(event);
-
}
-
}
-
-
@Override
-
public boolean onDown(MotionEvent e) {
-
if (!isDeleteShown) {
-
selectedItem = pointToPosition((int) e.getX(), (int) e.getY());
-
}
-
return false;
-
}
-
-
@Override
-
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
-
float velocityY) {
-
if (!isDeleteShown && Math.abs(velocityX) > Math.abs(velocityY)) {
-
deleteButton = LayoutInflater.from(getContext()).inflate(
-
R.layout.delete_button, null);
-
deleteButton.setOnClickListener(new OnClickListener() {
-
@Override
-
public void onClick(View v) {
-
itemLayout.removeView(deleteButton);
-
deleteButton = null;
-
isDeleteShown = false;
-
listener.onDelete(selectedItem);
-
}
-
});
-
itemLayout = (ViewGroup) getChildAt(selectedItem
-
- getFirstVisiblePosition());
-
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(
-
LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
-
params.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
-
params.addRule(RelativeLayout.CENTER_VERTICAL);
-
itemLayout.addView(deleteButton, params);
-
isDeleteShown = true;
-
}
-
return false;
-
}
-
-
@Override
-
public boolean onSingleTapUp(MotionEvent e) {
-
return false;
-
}
-
-
@Override
-
public void onShowPress(MotionEvent e) {
-
-
}
-
-
@Override
-
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
-
float distanceY) {
-
return false;
-
}
-
-
@Override
-
public void onLongPress(MotionEvent e) {
-
}
-
-
public interface OnDeleteListener {
-
-
void onDelete(int index);
-
-
}
-
-
}
由於代碼邏輯比較簡單,我就沒有加註釋。這裏在MyListView的構造方法中創建了一個GestureDetector的實例用於監聽手勢,然後給MyListView註冊了touch監聽事件。然後在onTouch()方法中進行判斷,如果刪除按鈕已經顯示了,就將它移除掉,如果刪除按鈕沒有顯示,就使用GestureDetector來處理當前手勢。
當手指按下時,會調用OnGestureListener的onDown()方法,在這裏通過pointToPosition()方法來判斷出當前選中的是ListView的哪一行。當手指快速滑動時,會調用onFling()方法,在這裏會去加載delete_button.xml這個佈局,然後將刪除按鈕添加到當前選中的那一行item上。注意,我們還給刪除按鈕添加了一個點擊事件,當點擊了刪除按鈕時就會回調onDeleteListener的onDelete()方法,在回調方法中應該去處理具體的刪除操作。
好了,自定義View的功能到此就完成了,接下來我們需要看一下如何才能使用這個自定義View。首先需要創建一個ListView子項的佈局文件,新建my_list_view_item.xml,代碼如下所示:
-
<?xml version="1.0" encoding="utf-8"?>
-
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
-
android:layout_width="match_parent"
-
android:layout_height="match_parent"
-
android:descendantFocusability="blocksDescendants"
-
android:orientation="vertical" >
-
-
<TextView
-
android:id="@+id/text_view"
-
android:layout_width="wrap_content"
-
android:layout_height="50dp"
-
android:layout_centerVertical="true"
-
android:gravity="left|center_vertical"
-
android:textColor="#000" />
-
-
</RelativeLayout>
然後創建一個適配器MyAdapter,在這個適配器中去加載my_list_view_item佈局,代碼如下所示:
-
public class MyAdapter extends ArrayAdapter<String> {
-
-
public MyAdapter(Context context, int textViewResourceId, List<String> objects) {
-
super(context, textViewResourceId, objects);
-
}
-
-
@Override
-
public View getView(int position, View convertView, ViewGroup parent) {
-
View view;
-
if (convertView == null) {
-
view = LayoutInflater.from(getContext()).inflate(R.layout.my_list_view_item, null);
-
} else {
-
view = convertView;
-
}
-
TextView textView = (TextView) view.findViewById(R.id.text_view);
-
textView.setText(getItem(position));
-
return view;
-
}
-
-
}
到這裏就基本已經完工了,下面在程序的主佈局文件裏面引入MyListView這個控件,如下所示:
-
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
-
xmlns:tools="http://schemas.android.com/tools"
-
android:layout_width="match_parent"
-
android:layout_height="match_parent" >
-
-
<com.example.customview.MyListView
-
android:id="@+id/my_list_view"
-
android:layout_width="match_parent"
-
android:layout_height="wrap_content" >
-
</com.example.customview.MyListView>
-
-
</RelativeLayout>
最後在Activity中初始化MyListView中的數據,並處理了onDelete()方法的刪除邏輯,代碼如下所示:
-
public class MainActivity extends Activity {
-
-
private MyListView myListView;
-
-
private MyAdapter adapter;
-
-
private List<String> contentList = new ArrayList<String>();
-
-
@Override
-
protected void onCreate(Bundle savedInstanceState) {
-
super.onCreate(savedInstanceState);
-
requestWindowFeature(Window.FEATURE_NO_TITLE);
-
setContentView(R.layout.activity_main);
-
initList();
-
myListView = (MyListView) findViewById(R.id.my_list_view);
-
myListView.setOnDeleteListener(new OnDeleteListener() {
-
@Override
-
public void onDelete(int index) {
-
contentList.remove(index);
-
adapter.notifyDataSetChanged();
-
}
-
});
-
adapter = new MyAdapter(this, 0, contentList);
-
myListView.setAdapter(adapter);
-
}
-
-
private void initList() {
-
contentList.add("Content Item 1");
-
contentList.add("Content Item 2");
-
contentList.add("Content Item 3");
-
contentList.add("Content Item 4");
-
contentList.add("Content Item 5");
-
contentList.add("Content Item 6");
-
contentList.add("Content Item 7");
-
contentList.add("Content Item 8");
-
contentList.add("Content Item 9");
-
contentList.add("Content Item 10");
-
contentList.add("Content Item 11");
-
contentList.add("Content Item 12");
-
contentList.add("Content Item 13");
-
contentList.add("Content Item 14");
-
contentList.add("Content Item 15");
-
contentList.add("Content Item 16");
-
contentList.add("Content Item 17");
-
contentList.add("Content Item 18");
-
contentList.add("Content Item 19");
-
contentList.add("Content Item 20");
-
}
-
-
}
這樣就把整個例子的代碼都完成了,現在運行一下程序,會看到MyListView可以像ListView一樣,正常顯示所有的數據,但是當你用手指在MyListView的某一行上快速滑動時,就會有一個刪除按鈕顯示出來,如下圖所示:
點擊一下刪除按鈕就可以將第6行的數據刪除了。此時的MyListView不僅保留了ListView原生的所有功能,還增加了一個滑動進行刪除的功能,確實是一個不折不扣的繼承控件。