- 前言
相信很多剛接觸AndroidTV開發(fā)的開發(fā)者涧黄,都會(huì)被各種焦點(diǎn)問(wèn)題給折磨的不行。不管是學(xué)技術(shù)還是學(xué)習(xí)其他知識(shí)赋荆,都要學(xué)習(xí)和理解其中原理笋妥,碰到問(wèn)題我們才能得心應(yīng)手。下面就來(lái)探一探Android的焦點(diǎn)分發(fā)的過(guò)程窄潭。
- Android焦點(diǎn)分發(fā)春宣,攔截過(guò)程的實(shí)現(xiàn)
Android焦點(diǎn)事件的分發(fā)是從ViewRootImpl的processKeyEvent開始的,源碼如下:
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;
}
源碼比較長(zhǎng)嫉你,下面我就慢慢來(lái)講解一下具體的每一個(gè)細(xì)節(jié)月帝。
- (1) 首先由dispatchKeyEvent進(jìn)行焦點(diǎn)的分發(fā),如果dispatchKeyEvent方法返回true幽污,那么下面的焦點(diǎn)查找步驟就不會(huì)繼續(xù)了嚷辅。
dispatchKeyEvent方法返回true代表焦點(diǎn)事件被消費(fèi)了。
// Deliver the key to the view hierarchy.
if (mView.dispatchKeyEvent(event)) {
return FINISH_HANDLED;
}
- 首先會(huì)執(zhí)行mView的dispatchKeyEvent方法距误,估計(jì)大家會(huì)好奇這個(gè)mView是個(gè)什么鬼簸搞?其實(shí)它就是Activity的頂層容器DecorView,它是一FrameLayout准潭。所以這里的dispatchKeyEvent方法應(yīng)該執(zhí)行的是ViewGroup的dispatchKeyEvent()方法趁俊,而不是View的dispatchKeyEvent方法。
ViewGroup的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;
}
(2)ViewGroup的dispatchKeyEvent執(zhí)行流程
- 首先ViewGroup會(huì)一層一層往上執(zhí)行父類的dispatchKeyEvent方法刑然,如果返回true那么父類的dispatchKeyEvent方法就會(huì)返回true寺擂,也就代表父類消費(fèi)了該焦點(diǎn)事件,那么焦點(diǎn)事件自然就不會(huì)往下進(jìn)行分發(fā)泼掠。
- 然后ViewGroup會(huì)判斷mFocused這個(gè)view是否為空怔软,如果為空就會(huì)return false,焦點(diǎn)繼續(xù)往下傳遞武鲁;如果不為空爽雄,那就會(huì)return mFocused的dispatchKeyEvent方法返回的結(jié)果蝠检。這個(gè)mFocused是什么呢沐鼠?其實(shí)
是ViewGroup中當(dāng)前獲取焦點(diǎn)的子View,這個(gè)可以從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);
}
}
(3)下面再來(lái)瞧瞧view的dispatchKeyEvent方法的具體的執(zhí)行過(guò)程
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;
}
驚奇的發(fā)現(xiàn)執(zhí)行了onKeyListener中的onKey方法饲梭,如果onKey方法返回true,那么dispatchKeyEvent方法也會(huì)返回true
可以得出結(jié)論:如果想要修改ViewGroup焦點(diǎn)事件的分發(fā)焰檩,可以這么干:
- 重寫view的dispatchKeyEvent方法
- 給某個(gè)子view設(shè)置onKeyListener監(jiān)聽
注意:實(shí)際開發(fā)中憔涉,理論上所有焦點(diǎn)問(wèn)題都可以通過(guò)給dispatchKeyEvent方法增加監(jiān)聽來(lái)來(lái)攔截來(lái)控制。
- **回到ViewRootImpl中析苫,焦點(diǎn)沒(méi)有被dispatchKeyEvent攔截的情況下的處理過(guò)程 **
(1)dispatchKeyEvent方法返回false后兜叨,先得到按鍵的方向direction值穿扳,這個(gè)值是一個(gè)int類型參數(shù)。這個(gè)direction值是后面來(lái)進(jìn)行焦點(diǎn)查找的国旷。
// 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;
}
(2)接著會(huì)調(diào)用DecorView的findFocus()方法一層一層往下查找已經(jīng)獲取焦點(diǎn)的子View矛物。
ViewGroup的findFocus方法如下:
@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方法
public View findFocus() {
return (mPrivateFlags & PFLAG_FOCUSED) != 0 ? this : null;
}
說(shuō)明:判斷view是否獲取焦點(diǎn)的isFocused()方法, (mPrivateFlags & PFLAG_FOCUSED) != 0 和view 的isFocused()方法是一致的跪但。
@ViewDebug.ExportedProperty(category = "focus")
public boolean isFocused() {
return (mPrivateFlags & PFLAG_FOCUSED) != 0;
}
其中isFocused()方法的作用是判斷view是否已經(jīng)獲取焦點(diǎn)履羞,如果viewGroup已經(jīng)獲取到了焦點(diǎn),那么返回本身即可屡久,否則通過(guò)mFocused的findFocus()方法來(lái)找焦點(diǎn)忆首。mFocused其實(shí)就是ViewGroup中獲取焦點(diǎn)的子view,如果mView不是ViewGourp的話被环,findFocus其實(shí)就是判斷本身是否已經(jīng)獲取焦點(diǎn)糙及,如果已經(jīng)獲取焦點(diǎn)了,返回本身蛤售。
(3)回到processKeyEvent方法中丁鹉,如果findFocus方法返回的mFocused不為空,說(shuō)明找到了當(dāng)前獲取焦點(diǎn)的view(mFocused)悴能,接著focusSearch會(huì)把direction(遙控器按鍵按下的方向)作為參數(shù)揣钦,找到特定方向下一個(gè)將要獲取焦點(diǎn)的view,最后如果該view不為空漠酿,那么就讓該view獲取焦點(diǎn)冯凹。
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;
}
}
}
(4)focusSearch方法的具體實(shí)現(xiàn)。
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其實(shí)是一層一層地網(wǎng)上調(diào)用父View的focusSearch方法炒嘲,直到當(dāng)前view是根布局(isRootNamespace()方法)宇姚,通過(guò)注釋可以知道focusSearch最終會(huì)調(diào)用DecorView的focusSearch方法。而DecorView的focusSearch方法找到的焦點(diǎn)view是通過(guò)FocusFinder來(lái)找到的夫凸。
@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;
}
(5)FocusFinder是什么浑劳?
它其實(shí)是一個(gè)實(shí)現(xiàn) 根據(jù)給定的按鍵方向,通過(guò)當(dāng)前的獲取焦點(diǎn)的View夭拌,查找下一個(gè)獲取焦點(diǎn)的view這樣算法的類魔熏。焦點(diǎn)沒(méi)有被攔截的情況下,Android框架焦點(diǎn)的查找最終都是通過(guò)FocusFinder類來(lái)實(shí)現(xiàn)的鸽扁。
(6)FocusFinder是如何通過(guò)findNextFocus方法尋找焦點(diǎn)的蒜绽。
下面就來(lái)看看FocusFinder類是如何通過(guò)findNextFocus來(lái)找焦點(diǎn)的。一層一層往下看桶现,后面會(huì)執(zhí)行findNextUserSpecifiedFocus()方法躲雅,這個(gè)方法會(huì)執(zhí)行focused(即當(dāng)前獲取焦點(diǎn)的View)的findUserSetNextFocus方法,如果該方法返回的View不為空骡和,且isFocusable = true && isInTouchMode() = true的話相赁,F(xiàn)ocusFinder找到的焦點(diǎn)就是findNextUserSpecifiedFocus()返回的View相寇。
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;
}
(7)findNextFocus會(huì)優(yōu)先根據(jù)XML里設(shè)置的下一個(gè)將獲取焦點(diǎn)的View ID值來(lái)尋找將要獲取焦點(diǎn)的View。
看看View的findUserSetNextFocus方法內(nèi)部都干了些什么钮科,OMG不就是通過(guò)我們xml布局里設(shè)置的nextFocusLeft裆赵,nextFocusRight的viewId來(lái)找焦點(diǎn)嗎,如果按下Left鍵跺嗽,那么便會(huì)通過(guò)nextFocusLeft值里的View Id值去找下一個(gè)獲取焦點(diǎn)的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;
}
可以得出以下結(jié)論:
1. 如果一個(gè)View在XML布局中設(shè)置了focusable = true && isInTouchMode = true,那么這個(gè)View會(huì)優(yōu)先獲取焦點(diǎn)桨嫁。
2. 通過(guò)設(shè)置nextFocusLeft植兰,nextFocusRight,nextFocusUp璃吧,nextFocusDown值可以控制View的下一個(gè)焦點(diǎn)楣导。
Android焦點(diǎn)的原理實(shí)現(xiàn)就這些⌒蟀ぃ總結(jié)一下:
**首先DecorView會(huì)調(diào)用dispatchKey一層一層進(jìn)行焦點(diǎn)的分發(fā)筒繁,如果dispatchKeyEvent方法返回true的話,那么焦點(diǎn)就不會(huì)往下分發(fā)了巴元。 **
中途可以給某個(gè)子View設(shè)置OnKeyListener進(jìn)行焦點(diǎn)的攔截毡咏。
**如果焦點(diǎn)沒(méi)有被攔截的話,那么焦點(diǎn)就會(huì)交給系統(tǒng)來(lái)處理 **
Android底層先會(huì)記錄按鍵的方向逮刨,后面DecorView會(huì)一層一層往下調(diào)用findFocus方法找到當(dāng)前獲取焦點(diǎn)的View
后面系統(tǒng)又會(huì)根據(jù)按鍵的方向呕缭,執(zhí)行focusSearch方法來(lái)尋找下一個(gè)將要獲取焦點(diǎn)的View
focusSearch內(nèi)部其實(shí)是通過(guò)FocusFinder來(lái)查找焦點(diǎn)的。FocusFinder會(huì)優(yōu)先通過(guò)View在XML布局設(shè)置的下一個(gè)焦點(diǎn)的ID來(lái)查找焦點(diǎn)修己。
最終如果找到將要獲取焦點(diǎn)的View恢总,就讓其requestFocus。
為了方便同志們學(xué)習(xí)睬愤,我這做了張導(dǎo)圖片仿,方便大家理解~