TextView的實現原理介紹


記得之前在騰訊面試的時候,被面試官問到這個問題,之前覺得沒什么特別的,一位是面試官故意問些刁鑽的問題來壓工資,現在看來當是確實是懂得不多啊。今天看到就轉載過來也方便自己和他人一起來了解,探尋這個簡單卻深奧的空間內部的密碼吧。
文章轉載自:http://blog.csdn.net/luoshengyang/article/details/8636153

實際上,每一個視圖都是一個控件,這些控制可以將自己的UI繪制在窗口的繪圖表面上,同時還可以與用戶進行交互,即獲得用戶的鍵盤或者觸摸屏輸入。在本文中,我們就詳細分析窗口控件的上述實現原理。
我想面試官當是問這個問題可能只是挑有代表性的控件吧,因為TextView是其它的一些基礎控件,例如Button、EditText和CheckBox等直接或者間接地的父類。每一個控件的實現都是相當復雜的,不過基本上都是一些細節問題,而且不同的控件有不同的實現細節。下面看看大神們是怎么去分析TextView的實現框架。
一下是上面博客的原話:
控件為了實現自己的功能而需要的東西是什么呢?有兩個材料是必不可少的。第一個材料是畫布,第二個材料是用戶輸入。有畫布才能繪制UI,而有用戶輸入才能與用戶進行交互。因此,接下來我們主要分析TextView的繪制流程,以及它獲得用戶輸入的過程。用戶輸入主要包括鍵盤輸入以及觸摸屏輸入,本文主要關注的是鍵盤輸入。觸摸屏輸入與鍵盤輸入的獲取過程是類似的,讀者如果有興趣的話,可以參照本文的內容來自己研究一下。
應用程序窗口,即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繪制框架
Activity窗口的UI繪制操作分為三步來走,分別是測量、布局和繪制。
1. 測量
為了能告訴父視圖自己的所占據的空間的大小,所有控件都必須要重寫父類View的成員函數onMeasure。
TextView類的成員函數onMeasure的實現如下所示:

[java] view plain copy 在CODE上查看代碼片派生到我的代碼片
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的實現如下所示:
[java] view plain copy 在CODE上查看代碼片派生到我的代碼片
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了。
Java層的Canvas實際上是封裝了C++層的SkCanvas。C++層的SkCanvas內部有一塊圖形緩沖區,這塊圖形緩沖區就是窗口的繪圖表面(Surface)里面的那塊圖形緩沖區。
窗口的繪圖表面里面的那塊圖形緩沖區實際上是一塊匿名共享內存,它是SurfaceFlinger服務負責創建的。SurfaceFlinger服務創建完成這塊匿名共享內存之后,就會將其返回給窗口所運行在的進程。窗口所運行在的進程獲得了這塊匿名共享內存之后,就會映射到自己的進程空間來,因此,窗口的控件就可以在本進程內訪問這塊匿名共享內存了,實際上就是往這塊匿名共享內存填入UI數據。注意,這個過程執行完成之后,控件的UI還沒有反映到屏幕上來,因為這時候將控件的UI數據填入到圖形緩沖區而已。
窗口的UI的顯示是WindowManagerService服務來控制的。因此,當窗口的所有控件都繪制完成自己的UI之后,窗口就會向WindowManagerService服務發送一個Binder進程間程通信請求。WindowManagerService服務接收到這個Binder進程間程通信請求之后,就會請求SurfaceFlinger服務刷新相應的窗口的UI
從上面的描述就可以看出,控件的UI雖然是在一塊簡單的畫布進行繪制,但是其中蘊含了豐富的知識點,並且需要應用程序進程、WindowManagerService服務和SurfaceFlinger服務三方緊密而有序的配合。一個窗口的所有控件的UI都是繪制在窗口的繪圖表面上的,也就是說,一個窗口的所有控件的UI數據都是填寫在同一塊圖形緩沖區中;一個窗口的所有控件的UI的繪制操作是在主線程中執行的,事實上,所有與UI相關的操作都是必須是要在主線程中執行,否則的話,就會拋出一個類型為CalledFromWrongThreadException的異常來。
為什么要規定所有與UI相關的操作都必須在主線程中執行呢?我們知道,這些與UI相關的操作都涉及到大量的控件內部狀態以及需要訪問窗口的繪圖表面,也就是說,要大量地訪問控件類的成員變量以及窗口繪圖表面里面的圖形緩沖區,因此,如果不將這些與UI相關的操作限定在同一個線程中執行的話,那么就會涉及到線程同步問題。線程同步的開銷是很大的,因此,就要保證那些與UI相關的操作都在同一個線程中執行。這個負責執行UI相關操作的線程便是應用程序進程的主線程,因此我們也將應用程序進程的主線程稱為UI線程。
我們知道,應用程序進程的主線程除了負責執行與UI相關的操作之外,還負責響應用戶的輸入,因此,我們就要盡量地避免執行很耗時的UI操作,否則的話,系統就會由於應用程序進程的主線程無法及時響應用戶輸入而彈出ANR對話框。
那么,有沒有辦法讓某一個控件的UI享有獨立的圖形緩沖區呢?也就是這個控件不將自己的UI數據填入到它的宿主窗口的繪圖表面的圖形緩沖區里面去。如果可以的話,那么我們就可以在另外一個獨立的線程中繪制該控件的UI。這樣做的好處是顯而易見——可以在這個獨立的線程執行相對比較耗時的UI繪制操作而不會導致主線程無法及時響應用戶輸入。答案是肯定的,在接下來的一篇文章中,我們就分析一個可以具有獨立圖形緩沖區的控件——SurfaceView。
二. TextView控件獲取鍵盤輸入的過程分析
每一個窗口的創建的時候,都會與系統的輸入管理器建立一個用戶輸入接收通道。輸入管理器在啟動兩個線程,其中一個用來監控用戶輸入,即監控用戶是否按下或者放開了鍵盤按鍵,或者是否觸摸了屏幕,另外一個用來將監控到的用戶輸入事件分發給當前激活的窗口來處理,而這個分發過程就是通過前面建立的通道來進行的。
當前激活的窗口接收到輸入管理器分發過來的用戶輸入事件之后,就會該事件封裝成一個消息發送到當前激活的窗口所運行在的應用程序進程的主線程的消息隊列中去。等到這個消息被處理的時候,就會調用與當前激活的窗口所關聯的一個ViewRoot對象的成員函數deliverKeyEvent或者deliverPointerEvent來將前面接收到的用戶輸入分發給合適的控件。其中,ViewRoot類的成員函數deliverKeyEvent負責分發鍵盤輸入事件,而ViewRoot類的成員函數deliverPointerEvent負責分發觸摸屏輸入事件。
接下來,我們就從ViewRoot類的成員函數deliverKeyEvent開始,分析一個TextView控件獲得鍵盤輸入的過程(獲得觸摸屏輸入的過程是類似的),如圖2所示:

圖2 TextView控件獲得鍵盤輸入的過程
這個過程可以分為14個步驟,接下來我們就詳細分析每一個步驟。
Step 1. ViewRoot.deliverKeyEvent
[java] view plain copy 在CODE上查看代碼片派生到我的代碼片
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
[java] view plain copy 在CODE上查看代碼片派生到我的代碼片
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
[java] view plain copy 在CODE上查看代碼片派生到我的代碼片
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
[java] view plain copy 在CODE上查看代碼片派生到我的代碼片
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
[java] view plain copy 在CODE上查看代碼片派生到我的代碼片
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
[java] view plain copy 在CODE上查看代碼片派生到我的代碼片
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
[java] view plain copy 在CODE上查看代碼片派生到我的代碼片
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
[java] view plain copy 在CODE上查看代碼片派生到我的代碼片
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
[java] view plain copy 在CODE上查看代碼片派生到我的代碼片
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
[java] view plain copy 在CODE上查看代碼片派生到我的代碼片
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
[java] view plain copy 在CODE上查看代碼片派生到我的代碼片
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
[java] view plain copy 在CODE上查看代碼片派生到我的代碼片
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
[java] view plain copy 在CODE上查看代碼片派生到我的代碼片
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繪制。

怎么樣,是不是覺得自己干了這么久的android了,實現過非常復雜的功能,但是讓你自己說說這個基本控件的原理,你能說出來嗎?現在看了之后是不是覺得眼前一片明亮,原來如此!!

注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2021 ITdaan.com