原文出處——>[Android控件TextView的實現原理分析](http://blog.csdn.net/luoshengyang/article/details/8636153)
在前面一個系列的文章中,我們以窗口為單位,分析了WindowManagerService服務的實現。同時,在再前面一個系列的文章中,我們又分析了窗口的組成。簡單來說,窗口就是由一系列的視圖按照一定的布局組織起來的。實際上,每一個視圖都是一個控件,這些控制可以將自己的UI繪制在窗口的繪圖表面上,同時還可以與用戶進行交互,即獲得用戶的鍵盤或者觸摸屏輸入。在本文中,我們就詳細分析窗口控件的上述實現原理。
由于Android系統提供的控件比較多,因此我們只能挑一個比較有代表的控件進行分析。這個比較有代表性的控件便是TextView,其它的一些基礎控件,例如Button、EditText和CheckBox等,都是直接或者間接地以它為父類的。每一個控件的實現都是相當復雜的,不過基本上都是一些細節問題,而且不同的控件有不同的實現細節,因此,本文并不打算詳細地分析TextView的具體實現,而是從所有控件為了實現自己的功能而需要的東西出發,去分析TextView的實現框架。
那么,控件為了實現自己的功能而需要的東西是什么呢?有兩個材料是必不可少的。第一個材料是畫布,第二個材料是用戶輸入。有畫布才能繪制UI,而有用戶輸入才能與用戶進行交互。因此,接下來我們主要分析TextView的繪制流程,以及它獲得用戶輸入的過程。用戶輸入主要包括鍵盤輸入以及觸摸屏輸入,本文主要關注的是鍵盤輸入。觸摸屏輸入與鍵盤輸入的獲取過程是類似的,讀者如果有興趣的話,可以參照本文的內容來自己研究一下。
從前面Android應用程序窗口(Activity)實現框架簡要介紹和學習計劃這個系列的文章可以知道,應用程序窗口,即Activity窗口,是由一個PhoneWindow對象,一個DecorView對象,以及一個ViewRoot對象來描述的。其中,PhoneWindow對象用來描述窗口對象,DecorView對象用來描述窗口的頂層視圖,ViewRoot對象除了用來與WindowManagerService服務通信之外,還用來接收用戶輸入。窗口控件本身也是一個視圖,即一個View對象,它們是以樹形結構組織在一起形成整個窗口的UI的。為了簡單起見,本文假設要分析的TextView控件是直接以窗口的頂層視圖為父視圖的,即以DecorView為父視圖,如圖1所示:
:-: 
圖1 窗口結構示意圖以及DecorView、TextView的類關系圖
圖1顯示的是一個包含了TextView控件的Activity窗口的結構示意圖以及DecorView、TextView的簡單類關系圖,從中可以看出:
1. 用戶輸入首先是由ViewRoot接收,然后再分發給TextView處理;
2. DecorView是一個視圖容器,因此,它是從ViewGroup繼承下來,而ViewGroup本身又是從View繼承下來的;
3. TextView是一個簡單視圖,因此,它是直接繼承了View。
接下來,我們就以圖1所示的Activity窗口為例,來分析TextView控件的UI繪制框架及其獲得鍵盤輸入的過程。
#### **一. TextView控件的UI繪制框架**
從前面Android應用程序窗口(Activity)的測量(Measure)、布局(Layout)和繪制(Draw)過程分析一文可以知道,Activity窗口的UI繪制操作分為三步來走,分別是測量、布局和繪制。
1. 測量
為了能告訴父視圖自己的所占據的空間的大小,所有控件都必須要重寫父類View的成員函數onMeasure。
TextView類的成員函數onMeasure的實現如下所示:
~~~
public class TextView extends View implements ViewTreeObserver.OnPreDrawListener {
......
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width;
int height;
//計算TextView控件的寬度和高度
......
setMeasuredDimension(width, height);
}
......
}
~~~
這個函數定義在文件frameworks/base/core/java/android/widget/TextView.java中。
參數widthMeasureSpec和heightMeasureSpec分別用來描述寬度測量規范和高度測量規范。測量規范使用一個int值來表法,這個int值包含了兩個分量。
第一個是mode分量,使用最高2位來表示。測量模式有三種,分別是MeasureSpec.UNSPECIFIED(0)、MeasureSpec.EXACTLY(1)、和MeasureSpec.AT_MOST(2)。
第二個是size分量,使用低30位來表示。當mode分量等于MeasureSpec.EXACTLY時,size分量的值就是父視圖要求當前控件要設置的寬度或者高度;當mode分量等于MeasureSpec.AT_MOST時,size分量的值就是父視圖限定當前控件可以設置的最大寬度或者高度;當mode分量等于MeasureSpec.UNSPECIFIED時,父視圖不限定當前控件所設置的寬度或者高度,這時候當前控件一般就按照實際需求來設置自己的寬度和高度。
TextView類的成員函數onMeasure根據上述規則計算好自己的寬度wdith和高度height之后,必須要調用從父類View繼承下來的成員函數setMeasuredDimension來通知父視圖它所要設置的寬度和高度,否則的話,該函數調用結束之后,就會拋出一個類型為IllegalStateException的異常。
2. 布局
前面的測量工作實際上是確定了控件的大小,但是控件的位置還未確定。控件的位置是通過布局這個操作來完成的。
控件是按照樹形結構組織在一起的,其中,子控件的位置由父控件來設置,也就是說,只有容器類控件才需要執行布局操作,這是通過重寫父類View的成員函數onLayout來實現的。從Activity窗口的結構可以知道,它的頂層視圖是一個DecorView,這是一個容器類控件。Activity窗口的布局操作就是從其頂層視圖開始執行的,每碰到一個容器類的子控件,就調用它的成員函數onLayout來讓它有機會對自己的子控件的位置進行設置,依次類推。
我們常見的FrameLayout、LinearLayout、RelativeLayout、TableLayout和AbsoluteLayout,都是屬于容器類控件,因此,它們都需要重寫父類View的成員函數onLayout。由于TextView控件不是容器類控件,因此,它可以不重寫父類View的成員函數onLayout。
3. 繪制
有了前面兩個操作之后,控件的位置的大小就確定下來了,接下來就可以對它們的UI進行繪制了。控件為了能夠繪制自己的UI,必須要重寫父類View的成員函數onDraw。
TextView類的成員函數onDraw的實現如下所示:
~~~
public class TextView extends View implements ViewTreeObserver.OnPreDrawListener {
......
@Override
protected void onDraw(Canvas canvas) {
//在畫布canvas上繪制UI
......
}
......
}
~~~
這個函數定義在文件frameworks/base/core/java/android/widget/TextView.java中。
參數canvas描述的是一塊畫布,控件的UI就是繪制在這塊畫布上面的。畫布提供了豐富的接口來繪制UI,例如畫線(drawLine)、畫圓(drawCircle)和貼圖(drawBitmap)等等。有了這些UI畫圖接口之后,就可以隨心所欲地繪制控件的UI了。
從前面Android應用程序窗口(Activity)的測量(Measure)、布局(Layout)和繪制(Draw)過程分析一文可以知道,Java層的Canvas實際上是封裝了C++層的SkCanvas。C++層的SkCanvas內部有一塊圖形緩沖區,這塊圖形緩沖區就是窗口的繪圖表面(Surface)里面的那塊圖形緩沖區。
從前面Android應用程序與SurfaceFlinger服務的關系概述和學習計劃一文可以知道,窗口的繪圖表面里面的那塊圖形緩沖區實際上是一塊匿名共享內存,它是SurfaceFlinger服務負責創建的。SurfaceFlinger服務創建完成這塊匿名共享內存之后,就會將其返回給窗口所運行在的進程。窗口所運行在的進程獲得了這塊匿名共享內存之后,就會映射到自己的進程空間來,因此,窗口的控件就可以在本進程內訪問這塊匿名共享內存了,實際上就是往這塊匿名共享內存填入UI數據。注意,這個過程執行完成之后,控件的UI還沒有反映到屏幕上來,因為這時候將控件的UI數據填入到圖形緩沖區而已。
從前面Android窗口管理服務WindowManagerService的簡要介紹和學習計劃一文可以知道,窗口的UI的顯示是WindowManagerService服務來控制的。因此,當窗口的所有控件都繪制完成自己的UI之后,窗口就會向WindowManagerService服務發送一個Binder進程間程通信請求。WindowManagerService服務接收到這個Binder進程間程通信請求之后,就會請求SurfaceFlinger服務刷新相應的窗口的UI。SurfaceFlinger服務刷新窗口UI的過程可以參考前面Android系統Surface機制的SurfaceFlinger服務渲染應用程序UI的過程分析一文。
從上面的描述就可以看出,控件的UI雖然是在一塊簡單的畫布進行繪制,但是其中蘊含了豐富的知識點,并且需要應用程序進程、WindowManagerService服務和SurfaceFlinger服務三方緊密而有序的配合。
如果我們仔細閱讀Android應用程序窗口(Activity)的測量(Measure)、布局(Layout)和繪制(Draw)過程分析一文,還可以得出以下兩個結論:
* (1). 一個窗口的所有控件的UI都是繪制在窗口的繪圖表面上的,也就是說,一個窗口的所有控件的UI數據都是填寫在同一塊圖形緩沖區中;
* (2). 一個窗口的所有控件的UI的繪制操作是在主線程中執行的,事實上,所有與UI相關的操作都是必須是要在主線程中執行,否則的話,就會拋出一個類型為CalledFromWrongThreadException的異常來。
為什么要規定所有與UI相關的操作都必須在主線程中執行呢?我們知道,這些與UI相關的操作都涉及到大量的控件內部狀態以及需要訪問窗口的繪圖表面,也就是說,要大量地訪問控件類的成員變量以及窗口繪圖表面里面的圖形緩沖區,因此,如果不將這些與UI相關的操作限定在同一個線程中執行的話,那么就會涉及到線程同步問題。線程同步的開銷是很大的,因此,就要保證那些與UI相關的操作都在同一個線程中執行。這個負責執行UI相關操作的線程便是應用程序進程的主線程,因此我們也將應用程序進程的主線程稱為UI線程。
我們知道,應用程序進程的主線程除了負責執行與UI相關的操作之外,還負責響應用戶的輸入,因此,我們就要盡量地避免執行很耗時的UI操作,否則的話,系統就會由于應用程序進程的主線程無法及時響應用戶輸入而彈出ANR對話框。
那么,有沒有辦法讓某一個控件的UI享有獨立的圖形緩沖區呢?也就是這個控件不將自己的UI數據填入到它的宿主窗口的繪圖表面的圖形緩沖區里面去。如果可以的話,那么我們就可以在另外一個獨立的線程中繪制該控件的UI。這樣做的好處是顯而易見——可以在這個獨立的線程執行相對比較耗時的UI繪制操作而不會導致主線程無法及時響應用戶輸入。答案是肯定的,在接下來的一篇文章中,我們就分析一個可以具有獨立圖形緩沖區的控件——SurfaceView。
#### **二. TextView控件獲取鍵盤輸入的過程分析**
從前面Android應用程序鍵盤(Keyboard)消息處理機制分析一文可以知道,每一個窗口的創建的時候,都會與系統的輸入管理器建立一個用戶輸入接收通道。輸入管理器在啟動兩個線程,其中一個用來監控用戶輸入,即監控用戶是否按下或者放開了鍵盤按鍵,或者是否觸摸了屏幕,另外一個用來將監控到的用戶輸入事件分發給當前激活的窗口來處理,而這個分發過程就是通過前面建立的通道來進行的。
當前激活的窗口接收到輸入管理器分發過來的用戶輸入事件之后,就會該事件封裝成一個消息發送到當前激活的窗口所運行在的應用程序進程的主線程的消息隊列中去。等到這個消息被處理的時候,就會調用與當前激活的窗口所關聯的一個ViewRoot對象的成員函數deliverKeyEvent或者deliverPointerEvent來將前面接收到的用戶輸入分發給合適的控件。其中,ViewRoot類的成員函數deliverKeyEvent負責分發鍵盤輸入事件,而ViewRoot類的成員函數deliverPointerEvent負責分發觸摸屏輸入事件。
接下來,我們就從ViewRoot類的成員函數deliverKeyEvent開始,分析一個TextView控件獲得鍵盤輸入的過程(獲得觸摸屏輸入的過程是類似的),如圖2所示:
:-: 
圖2 TextView控件獲得鍵盤輸入的過程
這個過程可以分為14個步驟,接下來我們就詳細分析每一個步驟。
**Step 1. ViewRoot.deliverKeyEvent**
~~~
public final class ViewRoot extends Handler implements ViewParent,
View.AttachInfo.Callbacks {
......
private void deliverKeyEvent(KeyEvent event, boolean sendDone) {
// If mView is null, we just consume the key event because it doesn't
// make sense to do anything else with it.
boolean handled = mView != null
? mView.dispatchKeyEventPreIme(event) : true;
if (handled) {
if (sendDone) {
finishInputEvent();
}
return;
}
// If it is possible for this window to interact with the input
// method window, then we want to first dispatch our key events
// to the input method.
if (mLastWasImTarget) {
InputMethodManager imm = InputMethodManager.peekInstance();
if (imm != null && mView != null) {
int seq = enqueuePendingEvent(event, sendDone);
......
imm.dispatchKeyEvent(mView.getContext(), seq, event,
mInputMethodCallback);
return;
}
}
deliverKeyEventToViewHierarchy(event, sendDone);
}
......
}
~~~
這個函數定義在文件frameworks/base/core/java/android/view/ViewRoot.java中。
參數event描述的是窗口接收到的鍵盤事件,另外一個參數sendDone表示該鍵盤事件處理完成后,是否需要向系統的輸入管理器發送一個通知。
ViewRoot類的成員變量mView描述的是窗口的頂層視圖,即它指向的是一個DecorView對象,ViewRoot類的成員函數deliverKeyEvent首先是調用它的成員函數dispatchKeyEventPreIme來讓它優先于輸入法處理參數event所描述的鍵盤事件。如果這個DecorView對象的成員函數dispatchKeyEventPreIme的返回值handled等于true,那么就說明參數event所描述的鍵盤事件已經處理完畢,即ViewRoot類的成員函數deliverKeyEvent不用往下執行了。在這種情況下,如果參數sendDone的值等于true,那么ViewRoot類的成員函數deliverKeyEvent在返回之前,還會調用成員函數finishInputEvent來通知系統的輸入管理器,當前激活的窗口已經處理完成剛剛發生的鍵盤事件了。在接下來的Step 2到Step 4中,我們再詳細分析鍵盤事件優先于輸入法分發給窗口處理的過程。
假設窗口不在輸入法前面攔截參數event所描述的鍵盤事件,接下來ViewRoot類的成員函數deliverKeyEvent就會將該鍵盤事件分發給輸入法處理,這個分發過程如下所示:
1. 調用InputMethodManager類的靜態成員函數peekInstance獲得一個類型為InputMethodManager輸入法管理器imm;
2. 調用ViewRoot類的成員函數enqueuePendingEvent將參數event所描述的鍵盤事件緩存起來,等到輸入法處理完成該鍵盤事件之后,再繼續對它進行處理;
3. 調用第1步獲得的輸入法管理器imm的成員函數dispatchKeyEvent來將參數event所描述的鍵盤事件分發給輸入法處理。
這里有兩個地方是需要注意的。第一個地方是只有當前窗口正在顯示輸入法的情況下,ViewRoot類的成員函數deliverKeyEvent才會將參數event所描述的鍵盤事件分發給輸入法處理,這是通過檢查ViewRoot類的成員變量mLastWasImTarget的值是否等于true來確定的。第二個地方是在將參數event所描述的鍵盤事件分發給輸入法處理時,ViewRoot類的成員函數deliverKeyEvent會同時傳遞一個類型為InputMethodCallback的回調接口給輸入法,以便輸入法處理完成參數event所描述的鍵盤事件之后,可以調用這個回調接口的成員函數finishedEvent來向窗口發送一個鍵盤事件處理完成通知。這個類型為InputMethodCallback的回調接口就保存在ViewRoot類的成員變量mInputMethodCallback中,當它的成員函數finishedEvent被調用的時候,它就會調用ViewRoot類的成員函數deliverKeyEventToViewHierarchy來繼續將參數event所描述的鍵盤事件分發給窗口處理。
如果窗口當前不需要與輸入法交互,即ViewRoot類的成員變量mLastWasImTarget的值等于false,那么ViewRoot類的成員函數deliverKeyEvent就會直接調用成員函數deliverKeyEventToViewHierarchy來將參數event所描述的鍵盤事件分發給窗口處理。
接下來,我們就先析窗口在輸入法之前處理鍵盤輸入的過程,接著再分析窗口在輸入法之后處理鍵盤輸入的過程。
從前面的分析可以知道:ViewRoot類的成員函數deliverKeyEvent是通過調用DecorView類的成員函數,dispatchKeyEventPreIme來將獲得的鍵盤輸入優先于輸入法分發給窗口處理的。DecorView類的成員函數dispatchKeyEventPreIme是從父類ViewGroup繼承下來的,因此,接下來我們就繼續分析ViewGroup類的成員函數dispatchKeyEventPreIme的實現。
**Step 2. ViewGroup.dispatchKeyEventPreIme**
~~~
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
......
// The view contained within this ViewGroup that has or contains focus.
private View mFocused;
......
@Override
public boolean dispatchKeyEventPreIme(KeyEvent event) {
if ((mPrivateFlags & (FOCUSED | HAS_BOUNDS)) == (FOCUSED | HAS_BOUNDS)) {
return super.dispatchKeyEventPreIme(event);
} else if (mFocused != null && (mFocused.mPrivateFlags & HAS_BOUNDS) == HAS_BOUNDS) {
return mFocused.dispatchKeyEventPreIme(event);
}
return false;
}
......
}
~~~
這個函數定義在文件frameworks/base/core/java/android/view/ViewGroup.java中。
ViewGroup類的成員函數dispatchKeyEventPreIme首先是檢查當前正在處理的視圖容器是否能夠獲得焦點。如果能夠獲得焦點的話,那么ViewGroup類的成員變量mPrivateFlags的FOCUSED位就會等于1。在當前正在處理的視圖容器能夠獲得焦點的情況下,還要檢查正在處理的視圖容器是否已經計算過大小了,即檢查ViewGroup類的成員變量mPrivateFlags的HAS_BOUNDS位是否等于1。只有在已經計算過大小并且能夠獲得焦點的情況下,那么正在處理的視圖容器才有資格處理參數event所描述的鍵盤事件。注意,正在處理的視圖容器是通過調用其父類View的成員函數dispatchKeyEventPreIme來處理參數event所描述的鍵盤事件的。
如果當前正在處理的視圖容器沒有資格處理參數event所描述的鍵盤事件,但是它有一個能夠獲得焦點的子視圖,并且這個子視圖的大小也是已經計算好了的,那么ViewGroup類的成員函數dispatchKeyEventPreIme就會將參數event所描述的鍵盤事件分發給該子視圖處理。當前正在處理的視圖容器能夠獲得焦點的子視圖是通過ViewGroup類的成員變量mFocused來描述的,通過調用這個成員變量所描述的一個View對象的成員函數dispatchKeyEventPreIme即可將參數event所描述的鍵盤事件分發給夠獲得焦點的子視圖處理。
一個視圖容器是如何知道它的焦點子視圖的呢?我們知道,當我們在屏幕上觸摸一個窗口時,就會發生一個Pointer事件。這個Pointer事件關聯有一個觸摸點,通過檢查這個觸摸點當前是包含在窗口頂層視圖的哪一個子視圖里面,就可以知道哪一個子視圖是焦點子視圖了。
從上面的分析可以知道,無論是當前正在處理的視圖容器獲得焦點,還是它的子視圖獲得焦點,最終都是通過調用View類的成員函數dispatchKeyEventPreIme來在輸入法之前處理參數event所描述的鍵盤事件,因此,接下來我們就繼續分析View類的成員函數dispatchKeyEventPreIme的實現。
**Step 3. View.dispatchKeyEventPreIme**
~~~
public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {
......
public boolean dispatchKeyEventPreIme(KeyEvent event) {
return onKeyPreIme(event.getKeyCode(), event);
}
......
}
~~~
這個函數定義在文件frameworks/base/core/java/android/view/View.java中。
View類的成員函數dispatchKeyEventPreIme的實現很簡單,它只是通過調用另外一個成員函數onKeyPreIme來在輸入法之前處理參數event所描述的鍵盤事件。
**Step 4. View.onKeyPreIme**
~~~
public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {
......
public boolean onKeyPreIme(int keyCode, KeyEvent event) {
return false;
}
......
}
~~~
這個函數定義在文件frameworks/base/core/java/android/view/View.java中。
View類的成員函數onKeyPreIme默認是不會在輸入法之前處理參數event所描述的鍵盤事件的,因此,我們在實現自己的控件的時候,如果需要在輸入法之前處理鍵盤輸入,那么就必須重寫父類View的成員函數onKeyPreIme。在重寫父類View的成員函數onKeyPreIme來處理一個鍵盤事件的時候,如果不希望這個鍵盤事件分發給輸入法處理,那么就返回一個true值,否則的話,就返回一個false值。
我們假設當前獲得焦點的是圖1所示的TextView控件,但是由于TextView類沒有重寫其父類View的成員函數onKeyPreIme,因此,參數event所描述的鍵盤事件接下來就會繼續分發給輸入法或者當前激活的窗口處理。
這一步執行完成之后,回到前面的Step 1中,即ViewRoot類的成員函數deliverKeyEvent中,無論接下來是否需要先將一個鍵盤事件分發給輸入法處理,最終都會調用到ViewRoot類的成員函數deliverKeyEventToViewHierarchy來繼續將該鍵盤事件分發給當前激活的窗口處理。
**Step 5. ViewRoot.deliverKeyEventToViewHierarchy**
~~~
public final class ViewRoot extends Handler implements ViewParent,
View.AttachInfo.Callbacks {
......
private void deliverKeyEventToViewHierarchy(KeyEvent event, boolean sendDone) {
try {
if (mView != null && mAdded) {
final int action = event.getAction();
boolean isDown = (action == KeyEvent.ACTION_DOWN);
......
boolean keyHandled = mView.dispatchKeyEvent(event);
if (!keyHandled && isDown) {
int direction = 0;
switch (event.getKeyCode()) {
case KeyEvent.KEYCODE_DPAD_LEFT:
direction = View.FOCUS_LEFT;
break;
case KeyEvent.KEYCODE_DPAD_RIGHT:
direction = View.FOCUS_RIGHT;
break;
case KeyEvent.KEYCODE_DPAD_UP:
direction = View.FOCUS_UP;
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
direction = View.FOCUS_DOWN;
break;
}
if (direction != 0) {
View focused = mView != null ? mView.findFocus() : null;
if (focused != null) {
View v = focused.focusSearch(direction);
......
if (v != null && v != focused) {
......
focusPassed = v.requestFocus(direction, mTempRect);
}
......
}
}
}
}
} finally {
if (sendDone) {
finishInputEvent();
}
......
}
}
......
}
~~~
這個函數定義在文件frameworks/base/core/java/android/view/ViewRoot.java中。
ViewRoot類的成員函數deliverKeyEventToViewHierarchy首先將參數event所描述的鍵盤事件交給當前激活的窗口的頂層視圖來處理,這是通過調用ViewRoot類的成員變量mView所描述的一個DecorView對象的成員函數dispatchKeyEvent來實現的。
如果當前激活的窗口的頂層視圖在處理完成參數event所描述的鍵盤事件之后,希望該鍵盤事件還能繼續被ViewRoot類的成員函數deliverKeyEventToViewHierarchy處理,那么前面調用DecorView類的成員函數dispatchKeyEvent得到的返回值keyHandled的值就會等于false。在這種情況下,如果參數event描述的是一個按下的鍵盤事件,即變量isDown的值等于true,那么ViewRoot類的成員函數deliverKeyEventToViewHierarchy就會繼續檢查參數event描述的是否是一個DPAD事件。如果是的話,那么就可能需要改變窗口當前的焦點子視圖。
如果參數event描述的是一個DPAD事件,那么最終得到的變量direction的值就不會等于0,并且它描述的是當前按下的是哪一個方向的DPAD鍵。假設這時候窗口已經有一個焦點子視圖,即調用ViewRoot類的成員變量mView所描述的一個DecorView對象的成員函數findFocus的返回值focused不等于null,那么接下來就要根據變量direction的值來決定下一個焦點子視圖是誰。例如,假設變量direction的值等于View.FOCUS_LEFT,那么就表示在當前的焦點子視圖focused的左邊查找一個最靠近的子視圖作為下一個焦點子視圖,這是通過調用當前焦點子視圖focused的成員函數focusSearch來實現的。
一旦找到了下一個焦點子視圖v,并且該子視圖不是當前的焦點子視圖focused,那么ViewRoot類的成員函數deliverKeyEventToViewHierarchy就需要將子視圖v設置為焦點子視圖,這是通過調用變量v所描述的一個View對象的成員函數requestFocus來實現的。
通過前面的操作,參數event所描述的鍵盤事件就處理完成了。如果這時候參數sendDone的值等于true,那么就表示需要通知系統的輸入管理器,參數event所描述的鍵盤事件已經處理完成了,這是通過調用ViewRoot類的成員函數finishInputEvent來實現的。
接下來,我們就繼續分析DecorView類的成員函數dispatchKeyEvent的實現,以便可以了解窗口的頂層視圖分發鍵盤事件的過程。
**Step 6. DecorView.dispatchKeyEvent**
~~~
public class PhoneWindow extends Window implements MenuBuilder.Callback {
......
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {
......
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
final int keyCode = event.getKeyCode();
final boolean isDown = event.getAction() == KeyEvent.ACTION_DOWN;
......
final Callback cb = getCallback();
final boolean handled = cb != null && mFeatureId < 0 ? cb.dispatchKeyEvent(event)
: super.dispatchKeyEvent(event);
if (handled) {
return true;
}
return isDown ? PhoneWindow.this.onKeyDown(mFeatureId, event.getKeyCode(), event)
: PhoneWindow.this.onKeyUp(mFeatureId, event.getKeyCode(), event);
}
......
}
......
}
~~~
這個函數定義在文件frameworks/base/policy/src/com/android/internal/policy/impl/PhoneWindow.java中。
PhoneWindow類的成員函數getCallback是從父類Window繼承下來的,它返回的是一個Window.Callback接口。每一個Activity組件都會實現一個Window.Callback接口,并且將這個Window.Callback接口設置到與它所關聯的一個PhoneWindow對象的內部去,這樣當該PhoneWindow對象接收到鍵盤事件的時候,就可以該鍵盤事件分發給與它所關聯的Activity組件處理。
DecorView類的成員變量mFeatureId用來描述當前正在處理的DecorView對象的特征,當它的值小于0的時候,就表示當前正在處理的一個DecorView對象是用來描述一個Activity組件窗口的頂層視圖的。
因此,當當前正在處理的DecorView對象描述的是一個Activity組件窗口的頂層視圖,并且這個Activity組件實現有一個Window.Callback接口時,DecorView類的成員函數dispatchKeyEvent就會調用該Window.Callback接口的成員函數dispatchKeyEvent來通知對應的Activity組件,它接收到一個鍵盤事件了。否則的話,參數event所描述的鍵盤事件就會被分發給當前正在處理的DecorView對象的父對象來處理,這是通過調用DecorView類的父類View的成員函數dispatchKeyEvent來實現的。
我們假設當前正在處理的DecorView對象描述的是一個Activity組件窗口的頂層視圖,并且這個Activity組件實現有一個Window.Callback接口,那么參數event所描述的鍵盤事件接下來就會分給該Activity組件處理。如果該Activity組件在處理完成這個鍵盤事件之后,希望該鍵盤事件還能繼續分發下去給其它對象處理,那么它所實現的Window.Callback接口的成員函數dispatchKeyEvent的返回值handled就會等于false,這時候DecorView類的成員函數dispatchKeyEvent就會將該鍵盤事件分發給與當前正在處理的DecorView對象所關聯的一個PhoneWindow對象的成員函數onKeyDown或者onKeyUp來處理,取決于變量isDown的值是true還是false,即當前發生的鍵盤事件是與按鍵按下有關,還是與按鍵松開有關。
PhoneWindow類的成員函數onKeyDown和onKeyUp主要是有來監控一些特殊按鍵事件,例如電話鍵和音量鍵,以便可以執行一些對應的邏輯。例如,當按下電話鍵時,就打開撥號程序;又如,當按下音量鍵時,就調節音量的大小。
接下來,我們就繼續分析Activity類所實現的Window.Callback接口的成員函數dispatchKeyEvent的實現,以便可以了解鍵盤事件在Activity組件窗口的分發過程。
**Step 7. Activity.dispatchKeyEvent**
~~~
public class Activity extends ContextThemeWrapper
implements LayoutInflater.Factory,
Window.Callback, KeyEvent.Callback,
OnCreateContextMenuListener, ComponentCallbacks {
......
public boolean dispatchKeyEvent(KeyEvent event) {
......
Window win = getWindow();
if (win.superDispatchKeyEvent(event)) {
return true;
}
View decor = mDecor;
if (decor == null) decor = win.getDecorView();
return event.dispatch(this, decor != null
? decor.getKeyDispatcherState() : null, this);
}
......
}
~~~
這個函數定義在文件frameworks/base/core/java/android/app/Activity.java中。
Activity類的成員函數getWindow返回的是與當前正處理的Activity組件所關聯的一個PhoneWindow對象,Activity類的成員函數dispatchKeyEvent獲得了這個PhoneWindow對象之后,就會調用它的成員函數superDispatchKeyEvent,以便可以將參數event所描述的鍵盤事件分發給它處理。
這個PhoneWindow對象在處理完成參數event所描述的鍵盤事件之后,如果希望該鍵盤事件能繼續往下分發,那么Activity類的成員函數dispatchKeyEvent就會將該鍵盤事件分發給當前正在處理的Activity組件處理,這是通過調用參數event所描述的一個KeyEvent對象的成員函數dispatch來實現的。
注意,在調用event所描述的一個KeyEvent對象的成員函數dispatch的時候,第一個參數指定為當前正在處理的Activity組件所實現的一個KeyEvent.Callback接口。參數event所指向的一個KeyEvent對象的成員函數dispatch的執行的過程中,就會相應地調用這個KeyEvent.Callback接口的成員函數onKeyDown、onKeyUp或者onKeyMultiple來處理它所描述的鍵盤事件,實際上就是調用Activity類的成員函數onKeyDown、onKeyUp或者onKeyMultiple來處理參數event所描述的鍵盤事件。因此,我們在自定義一個Activity組件時,如果需要處理分發給該Activity組件的鍵盤事件,那么就需要重寫父類Activity的成員函數onKeyDown、onKeyUp或者onKeyMultiple。
接下來,我們就繼續分析PhoneWindow類的成員函數superDispatchKeyEvent的實現,以便可以了解鍵盤事件在Activity組件窗口的分發過程。
**Step 8. PhoneWindow.superDispatchKeyEvent**
~~~
public class PhoneWindow extends Window implements MenuBuilder.Callback {
......
// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;
......
@Override
public boolean superDispatchKeyEvent(KeyEvent event) {
return mDecor.superDispatchKeyEvent(event);
}
......
}
~~~
這個函數定義在文件frameworks/base/policy/src/com/android/internal/policy/impl/PhoneWindow.java中。
PhoneWindow類的成員變量mDecor描述的是當前正在處理的Activity組件窗口的頂層視圖,PhoneWindow類的成員函數superDispatchKeyEvent通過調用它所指向的一個DecorView對象的成員函數superDispatchKeyEvent來處理參數event所描述的鍵盤事件。
**Step 9. DecorView.superDispatchKeyEvent**
~~~
public class PhoneWindow extends Window implements MenuBuilder.Callback {
......
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {
......
public boolean superDispatchKeyEvent(KeyEvent event) {
return super.dispatchKeyEvent(event);
}
......
}
......
}
~~~
這個函數定義在文件frameworks/base/policy/src/com/android/internal/policy/impl/PhoneWindow.java中。
DecorView類的成員函數superDispatchKeyEvent的實現很簡單,它只是調用父類ViewGroup的成員函數dispatchKeyEvent來處理參數event所描述的鍵盤事件。
**Step 10. ViewGroup.dispatchKeyEvent**
~~~
public abstract class ViewGroup extends View implements ViewParent, ViewManager {
......
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
if ((mPrivateFlags & (FOCUSED | HAS_BOUNDS)) == (FOCUSED | HAS_BOUNDS)) {
return super.dispatchKeyEvent(event);
} else if (mFocused != null && (mFocused.mPrivateFlags & HAS_BOUNDS) == HAS_BOUNDS) {
return mFocused.dispatchKeyEvent(event);
}
return false;
}
......
}
~~~
這個函數定義在文件frameworks/base/core/java/android/view/ViewGroup.java中。
ViewGroup類的成員函數dispatchKeyEvent的實現與在前面的Step 3中所介紹的ViewGroup類的成員函數dispatchKeyEventPreIme的實現是類似的,即如果當前正在處理的視圖容器能夠獲得焦點并且該視圖容器的大小已經計算好了,那么就會將參數event所描述的鍵盤事件分發給它的父類View的成員函數dispatchKeyEvent來處理,否則的話,如果當前正在處理的視圖容器有一個焦點子視圖,并且這個焦點子視圖的大小已經計算好了,那么就將參數event所描述的鍵盤事件分發給該焦點子視圖的父類View的成員函數dispatchKeyEvent來處理。
從前面的調用過程可以知道,當前正在處理的視圖容器即為Activity組件窗口的頂層視圖。我們假設在該頂層視圖中,獲得焦點的是一個TextView控件,并且這個TextView控件的大小已經計算好了,那么接下來就會調用這個TextView控件的父類View的成員函數dispatchKeyEvent來處理參數event所描述的鍵盤事件。
**Step 11. View.dispatchKeyEvent**
~~~
public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {
......
private OnKeyListener mOnKeyListener;
......
public boolean dispatchKeyEvent(KeyEvent event) {
// If any attached key listener a first crack at the event.
//noinspection SimplifiableIfStatement
......
if (mOnKeyListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
&& mOnKeyListener.onKey(this, event.getKeyCode(), event)) {
return true;
}
return event.dispatch(this, mAttachInfo != null
? mAttachInfo.mKeyDispatchState : null, this);
}
......
}
~~~
這個函數定義在文件frameworks/base/core/java/android/view/View.java中。
當View類的成員變量mOnKeyListener的值不等于null時,它所指向的一個OnKeyListener對象描述的是注冊到當前正在處理的視圖的一個鍵盤事件監聽器。在這種情況下,如果當前正在處理的視圖是處于啟用狀態的,即它的成員變量mViewFlags的ENABLED位等于1,那么參數event所描述的鍵盤事件就先分給該鍵盤事件監聽器處理,這是通過調用View類的成員變量mOnKeyListener所指向的一個OnKeyListener對象的成員函數onKey來實現的。
注冊到當前正在處理的視圖的鍵盤事件監聽器在處理完成參數event所描述的鍵盤事件之后,如果希望該鍵盤事件還能繼續往下處理,那么View類的成員函數dispatchKeyEvent就會繼續調用參數event所指向的一個KeyEvent對象的成員函數dispatch來處理該鍵盤事件。
接下來,我們就繼續分析KeyEvent類的成員函數dispatch的實現,以便可以了解鍵盤事件在Activity組件窗口的分發過程。
**Step 12. KeyEvent.dispatch**
~~~
public class KeyEvent extends InputEvent implements Parcelable {
......
public final boolean dispatch(Callback receiver, DispatcherState state,
Object target) {
switch (mAction) {
case ACTION_DOWN: {
......
boolean res = receiver.onKeyDown(mKeyCode, this);
......
return res;
}
case ACTION_UP:
......
return receiver.onKeyUp(mKeyCode, this);
case ACTION_MULTIPLE:
final int count = mRepeatCount;
final int code = mKeyCode;
if (receiver.onKeyMultiple(code, count, this)) {
return true;
}
......
return false;
}
return false;
}
......
}
~~~
這個函數定義在文件frameworks/base/core/java/android/view/KeyEvent.java中。
從前面的調用過程可以知道,參數receiver指向的是一個View對象所實現的一個KeyEvent.Callback接口,這個KeyEvent.Callback接口是用來接收當前正在處理的鍵盤事件。
KeyEvent類的成員變量mAction描述的的是當前正在處理的鍵盤事件的類型,當它的值等于ACTION_DOWN、ACTION_UP和ACTION_MULTIPLE的時候,KeyEvent類的成員函數dispatch就會分別調用參數receiver所指向的一個View對象的成員函數onKeyDown、onKeyUp和onKeyMultiple來接收當前正在處理的鍵盤事件。
假設當前正在處理的鍵盤事件是與按鍵按下相關的,即KeyEvent類的成員變量mAction的值等于ACTION_DOWN,那么接下來就會調用參數receiver所指向的一個View對象的成員函數onKeyDown來接收當前正在處理的鍵盤事件。
由于前面我們已經假設了當前獲得焦點的是一個TextView控件,因此,參數receiver指向的實際上是一個TextView對象。TextView類重寫了父類View的成員函數onKeyDown,因此,接下來KeyEvent類的成員函數dispatch就會調用TextView類的成員函數onKeyDown來接收當前正在處理的鍵盤事件。
**Step 13. TextView.onKeyDown**
~~~
public class TextView extends View implements ViewTreeObserver.OnPreDrawListener {
......
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
int which = doKeyDown(keyCode, event, null);
if (which == 0) {
// Go through default dispatching.
return super.onKeyDown(keyCode, event);
}
return true;
}
......
}
~~~
這個函數定義在文件frameworks/base/core/java/android/widget/TextView.java中。
TextView類的成員函數onKeyDown調用另外一個成員函數doKeyDown來處理參數event所描述的鍵盤事件,以便可以相應地改變當前獲得焦點的TextView控件的UI。當TextView類的成員函數doKeyDown的返回值which等于0的時候,就表示當前獲得焦點的TextView控件希望參數event所描述的鍵盤事件可以繼續分發給它的父類View處理,這是通過調用父類View的成員函數onKeyDown來實現的。
至此,我們就分析完成TextView控件獲得鍵盤事件的過程了,整個TextView控件的實現框架也分析完成了。
在Android系統中,其它的Android控件與TextView控件的實現框架都是類似的,區別就在于實現細節和所表現的UI不一樣,而且它們一般都有一個共同的特點,那就是都在宿主窗口的繪圖表面上進行UI繪制。在接下來的一篇文章中,我們就分析另外一個種以SurfaceView為代表的特殊控件,它們的UI是繪制在一個專用的繪圖表面上面的,即它們不與宿主窗口共享同一個繪圖表面。敬請關注!
- 前言
- Android組件設計思想
- Android源代碼開發和調試環境搭建
- Android源代碼下載和編譯
- Android源代碼情景分析法
- Android源代碼調試分析法
- 手把手教你為手機編譯ROM
- 在Ubuntu上下載、編譯和安裝Android最新源代碼
- 在Ubuntu上下載、編譯和安裝Android最新內核源代碼(Linux Kernel)
- 如何單獨編譯Android源代碼中的模塊
- 在Ubuntu上為Android系統編寫Linux內核驅動程序
- 在Ubuntu上為Android系統內置C可執行程序測試Linux內核驅動程序
- 在Ubuntu上為Android增加硬件抽象層(HAL)模塊訪問Linux內核驅動程序
- 在Ubuntu為Android硬件抽象層(HAL)模塊編寫JNI方法提供Java訪問硬件服務接口
- 在Ubuntu上為Android系統的Application Frameworks層增加硬件訪問服務
- 在Ubuntu上為Android系統內置Java應用程序測試Application Frameworks層的硬件服務
- Android源代碼倉庫及其管理工具Repo分析
- Android編譯系統簡要介紹和學習計劃
- Android編譯系統環境初始化過程分析
- Android源代碼編譯命令m/mm/mmm/make分析
- Android系統鏡像文件的打包過程分析
- 從CM刷機過程和原理分析Android系統結構
- Android系統架構概述
- Android系統整體架構
- android專用驅動
- Android硬件抽象層HAL
- Android應用程序組件
- Android應用程序框架
- Android用戶界面架構
- Android虛擬機之Dalvik虛擬機
- Android硬件抽象層
- Android硬件抽象層(HAL)概要介紹和學習計劃
- Android專用驅動
- Android Logger驅動系統
- Android日志系統驅動程序Logger源代碼分析
- Android應用程序框架層和系統運行庫層日志系統源代碼分析
- Android日志系統Logcat源代碼簡要分析
- Android Binder驅動系統
- Android進程間通信(IPC)機制Binder簡要介紹和學習計劃
- 淺談Service Manager成為Android進程間通信(IPC)機制Binder守護進程之路
- 淺談Android系統進程間通信(IPC)機制Binder中的Server和Client獲得Service Manager接口之路
- Android系統進程間通信(IPC)機制Binder中的Server啟動過程源代碼分析
- Android系統進程間通信(IPC)機制Binder中的Client獲得Server遠程接口過程源代碼分析
- Android系統進程間通信Binder機制在應用程序框架層的Java接口源代碼分析
- Android Ashmem驅動系統
- Android系統匿名共享內存Ashmem(Anonymous Shared Memory)簡要介紹和學習計劃
- Android系統匿名共享內存Ashmem(Anonymous Shared Memory)驅動程序源代碼分析
- Android系統匿名共享內存Ashmem(Anonymous Shared Memory)在進程間共享的原理分析
- Android系統匿名共享內存(Anonymous Shared Memory)C++調用接口分析
- Android應用程序進程管理
- Android應用程序進程啟動過程的源代碼分析
- Android系統進程Zygote啟動過程的源代碼分析
- Android系統默認Home應用程序(Launcher)的啟動過程源代碼分析
- Android應用程序消息機制
- Android應用程序消息處理機制(Looper、Handler)分析
- Android應用程序線程消息循環模型分析
- Android應用程序輸入事件分發和處理機制
- Android應用程序鍵盤(Keyboard)消息處理機制分析
- Android應用程序UI架構
- Android系統的開機畫面顯示過程分析
- Android幀緩沖區(Frame Buffer)硬件抽象層(HAL)模塊Gralloc的實現原理分析
- SurfaceFlinger
- Android系統Surface機制的SurfaceFlinger服務
- SurfaceFlinger服務簡要介紹和學習計劃
- 啟動過程分析
- 對幀緩沖區(Frame Buffer)的管理分析
- 線程模型分析
- 渲染應用程序UI的過程分析
- Android應用程序與SurfaceFlinger服務的關系
- 概述和學習計劃
- 連接過程分析
- 共享UI元數據(SharedClient)的創建過程分析
- 創建Surface的過程分析
- 渲染Surface的過程分析
- Android應用程序窗口(Activity)
- 實現框架簡要介紹和學習計劃
- 運行上下文環境(Context)的創建過程分析
- 窗口對象(Window)的創建過程分析
- 視圖對象(View)的創建過程分析
- 與WindowManagerService服務的連接過程分析
- 繪圖表面(Surface)的創建過程分析
- 測量(Measure)、布局(Layout)和繪制(Draw)過程分析
- WindowManagerService
- WindowManagerService的簡要介紹和學習計劃
- 計算Activity窗口大小的過程分析
- 對窗口的組織方式分析
- 對輸入法窗口(Input Method Window)的管理分析
- 對壁紙窗口(Wallpaper Window)的管理分析
- 計算窗口Z軸位置的過程分析
- 顯示Activity組件的啟動窗口(Starting Window)的過程分析
- 切換Activity窗口(App Transition)的過程分析
- 顯示窗口動畫的原理分析
- Android控件TextView的實現原理分析
- Android視圖SurfaceView的實現原理分析
- Android應用程序UI硬件加速渲染
- 簡要介紹和學習計劃
- 環境初始化過程分析
- 預加載資源地圖集服務(Asset Atlas Service)分析
- Display List構建過程分析
- Display List渲染過程分析
- 動畫執行過程分析
- Android應用程序資源管理框架
- Android資源管理框架(Asset Manager)
- Asset Manager 簡要介紹和學習計劃
- 編譯和打包過程分析
- Asset Manager的創建過程分析
- 查找過程分析
- Dalvik虛擬機和ART虛擬機
- Dalvik虛擬機
- Dalvik虛擬機簡要介紹和學習計劃
- Dalvik虛擬機的啟動過程分析
- Dalvik虛擬機的運行過程分析
- Dalvik虛擬機JNI方法的注冊過程分析
- Dalvik虛擬機進程和線程的創建過程分析
- Dalvik虛擬機垃圾收集機制簡要介紹和學習計劃
- Dalvik虛擬機Java堆創建過程分析
- Dalvik虛擬機為新創建對象分配內存的過程分析
- Dalvik虛擬機垃圾收集(GC)過程分析
- ART虛擬機
- Android ART運行時無縫替換Dalvik虛擬機的過程分析
- Android運行時ART簡要介紹和學習計劃
- Android運行時ART加載OAT文件的過程分析
- Android運行時ART加載類和方法的過程分析
- Android運行時ART執行類方法的過程分析
- ART運行時垃圾收集機制簡要介紹和學習計劃
- ART運行時Java堆創建過程分析
- ART運行時為新創建對象分配內存的過程分析
- ART運行時垃圾收集(GC)過程分析
- ART運行時Compacting GC簡要介紹和學習計劃
- ART運行時Compacting GC堆創建過程分析
- ART運行時Compacting GC為新創建對象分配內存的過程分析
- ART運行時Semi-Space(SS)和Generational Semi-Space(GSS)GC執行過程分析
- ART運行時Mark-Compact( MC)GC執行過程分析
- ART運行時Foreground GC和Background GC切換過程分析
- Android安全機制
- SEAndroid安全機制簡要介紹和學習計劃
- SEAndroid安全機制框架分析
- SEAndroid安全機制中的文件安全上下文關聯分析
- SEAndroid安全機制中的進程安全上下文關聯分析
- SEAndroid安全機制對Android屬性訪問的保護分析
- SEAndroid安全機制對Binder IPC的保護分析
- 從NDK在非Root手機上的調試原理探討Android的安全機制
- APK防反編譯
- Android視頻硬解穩定性問題探討和處理
- Android系統的智能指針(輕量級指針、強指針和弱指針)的實現原理分析
- Android應用程序安裝過程源代碼分析
- Android應用程序啟動過程源代碼分析
- 四大組件源代碼分析
- Activity
- Android應用程序的Activity啟動過程簡要介紹和學習計劃
- Android應用程序內部啟動Activity過程(startActivity)的源代碼分析
- 解開Android應用程序組件Activity的"singleTask"之謎
- Android應用程序在新的進程中啟動新的Activity的方法和過程分析
- Service
- Android應用程序綁定服務(bindService)的過程源代碼分析
- ContentProvider
- Android應用程序組件Content Provider簡要介紹和學習計劃
- Android應用程序組件Content Provider應用實例
- Android應用程序組件Content Provider的啟動過程源代碼分析
- Android應用程序組件Content Provider在應用程序之間共享數據的原理分析
- Android應用程序組件Content Provider的共享數據更新通知機制分析
- BroadcastReceiver
- Android系統中的廣播(Broadcast)機制簡要介紹和學習計劃
- Android應用程序注冊廣播接收器(registerReceiver)的過程分析
- Android應用程序發送廣播(sendBroadcast)的過程分析