在前面一個系列的文章中,我們以窗口為單位,分析了WindowManagerService服務(wù)的實(shí)現(xiàn)。同時,在再前面一個系列的文章中,我們又分析了窗口的組成。簡單來說,窗口就是由一系列的視圖按照一定的布局組織起來的。實(shí)際上,每一個視圖都是一個控件,這些控制可以將自己的UI繪制在窗口的繪圖表面上,同時還可以與用戶進(jìn)行交互,即獲得用戶的鍵盤或者觸摸屏輸入。在本文中,我們就詳細(xì)分析窗口控件的上述實(shí)現(xiàn)原理。
由于android系統(tǒng)提供的控件比較多,因此我們只能挑一個比較有代表的控件進(jìn)行分析。這個比較有代表性的控件便是TextView,其它的一些基礎(chǔ)控件,例如Button、EditText和CheckBox等,都是直接或者間接地以它為父類的。每一個控件的實(shí)現(xiàn)都是相當(dāng)復(fù)雜的,不過基本上都是一些細(xì)節(jié)問題,而且不同的控件有不同的實(shí)現(xiàn)細(xì)節(jié),因此,本文并不打算詳細(xì)地分析TextView的具體實(shí)現(xiàn),而是從所有控件為了實(shí)現(xiàn)自己的功能而需要的東西出發(fā),去分析TextView的實(shí)現(xiàn)框架。
那么,控件為了實(shí)現(xiàn)自己的功能而需要的東西是什么呢?有兩個材料是必不可少的。第一個材料是畫布,第二個材料是用戶輸入。有畫布才能繪制UI,而有用戶輸入才能與用戶進(jìn)行交互。因此,接下來我們主要分析TextView的繪制流程,以及它獲得用戶輸入的過程。用戶輸入主要包括鍵盤輸入以及觸摸屏輸入,本文主要關(guān)注的是鍵盤輸入。觸摸屏輸入與鍵盤輸入的獲取過程是類似的,讀者如果有興趣的話,可以參照本文的內(nèi)容來自己研究一下。
從前面[Android應(yīng)用程序窗口(Activity)實(shí)現(xiàn)框架簡要介紹和學(xué)習(xí)計(jì)劃]這個系列的文章可以知道,應(yīng)用程序窗口,即Activity窗口,是由一個PhoneWindow對象,一個DecorView對象,以及一個ViewRoot對象來描述的。其中,PhoneWindow對象用來描述窗口對象,DecorView對象用來描述窗口的頂層視圖,ViewRoot對象除了用來與WindowManagerService服務(wù)通信之外,還用來接收用戶輸入。窗口控件本身也是一個視圖,即一個View對象,它們是以樹形結(jié)構(gòu)組織在一起形成整個窗口的UI的。為了簡單起見,本文假設(shè)要分析的TextView控件是直接以窗口的頂層視圖為父視圖的,即以DecorView為父視圖,如圖1所示:

圖1 窗口結(jié)構(gòu)示意圖以及DecorView、TextView的類關(guān)系圖
圖1顯示的是一個包含了TextView控件的Activity窗口的結(jié)構(gòu)示意圖以及DecorView、TextView的簡單類關(guān)系圖,從中可以看出:
1. 用戶輸入首先是由ViewRoot接收,然后再分發(fā)給TextView處理;
2. DecorView是一個視圖容器,因此,它是從ViewGroup繼承下來,而ViewGroup本身又是從View繼承下來的;
3. TextView是一個簡單視圖,因此,它是直接繼承了View。
接下來,我們就以圖1所示的Activity窗口為例,來分析TextView控件的UI繪制框架及其獲得鍵盤輸入的過程。
一. TextView控件的UI繪制框架
從前面[Android應(yīng)用程序窗口(Activity)的測量(Measure)、布局(Layout)和繪制(Draw)過程分析]一文可以知道,Activity窗口的UI繪制操作分為三步來走,分別是測量、布局和繪制。
1. 測量
為了能告訴父視圖自己的所占據(jù)的空間的大小,所有控件都必須要重寫父類View的成員函數(shù)onMeasure。
TextView類的成員函數(shù)onMeasure的實(shí)現(xiàn)如下所示:
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;
//計(jì)算TextView控件的寬度和高度
......
setMeasuredDimension(width, height);
}
......
}
這個函數(shù)定義在文件frameworks/base/core/[Java](http://lib.csdn.net/base/java)/android/widget/TextView.java中。
參數(shù)widthMeasureSpec和heightMeasureSpec分別用來描述寬度測量規(guī)范和高度測量規(guī)范。測量規(guī)范使用一個int值來表法,這個int值包含了兩個分量。
第一個是mode分量,使用最高2位來表示。測量模式有三種,分別是MeasureSpec.UNSPECIFIED(0)、MeasureSpec.EXACTLY(1)、和MeasureSpec.AT_MOST(2)。
第二個是size分量,使用低30位來表示。當(dāng)mode分量等于MeasureSpec.EXACTLY時,size分量的值就是父視圖要求當(dāng)前控件要設(shè)置的寬度或者高度;當(dāng)mode分量等于MeasureSpec.AT_MOST時,size分量的值就是父視圖限定當(dāng)前控件可以設(shè)置的最大寬度或者高度;當(dāng)mode分量等于MeasureSpec.UNSPECIFIED時,父視圖不限定當(dāng)前控件所設(shè)置的寬度或者高度,這時候當(dāng)前控件一般就按照實(shí)際需求來設(shè)置自己的寬度和高度。
TextView類的成員函數(shù)onMeasure根據(jù)上述規(guī)則計(jì)算好自己的寬度wdith和高度height之后,必須要調(diào)用從父類View繼承下來的成員函數(shù)setMeasuredDimension來通知父視圖它所要設(shè)置的寬度和高度,否則的話,該函數(shù)調(diào)用結(jié)束之后,就會拋出一個類型為IllegalStateException的異常。
2. 布局
前面的測量工作實(shí)際上是確定了控件的大小,但是控件的位置還未確定。控件的位置是通過布局這個操作來完成的。
我們知道,控件是按照樹形結(jié)構(gòu)組織在一起的,其中,子控件的位置由父控件來設(shè)置,也就是說,只有容器類控件才需要執(zhí)行布局操作,這是通過重寫父類View的成員函數(shù)onLayout來實(shí)現(xiàn)的。從Activity窗口的結(jié)構(gòu)可以知道,它的頂層視圖是一個DecorView,這是一個容器類控件。Activity窗口的布局操作就是從其頂層視圖開始執(zhí)行的,每碰到一個容器類的子控件,就調(diào)用它的成員函數(shù)onLayout來讓它有機(jī)會對自己的子控件的位置進(jìn)行設(shè)置,依次類推。
我們常見的FrameLayout、LinearLayout、RelativeLayout、TableLayout和AbsoluteLayout,都是屬于容器類控件,因此,它們都需要重寫父類View的成員函數(shù)onLayout。由于TextView控件不是容器類控件,因此,它可以不重寫父類View的成員函數(shù)onLayout。
3. 繪制
有了前面兩個操作之后,控件的位置的大小就確定下來了,接下來就可以對它們的UI進(jìn)行繪制了??丶榱四軌蚶L制自己的UI,必須要重寫父類View的成員函數(shù)onDraw。
TextView類的成員函數(shù)onDraw的實(shí)現(xiàn)如下所示:
public class TextView extends View implements ViewTreeObserver.OnPreDrawListener {
......
@Override
protected void onDraw(Canvas canvas) {
//在畫布canvas上繪制UI
......
}
......
}
這個函數(shù)定義在文件frameworks/base/core/java/android/widget/TextView.java中。
參數(shù)canvas描述的是一塊畫布,控件的UI就是繪制在這塊畫布上面的。畫布提供了豐富的接口來繪制UI,例如畫線(drawLine)、畫圓(drawCircle)和貼圖(drawBitmap)等等。有了這些UI畫圖接口之后,就可以隨心所欲地繪制控件的UI了。
從前面[Android應(yīng)用程序窗口(Activity)的測量(Measure)、布局(Layout)和繪制(Draw)過程分析](http://blog.csdn.net/luoshengyang/article/details/8372924)一文可以知道,Java層的Canvas實(shí)際上是封裝了C++層的SkCanvas。C++層的SkCanvas內(nèi)部有一塊圖形緩沖區(qū),這塊圖形緩沖區(qū)就是窗口的繪圖表面(Surface)里面的那塊圖形緩沖區(qū)。
從前面[Android應(yīng)用程序與SurfaceFlinger服務(wù)的關(guān)系概述和學(xué)習(xí)計(jì)劃](http://blog.csdn.net/luoshengyang/article/details/7846923)一文可以知道,窗口的繪圖表面里面的那塊圖形緩沖區(qū)實(shí)際上是一塊匿名共享內(nèi)存,它是SurfaceFlinger服務(wù)負(fù)責(zé)創(chuàng)建的。SurfaceFlinger服務(wù)創(chuàng)建完成這塊匿名共享內(nèi)存之后,就會將其返回給窗口所運(yùn)行在的進(jìn)程。窗口所運(yùn)行在的進(jìn)程獲得了這塊匿名共享內(nèi)存之后,就會映射到自己的進(jìn)程空間來,因此,窗口的控件就可以在本進(jìn)程內(nèi)訪問這塊匿名共享內(nèi)存了,實(shí)際上就是往這塊匿名共享內(nèi)存填入UI數(shù)據(jù)。注意,這個過程執(zhí)行完成之后,控件的UI還沒有反映到屏幕上來,因?yàn)檫@時候?qū)⒖丶腢I數(shù)據(jù)填入到圖形緩沖區(qū)而已。
從前面[Android窗口管理服務(wù)WindowManagerService的簡要介紹和學(xué)習(xí)計(jì)劃](http://blog.csdn.net/luoshengyang/article/details/8462738)一文可以知道,窗口的UI的顯示是WindowManagerService服務(wù)來控制的。因此,當(dāng)窗口的所有控件都繪制完成自己的UI之后,窗口就會向WindowManagerService服務(wù)發(fā)送一個Binder進(jìn)程間程通信請求。WindowManagerService服務(wù)接收到這個Binder進(jìn)程間程通信請求之后,就會請求SurfaceFlinger服務(wù)刷新相應(yīng)的窗口的UI。SurfaceFlinger服務(wù)刷新窗口UI的過程可以參考前面[Android系統(tǒng)Surface機(jī)制的SurfaceFlinger服務(wù)渲染應(yīng)用程序UI的過程分析](http://blog.csdn.net/luoshengyang/article/details/8079456)一文。
從上面的描述就可以看出,控件的UI雖然是在一塊簡單的畫布進(jìn)行繪制,但是其中蘊(yùn)含了豐富的知識點(diǎn),并且需要應(yīng)用程序進(jìn)程、WindowManagerService服務(wù)和SurfaceFlinger服務(wù)三方緊密而有序的配合。
如果我們仔細(xì)閱讀[Android應(yīng)用程序窗口(Activity)的測量(Measure)、布局(Layout)和繪制(Draw)過程分析](http://blog.csdn.net/luoshengyang/article/details/8372924)一文,還可以得出以下兩個結(jié)論:
(1). 一個窗口的所有控件的UI都是繪制在窗口的繪圖表面上的,也就是說,一個窗口的所有控件的UI數(shù)據(jù)都是填寫在同一塊圖形緩沖區(qū)中;
(2). 一個窗口的所有控件的UI的繪制操作是在主線程中執(zhí)行的,事實(shí)上,所有與UI相關(guān)的操作都是必須是要在主線程中執(zhí)行,否則的話,就會拋出一個類型為CalledFromWrongThreadException的異常來。
為什么要規(guī)定所有與UI相關(guān)的操作都必須在主線程中執(zhí)行呢?我們知道,這些與UI相關(guān)的操作都涉及到大量的控件內(nèi)部狀態(tài)以及需要訪問窗口的繪圖表面,也就是說,要大量地訪問控件類的成員變量以及窗口繪圖表面里面的圖形緩沖區(qū),因此,如果不將這些與UI相關(guān)的操作限定在同一個線程中執(zhí)行的話,那么就會涉及到線程同步問題。線程同步的開銷是很大的,因此,就要保證那些與UI相關(guān)的操作都在同一個線程中執(zhí)行。這個負(fù)責(zé)執(zhí)行UI相關(guān)操作的線程便是應(yīng)用程序進(jìn)程的主線程,因此我們也將應(yīng)用程序進(jìn)程的主線程稱為UI線程。
我們知道,應(yīng)用程序進(jìn)程的主線程除了負(fù)責(zé)執(zhí)行與UI相關(guān)的操作之外,還負(fù)責(zé)響應(yīng)用戶的輸入,因此,我們就要盡量地避免執(zhí)行很耗時的UI操作,否則的話,系統(tǒng)就會由于應(yīng)用程序進(jìn)程的主線程無法及時響應(yīng)用戶輸入而彈出ANR對話框。
那么,有沒有辦法讓某一個控件的UI享有獨(dú)立的圖形緩沖區(qū)呢?也就是這個控件不將自己的UI數(shù)據(jù)填入到它的宿主窗口的繪圖表面的圖形緩沖區(qū)里面去。如果可以的話,那么我們就可以在另外一個獨(dú)立的線程中繪制該控件的UI。這樣做的好處是顯而易見——可以在這個獨(dú)立的線程執(zhí)行相對比較耗時的UI繪制操作而不會導(dǎo)致主線程無法及時響應(yīng)用戶輸入。答案是肯定的,在接下來的一篇文章中,我們就分析一個可以具有獨(dú)立圖形緩沖區(qū)的控件——SurfaceView。
二. TextView控件獲取鍵盤輸入的過程分析
從前面[Android應(yīng)用程序鍵盤(Keyboard)消息處理機(jī)制分析](http://blog.csdn.net/luoshengyang/article/details/6882903)一文可以知道,每一個窗口的創(chuàng)建的時候,都會與系統(tǒng)的輸入管理器建立一個用戶輸入接收通道。輸入管理器在啟動兩個線程,其中一個用來監(jiān)控用戶輸入,即監(jiān)控用戶是否按下或者放開了鍵盤按鍵,或者是否觸摸了屏幕,另外一個用來將監(jiān)控到的用戶輸入事件分發(fā)給當(dāng)前激活的窗口來處理,而這個分發(fā)過程就是通過前面建立的通道來進(jìn)行的。
當(dāng)前激活的窗口接收到輸入管理器分發(fā)過來的用戶輸入事件之后,就會該事件封裝成一個消息發(fā)送到當(dāng)前激活的窗口所運(yùn)行在的應(yīng)用程序進(jìn)程的主線程的消息隊(duì)列中去。等到這個消息被處理的時候,就會調(diào)用與當(dāng)前激活的窗口所關(guān)聯(lián)的一個ViewRoot對象的成員函數(shù)deliverKeyEvent或者deliverPointerEvent來將前面接收到的用戶輸入分發(fā)給合適的控件。其中,ViewRoot類的成員函數(shù)deliverKeyEvent負(fù)責(zé)分發(fā)鍵盤輸入事件,而ViewRoot類的成員函數(shù)deliverPointerEvent負(fù)責(zé)分發(fā)觸摸屏輸入事件。
接下來,我們就從ViewRoot類的成員函數(shù)deliverKeyEvent開始,分析一個TextView控件獲得鍵盤輸入的過程(獲得觸摸屏輸入的過程是類似的),如圖2所示:

圖2 TextView控件獲得鍵盤輸入的過程
這個過程可以分為14個步驟,接下來我們就詳細(xì)分析每一個步驟。
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);
}
......
}
這個函數(shù)定義在文件frameworks/base/core/java/android/view/ViewRoot.java中。
參數(shù)event描述的是窗口接收到的鍵盤事件,另外一個參數(shù)sendDone表示該鍵盤事件處理完成后,是否需要向系統(tǒng)的輸入管理器發(fā)送一個通知。
ViewRoot類的成員變量mView描述的是窗口的頂層視圖,即它指向的是一個DecorView對象,ViewRoot類的成員函數(shù)deliverKeyEvent首先是調(diào)用它的成員函數(shù)dispatchKeyEventPreIme來讓它優(yōu)先于輸入法處理參數(shù)event所描述的鍵盤事件。如果這個DecorView對象的成員函數(shù)dispatchKeyEventPreIme的返回值handled等于true,那么就說明參數(shù)event所描述的鍵盤事件已經(jīng)處理完畢,即ViewRoot類的成員函數(shù)deliverKeyEvent不用往下執(zhí)行了。在這種情況下,如果參數(shù)sendDone的值等于true,那么ViewRoot類的成員函數(shù)deliverKeyEvent在返回之前,還會調(diào)用成員函數(shù)finishInputEvent來通知系統(tǒng)的輸入管理器,當(dāng)前激活的窗口已經(jīng)處理完成剛剛發(fā)生的鍵盤事件了。在接下來的Step 2到Step 4中,我們再詳細(xì)分析鍵盤事件優(yōu)先于輸入法分發(fā)給窗口處理的過程。
假設(shè)窗口不在輸入法前面攔截參數(shù)event所描述的鍵盤事件,接下來ViewRoot類的成員函數(shù)deliverKeyEvent就會將該鍵盤事件分發(fā)給輸入法處理,這個分發(fā)過程如下所示:
1. 調(diào)用InputMethodManager類的靜態(tài)成員函數(shù)peekInstance獲得一個類型為InputMethodManager輸入法管理器imm;
2. 調(diào)用ViewRoot類的成員函數(shù)enqueuePendingEvent將參數(shù)event所描述的鍵盤事件緩存起來,等到輸入法處理完成該鍵盤事件之后,再繼續(xù)對它進(jìn)行處理;
3. 調(diào)用第1步獲得的輸入法管理器imm的成員函數(shù)dispatchKeyEvent來將參數(shù)event所描述的鍵盤事件分發(fā)給輸入法處理。
這里有兩個地方是需要注意的。第一個地方是只有當(dāng)前窗口正在顯示輸入法的情況下,ViewRoot類的成員函數(shù)deliverKeyEvent才會將參數(shù)event所描述的鍵盤事件分發(fā)給輸入法處理,這是通過檢查ViewRoot類的成員變量mLastWasImTarget的值是否等于true來確定的。第二個地方是在將參數(shù)event所描述的鍵盤事件分發(fā)給輸入法處理時,ViewRoot類的成員函數(shù)deliverKeyEvent會同時傳遞一個類型為InputMethodCallback的回調(diào)接口給輸入法,以便輸入法處理完成參數(shù)event所描述的鍵盤事件之后,可以調(diào)用這個回調(diào)接口的成員函數(shù)finishedEvent來向窗口發(fā)送一個鍵盤事件處理完成通知。這個類型為InputMethodCallback的回調(diào)接口就保存在ViewRoot類的成員變量mInputMethodCallback中,當(dāng)它的成員函數(shù)finishedEvent被調(diào)用的時候,它就會調(diào)用ViewRoot類的成員函數(shù)deliverKeyEventToViewHierarchy來繼續(xù)將參數(shù)event所描述的鍵盤事件分發(fā)給窗口處理。
如果窗口當(dāng)前不需要與輸入法交互,即ViewRoot類的成員變量mLastWasImTarget的值等于false,那么ViewRoot類的成員函數(shù)deliverKeyEvent就會直接調(diào)用成員函數(shù)deliverKeyEventToViewHierarchy來將參數(shù)event所描述的鍵盤事件分發(fā)給窗口處理。
接下來,我們就先析窗口在輸入法之前處理鍵盤輸入的過程,接著再分析窗口在輸入法之后處理鍵盤輸入的過程。
從前面的分析可以知道,ViewRoot類的成員函數(shù)deliverKeyEvent是通過調(diào)用DecorView類的成員函數(shù)dispatchKeyEventPreIme來將獲得的鍵盤輸入優(yōu)先于輸入法分發(fā)給窗口處理的。DecorView類的成員函數(shù)dispatchKeyEventPreIme是從父類ViewGroup繼承下來的,因此,接下來我們就繼續(xù)分析ViewGroup類的成員函數(shù)dispatchKeyEventPreIme的實(shí)現(xiàn)。
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;
}
......
}
這個函數(shù)定義在文件frameworks/base/core/java/android/view/ViewGroup.java中。
ViewGroup類的成員函數(shù)dispatchKeyEventPreIme首先是檢查當(dāng)前正在處理的視圖容器是否能夠獲得焦點(diǎn)。如果能夠獲得焦點(diǎn)的話,那么ViewGroup類的成員變量mPrivateFlags的FOCUSED位就會等于1。在當(dāng)前正在處理的視圖容器能夠獲得焦點(diǎn)的情況下,還要檢查正在處理的視圖容器是否已經(jīng)計(jì)算過大小了,即檢查ViewGroup類的成員變量mPrivateFlags的HAS_BOUNDS位是否等于1。只有在已經(jīng)計(jì)算過大小并且能夠獲得焦點(diǎn)的情況下,那么正在處理的視圖容器才有資格處理參數(shù)event所描述的鍵盤事件。注意,正在處理的視圖容器是通過調(diào)用其父類View的成員函數(shù)dispatchKeyEventPreIme來處理參數(shù)event所描述的鍵盤事件的。
如果當(dāng)前正在處理的視圖容器沒有資格處理參數(shù)event所描述的鍵盤事件,但是它有一個能夠獲得焦點(diǎn)的子視圖,并且這個子視圖的大小也是已經(jīng)計(jì)算好了的,那么ViewGroup類的成員函數(shù)dispatchKeyEventPreIme就會將參數(shù)event所描述的鍵盤事件分發(fā)給該子視圖處理。當(dāng)前正在處理的視圖容器能夠獲得焦點(diǎn)的子視圖是通過ViewGroup類的成員變量mFocused來描述的,通過調(diào)用這個成員變量所描述的一個View對象的成員函數(shù)dispatchKeyEventPreIme即可將參數(shù)event所描述的鍵盤事件分發(fā)給夠獲得焦點(diǎn)的子視圖處理。
一個視圖容器是如何知道它的焦點(diǎn)子視圖的呢?我們知道,當(dāng)我們在屏幕上觸摸一個窗口時,就會發(fā)生一個Pointer事件。這個Pointer事件關(guān)聯(lián)有一個觸摸點(diǎn),通過檢查這個觸摸點(diǎn)當(dāng)前是包含在窗口頂層視圖的哪一個子視圖里面,就可以知道哪一個子視圖是焦點(diǎn)子視圖了。
從上面的分析可以知道,無論是當(dāng)前正在處理的視圖容器獲得焦點(diǎn),還是它的子視圖獲得焦點(diǎn),最終都是通過調(diào)用View類的成員函數(shù)dispatchKeyEventPreIme來在輸入法之前處理參數(shù)event所描述的鍵盤事件,因此,接下來我們就繼續(xù)分析View類的成員函數(shù)dispatchKeyEventPreIme的實(shí)現(xiàn)。
Step 3. View.dispatchKeyEventPreIme
public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {
......
public boolean dispatchKeyEventPreIme(KeyEvent event) {
return onKeyPreIme(event.getKeyCode(), event);
}
......
}
這個函數(shù)定義在文件frameworks/base/core/java/android/view/View.java中。
View類的成員函數(shù)dispatchKeyEventPreIme的實(shí)現(xiàn)很簡單,它只是通過調(diào)用另外一個成員函數(shù)onKeyPreIme來在輸入法之前處理參數(shù)event所描述的鍵盤事件。
Step 4. View.onKeyPreIme
public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {
......
public boolean onKeyPreIme(int keyCode, KeyEvent event) {
return false;
}
......
}
這個函數(shù)定義在文件frameworks/base/core/java/android/view/View.java中。
View類的成員函數(shù)onKeyPreIme默認(rèn)是不會在輸入法之前處理參數(shù)event所描述的鍵盤事件的,因此,我們在實(shí)現(xiàn)自己的控件的時候,如果需要在輸入法之前處理鍵盤輸入,那么就必須重寫父類View的成員函數(shù)onKeyPreIme。在重寫父類View的成員函數(shù)onKeyPreIme來處理一個鍵盤事件的時候,如果不希望這個鍵盤事件分發(fā)給輸入法處理,那么就返回一個true值,否則的話,就返回一個false值。
我們假設(shè)當(dāng)前獲得焦點(diǎn)的是圖1所示的TextView控件,但是由于TextView類沒有重寫其父類View的成員函數(shù)onKeyPreIme,因此,參數(shù)event所描述的鍵盤事件接下來就會繼續(xù)分發(fā)給輸入法或者當(dāng)前激活的窗口處理。
這一步執(zhí)行完成之后,回到前面的Step 1中,即ViewRoot類的成員函數(shù)deliverKeyEvent中,無論接下來是否需要先將一個鍵盤事件分發(fā)給輸入法處理,最終都會調(diào)用到ViewRoot類的成員函數(shù)deliverKeyEventToViewHierarchy來繼續(xù)將該鍵盤事件分發(fā)給當(dāng)前激活的窗口處理。
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();
}
......
}
}
......
}
這個函數(shù)定義在文件frameworks/base/core/java/android/view/ViewRoot.java中。
ViewRoot類的成員函數(shù)deliverKeyEventToViewHierarchy首先將參數(shù)event所描述的鍵盤事件交給當(dāng)前激活的窗口的頂層視圖來處理,這是通過調(diào)用ViewRoot類的成員變量mView所描述的一個DecorView對象的成員函數(shù)dispatchKeyEvent來實(shí)現(xiàn)的。
如果當(dāng)前激活的窗口的頂層視圖在處理完成參數(shù)event所描述的鍵盤事件之后,希望該鍵盤事件還能繼續(xù)被ViewRoot類的成員函數(shù)deliverKeyEventToViewHierarchy處理,那么前面調(diào)用DecorView類的成員函數(shù)dispatchKeyEvent得到的返回值keyHandled的值就會等于false。在這種情況下,如果參數(shù)event描述的是一個按下的鍵盤事件,即變量isDown的值等于true,那么ViewRoot類的成員函數(shù)deliverKeyEventToViewHierarchy就會繼續(xù)檢查參數(shù)event描述的是否是一個DPAD事件。如果是的話,那么就可能需要改變窗口當(dāng)前的焦點(diǎn)子視圖。
如果參數(shù)event描述的是一個DPAD事件,那么最終得到的變量direction的值就不會等于0,并且它描述的是當(dāng)前按下的是哪一個方向的DPAD鍵。假設(shè)這時候窗口已經(jīng)有一個焦點(diǎn)子視圖,即調(diào)用ViewRoot類的成員變量mView所描述的一個DecorView對象的成員函數(shù)findFocus的返回值focused不等于null,那么接下來就要根據(jù)變量direction的值來決定下一個焦點(diǎn)子視圖是誰。例如,假設(shè)變量direction的值等于View.FOCUS_LEFT,那么就表示在當(dāng)前的焦點(diǎn)子視圖focused的左邊查找一個最靠近的子視圖作為下一個焦點(diǎn)子視圖,這是通過調(diào)用當(dāng)前焦點(diǎn)子視圖focused的成員函數(shù)focusSearch來實(shí)現(xiàn)的。
一旦找到了下一個焦點(diǎn)子視圖v,并且該子視圖不是當(dāng)前的焦點(diǎn)子視圖focused,那么ViewRoot類的成員函數(shù)deliverKeyEventToViewHierarchy就需要將子視圖v設(shè)置為焦點(diǎn)子視圖,這是通過調(diào)用變量v所描述的一個View對象的成員函數(shù)requestFocus來實(shí)現(xiàn)的。
通過前面的操作,參數(shù)event所描述的鍵盤事件就處理完成了。如果這時候參數(shù)sendDone的值等于true,那么就表示需要通知系統(tǒng)的輸入管理器,參數(shù)event所描述的鍵盤事件已經(jīng)處理完成了,這是通過調(diào)用ViewRoot類的成員函數(shù)finishInputEvent來實(shí)現(xiàn)的。
接下來,我們就繼續(xù)分析DecorView類的成員函數(shù)dispatchKeyEvent的實(shí)現(xiàn),以便可以了解窗口的頂層視圖分發(fā)鍵盤事件的過程。
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);
}
......
}
......
}
這個函數(shù)定義在文件frameworks/base/policy/src/com/android/internal/policy/impl/PhoneWindow.java中。
PhoneWindow類的成員函數(shù)getCallback是從父類Window繼承下來的,它返回的是一個Window.Callback接口。每一個Activity組件都會實(shí)現(xiàn)一個Window.Callback接口,并且將這個Window.Callback接口設(shè)置到與它所關(guān)聯(lián)的一個PhoneWindow對象的內(nèi)部去,這樣當(dāng)該P(yáng)honeWindow對象接收到鍵盤事件的時候,就可以該鍵盤事件分發(fā)給與它所關(guān)聯(lián)的Activity組件處理。
DecorView類的成員變量mFeatureId用來描述當(dāng)前正在處理的DecorView對象的特征,當(dāng)它的值小于0的時候,就表示當(dāng)前正在處理的一個DecorView對象是用來描述一個Activity組件窗口的頂層視圖的。
因此,當(dāng)當(dāng)前正在處理的DecorView對象描述的是一個Activity組件窗口的頂層視圖,并且這個Activity組件實(shí)現(xiàn)有一個Window.Callback接口時,DecorView類的成員函數(shù)dispatchKeyEvent就會調(diào)用該Window.Callback接口的成員函數(shù)dispatchKeyEvent來通知對應(yīng)的Activity組件,它接收到一個鍵盤事件了。否則的話,參數(shù)event所描述的鍵盤事件就會被分發(fā)給當(dāng)前正在處理的DecorView對象的父對象來處理,這是通過調(diào)用DecorView類的父類View的成員函數(shù)dispatchKeyEvent來實(shí)現(xiàn)的。
我們假設(shè)當(dāng)前正在處理的DecorView對象描述的是一個Activity組件窗口的頂層視圖,并且這個Activity組件實(shí)現(xiàn)有一個Window.Callback接口,那么參數(shù)event所描述的鍵盤事件接下來就會分給該Activity組件處理。如果該Activity組件在處理完成這個鍵盤事件之后,希望該鍵盤事件還能繼續(xù)分發(fā)下去給其它對象處理,那么它所實(shí)現(xiàn)的Window.Callback接口的成員函數(shù)dispatchKeyEvent的返回值handled就會等于false,這時候DecorView類的成員函數(shù)dispatchKeyEvent就會將該鍵盤事件分發(fā)給與當(dāng)前正在處理的DecorView對象所關(guān)聯(lián)的一個PhoneWindow對象的成員函數(shù)onKeyDown或者onKeyUp來處理,取決于變量isDown的值是true還是false,即當(dāng)前發(fā)生的鍵盤事件是與按鍵按下有關(guān),還是與按鍵松開有關(guān)。
PhoneWindow類的成員函數(shù)onKeyDown和onKeyUp主要是有來監(jiān)控一些特殊按鍵事件,例如電話鍵和音量鍵,以便可以執(zhí)行一些對應(yīng)的邏輯。例如,當(dāng)按下電話鍵時,就打開撥號程序;又如,當(dāng)按下音量鍵時,就調(diào)節(jié)音量的大小。
接下來,我們就繼續(xù)分析Activity類所實(shí)現(xiàn)的Window.Callback接口的成員函數(shù)dispatchKeyEvent的實(shí)現(xiàn),以便可以了解鍵盤事件在Activity組件窗口的分發(fā)過程。
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);
}
......
}
這個函數(shù)定義在文件frameworks/base/core/java/android/app/Activity.java中。
Activity類的成員函數(shù)getWindow返回的是與當(dāng)前正處理的Activity組件所關(guān)聯(lián)的一個PhoneWindow對象,Activity類的成員函數(shù)dispatchKeyEvent獲得了這個PhoneWindow對象之后,就會調(diào)用它的成員函數(shù)superDispatchKeyEvent,以便可以將參數(shù)event所描述的鍵盤事件分發(fā)給它處理。
這個PhoneWindow對象在處理完成參數(shù)event所描述的鍵盤事件之后,如果希望該鍵盤事件能繼續(xù)往下分發(fā),那么Activity類的成員函數(shù)dispatchKeyEvent就會將該鍵盤事件分發(fā)給當(dāng)前正在處理的Activity組件處理,這是通過調(diào)用參數(shù)event所描述的一個KeyEvent對象的成員函數(shù)dispatch來實(shí)現(xiàn)的。
注意,在調(diào)用event所描述的一個KeyEvent對象的成員函數(shù)dispatch的時候,第一個參數(shù)指定為當(dāng)前正在處理的Activity組件所實(shí)現(xiàn)的一個KeyEvent.Callback接口。參數(shù)event所指向的一個KeyEvent對象的成員函數(shù)dispatch的執(zhí)行的過程中,就會相應(yīng)地調(diào)用這個KeyEvent.Callback接口的成員函數(shù)onKeyDown、onKeyUp或者onKeyMultiple來處理它所描述的鍵盤事件,實(shí)際上就是調(diào)用Activity類的成員函數(shù)onKeyDown、onKeyUp或者onKeyMultiple來處理參數(shù)event所描述的鍵盤事件。因此,我們在自定義一個Activity組件時,如果需要處理分發(fā)給該Activity組件的鍵盤事件,那么就需要重寫父類Activity的成員函數(shù)onKeyDown、onKeyUp或者onKeyMultiple。
接下來,我們就繼續(xù)分析PhoneWindow類的成員函數(shù)superDispatchKeyEvent的實(shí)現(xiàn),以便可以了解鍵盤事件在Activity組件窗口的分發(fā)過程。
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);
}
......
}
這個函數(shù)定義在文件frameworks/base/policy/src/com/android/internal/policy/impl/PhoneWindow.java中。
PhoneWindow類的成員變量mDecor描述的是當(dāng)前正在處理的Activity組件窗口的頂層視圖,PhoneWindow類的成員函數(shù)superDispatchKeyEvent通過調(diào)用它所指向的一個DecorView對象的成員函數(shù)superDispatchKeyEvent來處理參數(shù)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);
}
......
}
......
}
這個函數(shù)定義在文件frameworks/base/policy/src/com/android/internal/policy/impl/PhoneWindow.java中。
DecorView類的成員函數(shù)superDispatchKeyEvent的實(shí)現(xiàn)很簡單,它只是調(diào)用父類ViewGroup的成員函數(shù)dispatchKeyEvent來處理參數(shù)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;
}
......
}
這個函數(shù)定義在文件frameworks/base/core/java/android/view/ViewGroup.java中。
ViewGroup類的成員函數(shù)dispatchKeyEvent的實(shí)現(xiàn)與在前面的Step 3中所介紹的ViewGroup類的成員函數(shù)dispatchKeyEventPreIme的實(shí)現(xiàn)是類似的,即如果當(dāng)前正在處理的視圖容器能夠獲得焦點(diǎn)并且該視圖容器的大小已經(jīng)計(jì)算好了,那么就會將參數(shù)event所描述的鍵盤事件分發(fā)給它的父類View的成員函數(shù)dispatchKeyEvent來處理,否則的話,如果當(dāng)前正在處理的視圖容器有一個焦點(diǎn)子視圖,并且這個焦點(diǎn)子視圖的大小已經(jīng)計(jì)算好了,那么就將參數(shù)event所描述的鍵盤事件分發(fā)給該焦點(diǎn)子視圖的父類View的成員函數(shù)dispatchKeyEvent來處理。
從前面的調(diào)用過程可以知道,當(dāng)前正在處理的視圖容器即為Activity組件窗口的頂層視圖。我們假設(shè)在該頂層視圖中,獲得焦點(diǎn)的是一個TextView控件,并且這個TextView控件的大小已經(jīng)計(jì)算好了,那么接下來就會調(diào)用這個TextView控件的父類View的成員函數(shù)dispatchKeyEvent來處理參數(shù)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);
}
......
}
這個函數(shù)定義在文件frameworks/base/core/java/android/view/View.java中。
當(dāng)View類的成員變量mOnKeyListener的值不等于null時,它所指向的一個OnKeyListener對象描述的是注冊到當(dāng)前正在處理的視圖的一個鍵盤事件監(jiān)聽器。在這種情況下,如果當(dāng)前正在處理的視圖是處于啟用狀態(tài)的,即它的成員變量mViewFlags的ENABLED位等于1,那么參數(shù)event所描述的鍵盤事件就先分給該鍵盤事件監(jiān)聽器處理,這是通過調(diào)用View類的成員變量mOnKeyListener所指向的一個OnKeyListener對象的成員函數(shù)onKey來實(shí)現(xiàn)的。
注冊到當(dāng)前正在處理的視圖的鍵盤事件監(jiān)聽器在處理完成參數(shù)event所描述的鍵盤事件之后,如果希望該鍵盤事件還能繼續(xù)往下處理,那么View類的成員函數(shù)dispatchKeyEvent就會繼續(xù)調(diào)用參數(shù)event所指向的一個KeyEvent對象的成員函數(shù)dispatch來處理該鍵盤事件。
接下來,我們就繼續(xù)分析KeyEvent類的成員函數(shù)dispatch的實(shí)現(xiàn),以便可以了解鍵盤事件在Activity組件窗口的分發(fā)過程。
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;
}
......
}
這個函數(shù)定義在文件frameworks/base/core/java/android/view/KeyEvent.java中。
從前面的調(diào)用過程可以知道,參數(shù)receiver指向的是一個View對象所實(shí)現(xiàn)的一個KeyEvent.Callback接口,這個KeyEvent.Callback接口是用來接收當(dāng)前正在處理的鍵盤事件。
KeyEvent類的成員變量mAction描述的的是當(dāng)前正在處理的鍵盤事件的類型,當(dāng)它的值等于ACTION_DOWN、ACTION_UP和ACTION_MULTIPLE的時候,KeyEvent類的成員函數(shù)dispatch就會分別調(diào)用參數(shù)receiver所指向的一個View對象的成員函數(shù)onKeyDown、onKeyUp和onKeyMultiple來接收當(dāng)前正在處理的鍵盤事件。
假設(shè)當(dāng)前正在處理的鍵盤事件是與按鍵按下相關(guān)的,即KeyEvent類的成員變量mAction的值等于ACTION_DOWN,那么接下來就會調(diào)用參數(shù)receiver所指向的一個View對象的成員函數(shù)onKeyDown來接收當(dāng)前正在處理的鍵盤事件。
由于前面我們已經(jīng)假設(shè)了當(dāng)前獲得焦點(diǎn)的是一個TextView控件,因此,參數(shù)receiver指向的實(shí)際上是一個TextView對象。TextView類重寫了父類View的成員函數(shù)onKeyDown,因此,接下來KeyEvent類的成員函數(shù)dispatch就會調(diào)用TextView類的成員函數(shù)onKeyDown來接收當(dāng)前正在處理的鍵盤事件。
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;
}
......
}
這個函數(shù)定義在文件frameworks/base/core/java/android/widget/TextView.java中。
TextView類的成員函數(shù)onKeyDown調(diào)用另外一個成員函數(shù)doKeyDown來處理參數(shù)event所描述的鍵盤事件,以便可以相應(yīng)地改變當(dāng)前獲得焦點(diǎn)的TextView控件的UI。當(dāng)TextView類的成員函數(shù)doKeyDown的返回值which等于0的時候,就表示當(dāng)前獲得焦點(diǎn)的TextView控件希望參數(shù)event所描述的鍵盤事件可以繼續(xù)分發(fā)給它的父類View處理,這是通過調(diào)用父類View的成員函數(shù)onKeyDown來實(shí)現(xiàn)的。
至此,我們就分析完成TextView控件獲得鍵盤事件的過程了,整個TextView控件的實(shí)現(xiàn)框架也分析完成了。
在Android系統(tǒng)中,其它的Android控件與TextView控件的實(shí)現(xiàn)框架都是類似的,區(qū)別就在于實(shí)現(xiàn)細(xì)節(jié)和所表現(xiàn)的UI不一樣,而且它們一般都有一個共同的特點(diǎn),那就是都在宿主窗口的繪圖表面上進(jìn)行UI繪制。在接下來的一篇文章中,我們就分析另外一個種以SurfaceView為代表的特殊控件,它們的UI是繪制在一個專用的繪圖表面上面的,即它們不與宿主窗口共享同一個繪圖表面。敬請關(guān)注!