如果您對TouchEvent事件分發機制不太了解的,可以參考我的這篇文章——安卓TounchEvent事件分發機制。
問題:TV端焦點滿天飛,如何解決和處理?
記得初入TV開發,以為很簡單。TV的這些界面與布局太簡單了,分分鐘就可以把頁面搭建出來,處理好,然后就沒有然后了。。。。
下面我們就從源碼來帶大家進行安卓TV焦點事件的傳遞
這里先給出Android系統View的繪制流程:
依次執行View類里面的如下三個方法:
- measure(int ,int) :測量View的大小
- layout(int ,int ,int ,int) :設置子View的位置
- draw(Canvas) :繪制View內容到Canvas畫布上
ViewRootImpl的主要作用如下(此處不多講,如有意圖,看源碼):
A:鏈接WindowManager和DecorView的紐帶,更廣一點可以說是Window和View之間的紐帶。
B:完成View的繪制過程,包括measure、layout、draw過程。
C:向DecorView分發收到的用戶發起的event事件,如按鍵,觸屏等事件。
ViewRootImpl不再多余敘述,進入正題:
Android焦點分發的主要方法以及攔截方法的講解。
在RootViewImpl中的函數通道是各種策略(InputStage)的組合,各策略負責的任務不同,如SyntheticInputStage、ViewPostImeInputStage、NativePostImeInputStage等等,這些策略以鏈表結構結構起來,當一個策略者沒有消費事件時,就傳遞個下一個策略者。其中觸摸和按鍵事件由ViewPostImeInputStage處理。
@Override
protected int onProcess(QueuedInputEvent q) {
if (q.mEvent instanceof KeyEvent) {
return processKeyEvent(q);//如果是按鍵事件走此處,處理按鍵和焦點問題了
} else {
final int source = q.mEvent.getSource();
if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
return processPointerEvent(q);//如果是觸摸事件走此處
} else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
return processTrackballEvent(q);
} else {
return processGenericMotionEvent(q);
}
}
}
processKeyEvent(QueuedInputEvent q)源碼如下:
@Override
protected void onDeliverToNext(QueuedInputEvent q) {
if (mUnbufferedInputDispatch
&& q.mEvent instanceof MotionEvent
&& ((MotionEvent)q.mEvent).isTouchEvent()
&& isTerminalInputEvent(q.mEvent)) {
mUnbufferedInputDispatch = false;
scheduleConsumeBatchedInput();
}
super.onDeliverToNext(q);
}
private int processKeyEvent(QueuedInputEvent q) {
final KeyEvent event = (KeyEvent)q.mEvent;
// Deliver the key to the view hierarchy.
if (mView.dispatchKeyEvent(event)) {
return FINISH_HANDLED;
}
if (shouldDropInputEvent(q)) {
return FINISH_NOT_HANDLED;
}
// If the Control modifier is held, try to interpret the key as a shortcut.
if (event.getAction() == KeyEvent.ACTION_DOWN
&& event.isCtrlPressed()
&& event.getRepeatCount() == 0
&& !KeyEvent.isModifierKey(event.getKeyCode())) {
if (mView.dispatchKeyShortcutEvent(event)) {
return FINISH_HANDLED;
}
if (shouldDropInputEvent(q)) {
return FINISH_NOT_HANDLED;
}
}
// Apply the fallback event policy.
if (mFallbackEventHandler.dispatchKeyEvent(event)) {
return FINISH_HANDLED;
}
if (shouldDropInputEvent(q)) {
return FINISH_NOT_HANDLED;
}
// Handle automatic focus changes.
if (event.getAction() == KeyEvent.ACTION_DOWN) {
int direction = 0;
switch (event.getKeyCode()) {
case KeyEvent.KEYCODE_DPAD_LEFT:
if (event.hasNoModifiers()) {
direction = View.FOCUS_LEFT;
}
break;
case KeyEvent.KEYCODE_DPAD_RIGHT:
if (event.hasNoModifiers()) {
direction = View.FOCUS_RIGHT;
}
break;
case KeyEvent.KEYCODE_DPAD_UP:
if (event.hasNoModifiers()) {
direction = View.FOCUS_UP;
}
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
if (event.hasNoModifiers()) {
direction = View.FOCUS_DOWN;
}
break;
case KeyEvent.KEYCODE_TAB:
if (event.hasNoModifiers()) {
direction = View.FOCUS_FORWARD;
} else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
direction = View.FOCUS_BACKWARD;
}
break;
}
if (direction != 0) {
View focused = mView.findFocus();
if (focused != null) {
View v = focused.focusSearch(direction);
if (v != null && v != focused) {
// do the math the get the interesting rect
// of previous focused into the coord system of
// newly focused view
focused.getFocusedRect(mTempRect);
if (mView instanceof ViewGroup) {
((ViewGroup) mView).offsetDescendantRectToMyCoords(
focused, mTempRect);
((ViewGroup) mView).offsetRectIntoDescendantCoords(
v, mTempRect);
}
if (v.requestFocus(direction, mTempRect)) {
playSoundEffect(SoundEffectConstants
.getContantForFocusDirection(direction));
return FINISH_HANDLED;
}
}
// Give the focused view a last chance to handle the dpad key.
if (mView.dispatchUnhandledMove(focused, direction)) {
return FINISH_HANDLED;
}
} else {
// find the best view to give focus to in this non-touch-mode with no-focus
View v = focusSearch(null, direction);
if (v != null && v.requestFocus(direction)) {
return FINISH_HANDLED;
}
}
}
}
return FORWARD;
}
進入源碼講解:
(1) 首先由dispatchKeyEvent進行焦點的分發
如果dispatchKeyEvent方法返回true,那么下面的焦點查找步驟就不會繼續了。
dispatchKeyEvent方法返回true代表事件(包括焦點和按鍵)被消費了。
dispatchKeyEvent(event)如果不了解,看我上一篇文章安卓TounchEvent事件分發機制。
mView的dispatchKeyEvent方法,
mView是是Activity的頂層容器DecorView,它是一FrameLayout。
所以這里的dispatchKeyEvent方法應該執行的是ViewGroup的dispatchKeyEvent()方法,而不是View的dispatchKeyEvent方法。
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onKeyEvent(event, 1);
}
if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS))
== (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) {
if (super.dispatchKeyEvent(event)) {
return true;
}
} else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS)
== PFLAG_HAS_BOUNDS) {
if (mFocused.dispatchKeyEvent(event)) {
return true;
}
}
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 1);
}
return false;
}
ViewGroup的dispatchKeyEvent簡略執行流程
首先ViewGroup會執行父類的dispatchKeyEvent方法,如果返回true那么父類的dispatchKeyEvent方法就會返回true,也就代表父類消費了該焦點事件,那么焦點事件自然就不會往下進行分發。
然后ViewGroup會判斷mFocused這個view是否為空,如果為空就會****return false,焦點繼續往下傳遞;如果不為空,那就會return mFocused的dispatchKeyEvent方法返回的結果。這個mFocused是ViewGroup中當前獲取焦點的子View,這個可以從requestChildFocus方法中得到答案。
requestChildFocus()的源碼如下:
@Override
public void requestChildFocus(View child, View focused) {
if (DBG) {
System.out.println(this + " requestChildFocus()");
}
if (getDescendantFocusability() == FOCUS_BLOCK_DESCENDANTS) {
return;
}
// Unfocus us, if necessary
super.unFocus(focused);
// We had a previous notion of who had focus. Clear it.
if (mFocused != child) {
if (mFocused != null) {
mFocused.unFocus(focused);
}
mFocused = child;
}
if (mParent != null) {
mParent.requestChildFocus(this, focused);
}
}
居然有這個彩蛋?
View的dispatchKeyEvent簡略執行流程
public boolean dispatchKeyEvent(KeyEvent event) {
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onKeyEvent(event, 0);
}
// Give any attached key listener a first crack at the event.
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnKeyListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnKeyListener.onKey(this, event.getKeyCode(), event)) {
return true;
}
if (event.dispatch(this, mAttachInfo != null
? mAttachInfo.mKeyDispatchState : null, this)) {
return true;
}
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
return false;
}
要修改ViewGroup焦點事件的分發:
- 重寫view的dispatchKeyEvent方法
- 給某個子view設置onKeyListener監聽
焦點沒有被dispatchKeyEvent攔截的情況下的繼續代碼中的處理過程,還是進入ViewRootImpl源碼
// Handle automatic focus changes.
if (event.getAction() == KeyEvent.ACTION_DOWN) {
int direction = 0;
switch (event.getKeyCode()) {
case KeyEvent.KEYCODE_DPAD_LEFT:
if (event.hasNoModifiers()) {
direction = View.FOCUS_LEFT;
}
break;
case KeyEvent.KEYCODE_DPAD_RIGHT:
if (event.hasNoModifiers()) {
direction = View.FOCUS_RIGHT;
}
break;
case KeyEvent.KEYCODE_DPAD_UP:
if (event.hasNoModifiers()) {
direction = View.FOCUS_UP;
}
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
if (event.hasNoModifiers()) {
direction = View.FOCUS_DOWN;
}
break;
case KeyEvent.KEYCODE_TAB:
if (event.hasNoModifiers()) {
direction = View.FOCUS_FORWARD;
} else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
direction = View.FOCUS_BACKWARD;
}
break;
}
if (direction != 0) {
View focused = mView.findFocus();
if (focused != null) {
View v = focused.focusSearch(direction);
if (v != null && v != focused) {
// do the math the get the interesting rect
// of previous focused into the coord system of
// newly focused view
focused.getFocusedRect(mTempRect);
if (mView instanceof ViewGroup) {
((ViewGroup) mView).offsetDescendantRectToMyCoords(
focused, mTempRect);
((ViewGroup) mView).offsetRectIntoDescendantCoords(
v, mTempRect);
}
if (v.requestFocus(direction, mTempRect)) {
playSoundEffect(SoundEffectConstants
.getContantForFocusDirection(direction));
return FINISH_HANDLED;
}
}
// Give the focused view a last chance to handle the dpad key.
if (mView.dispatchUnhandledMove(focused, direction)) {
return FINISH_HANDLED;
}
} else {
// find the best view to give focus to in this non-touch-mode with no-focus
View v = focusSearch(null, direction);
if (v != null && v.requestFocus(direction)) {
return FINISH_HANDLED;
}
}
}
}
dispatchKeyEvent方法返回false后,先得到按鍵的方向direction一個int值。direction值是后面來進行焦點查找的。
接著會調用DecorView的findFocus()方法一層一層往下查找已經獲取焦點的子View。
DecorView則是PhoneWindow類的一個內部類,繼承于FrameLayout,由此可知它是一個ViewGroup。
那么,DecroView到底充當了什么樣的角色呢?
其實,DecorView是整個ViewTree的最頂層View,它是一個FrameLayout布局,代表了整個應用的界面。在該布局下面,有標題view和內容view這兩個子元素。
@Override
public View findFocus() {
if (DBG) {
System.out.println("Find focus in " + this + ": flags="
+ isFocused() + ", child=" + mFocused);
}
if (isFocused()) {
return this;
}
if (mFocused != null) {
return mFocused.findFocus();
}
return null;
}
View的findFocus方法
/**
* Find the view in the hierarchy rooted at this view that currently has
* focus.
*
* @return The view that currently has focus, or null if no focused view can
* be found.
*/
public View findFocus() {
return (mPrivateFlags & PFLAG_FOCUSED) != 0 ? this : null;
}
View的hasFocus()方法和isFocused()方法對比
Stackoverflow解釋來了:
hasFocus() is different from isFocused(). hasFocus() == true means that the View or one of its descendants is focused. If you look closely, there's a chain of hasFocused Views till you reach the View that isFocused.
/**
* Returns true if this view has focus itself, or is the ancestor of the
* view that has focus.
*
* @return True if this view has or contains focus, false otherwise.
*/
@ViewDebug.ExportedProperty(category = "focus")
public boolean hasFocus() {
return (mPrivateFlags & PFLAG_FOCUSED) != 0;
}
/**
* Returns true if this view has focus
*
* @return True if this view has focus, false otherwise.
*/
@ViewDebug.ExportedProperty(category = "focus")
public boolean isFocused() {
return (mPrivateFlags & PFLAG_FOCUSED) != 0;
}
接著,如果mView.findFocus()方法返回的mFocused不為空,說明找到了當前獲取焦點的view(mFocused),接著focusSearch會把direction(遙控器按鍵按下的方向)作為參數,找到特定方向下一個將要獲取焦點的view,最后如果該view不為空,那么就讓該view獲取焦點。
我們來看一下focusSearch方法的源碼以及具體實現。
@Override
public View focusSearch(View focused, int direction) {
if (isRootNamespace()) {
// root namespace means we should consider ourselves the top of the
// tree for focus searching; otherwise we could be focus searching
// into other tabs. see LocalActivityManager and TabHost for more info
return FocusFinder.getInstance().findNextFocus(this, focused, direction);
} else if (mParent != null) {
return mParent.focusSearch(focused, direction);
}
return null;
}
focusSearch其實是一層一層地網上調用父View的focusSearch方法,直到當前view是根布局(isRootNamespace()方法),通過注釋可以知道focusSearch最終會調用DecorView的focusSearch方法。而DecorView的focusSearch方法找到的焦點view是通過FocusFinder來找到的。
FocusFinder是什么?
根據給定的按鍵方向,通過當前的獲取焦點的View,查找下一個獲取焦點的view這樣算法的類。焦點沒有被攔截的情況下,Android焦點的查找最終都是通過FocusFinder類來實現的。
FocusFinder是如何通過findNextFocus方法尋找焦點的?
public final View findNextFocus(ViewGroup root, View focused, int direction) {
return findNextFocus(root, focused, null, direction);
}
private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) {
View next = null;
if (focused != null) {
next = findNextUserSpecifiedFocus(root, focused, direction);
}
if (next != null) {
return next;
}
ArrayList<View> focusables = mTempList;
try {
focusables.clear();
root.addFocusables(focusables, direction);
if (!focusables.isEmpty()) {
next = findNextFocus(root, focused, focusedRect, direction, focusables);
}
} finally {
focusables.clear();
}
return next;
}
private View findNextFocus(ViewGroup root, View focused, Rect focusedRect,
int direction, ArrayList<View> focusables) {
if (focused != null) {
if (focusedRect == null) {
focusedRect = mFocusedRect;
}
// fill in interesting rect from focused
focused.getFocusedRect(focusedRect);
root.offsetDescendantRectToMyCoords(focused, focusedRect);
} else {
if (focusedRect == null) {
focusedRect = mFocusedRect;
// make up a rect at top left or bottom right of root
switch (direction) {
case View.FOCUS_RIGHT:
case View.FOCUS_DOWN:
setFocusTopLeft(root, focusedRect);
break;
case View.FOCUS_FORWARD:
if (root.isLayoutRtl()) {
setFocusBottomRight(root, focusedRect);
} else {
setFocusTopLeft(root, focusedRect);
}
break;
case View.FOCUS_LEFT:
case View.FOCUS_UP:
setFocusBottomRight(root, focusedRect);
break;
case View.FOCUS_BACKWARD:
if (root.isLayoutRtl()) {
setFocusTopLeft(root, focusedRect);
} else {
setFocusBottomRight(root, focusedRect);
break;
}
}
}
}
private View findNextUserSpecifiedFocus(ViewGroup root, View focused, int direction) {
// check for user specified next focus
View userSetNextFocus = focused.findUserSetNextFocus(root, direction);
if (userSetNextFocus != null && userSetNextFocus.isFocusable()
&& (!userSetNextFocus.isInTouchMode()
|| userSetNextFocus.isFocusableInTouchMode())) {
return userSetNextFocus;
}
return null;
}
FocusFinder類通過findNextFocus來找焦點的。一層一層往尋找,后面會執行findNextUserSpecifiedFocus()方法,這個方法會執行focused(即當前獲取焦點的View)的findUserSetNextFocus方法,如果該方法返回的View不為空,
且isFocusable = true && isInTouchMode() = true的話。
FocusFinder找到的焦點就是findNextUserSpecifiedFocus()返回的View。
findNextFocus會優先根據XML里設置的下一個將獲取焦點的View的ID值來尋找將要獲取焦點的View。
View findUserSetNextFocus(View root, @FocusDirection int direction) {
switch (direction) {
case FOCUS_LEFT:
if (mNextFocusLeftId == View.NO_ID) return null;
return findViewInsideOutShouldExist(root, mNextFocusLeftId);
case FOCUS_RIGHT:
if (mNextFocusRightId == View.NO_ID) return null;
return findViewInsideOutShouldExist(root, mNextFocusRightId);
case FOCUS_UP:
if (mNextFocusUpId == View.NO_ID) return null;
return findViewInsideOutShouldExist(root, mNextFocusUpId);
case FOCUS_DOWN:
if (mNextFocusDownId == View.NO_ID) return null;
return findViewInsideOutShouldExist(root, mNextFocusDownId);
case FOCUS_FORWARD:
if (mNextFocusForwardId == View.NO_ID) return null;
return findViewInsideOutShouldExist(root, mNextFocusForwardId);
case FOCUS_BACKWARD: {
if (mID == View.NO_ID) return null;
final int id = mID;
return root.findViewByPredicateInsideOut(this, new Predicate<View>() {
@Override
public boolean apply(View t) {
return t.mNextFocusForwardId == id;
}
});
}
}
return null;
}
焦點事件分發步驟:
DecorView會調用dispatchKey一層一層進行焦點的分發,如果dispatchKeyEvent方法返回true的話,那么焦點或者按鍵事件就不會往下分發了。
如果你想攔截某個子View,對其設置OnKeyListener進行焦點的攔截。
如果焦點沒有被攔截的話,那么焦點就會交給系統來處理,還是會繼續分發,直到找到那個獲取焦點的View
Android底層先會記錄按鍵的方向,后面DecorView會一層一層往下調用findFocus方法找到當前獲取焦點的View
后面系統又會根據按鍵的方向,執行focusSearch方法來尋找下一個將要獲取焦點的View
focusSearch內部其實是通過FocusFinder來查找焦點的。FocusFinder會優先通過View在XML布局設置的下一個焦點的ID來查找焦點。
最終如果找到將要獲取焦點的View,就讓其requestFocus。如果請求無效,將其放在onWindowFocusChanged()這個方法中去請求。這是在Activity尋找到焦點的時候。
我的前一篇文章,主要是介紹了TouchEvent事件分發機制,省略了焦點分發傳遞機制的代碼,這篇文章與此相反。如果將兩個結合起來,太繁雜,冗長了。分開反而有利于您的理解。至此,事件分發機制,你也了解的差不多了,給個粉吧!