前言:按键事件是基于焦点进行派发的,所以本节会谈论到控件的焦点以及下一个焦点控件的查找,并且,由于按键事件的特殊性,本节还会看到输入事件会传递给输入法
本节分析的是:按键事件的派发
1.说起按键事件,就不得不继续分析这句代码了:
stage = q.shouldSkipIme() ? mFirstPostImeInputStage : mFirstInputStage;
上面分析过shouldSkipIme()函数,如果存在如下两种情况中的一种,输入事件便不会传递给输入法处理:
mFlags中存在FLAG_DELIVER_POST_IME标签;
mEvent的类型是MotionEvent,并且输入事件的类型是触摸或者编码;
但是我们这里分析的是按键事件,在按键事件中,mEvent的类型是KeyEvent,并且mFlags中默认不存在FLAG_DELIVER_POST_IME标签,这一标签有啥意义呢?它表示此按键事件之前已经派发给了输入法,但输入法并没有消费它,所以当mFlags中存在这一标记时,无须再将事件派发给输入法;
因此,以上两种情况,按键事件都不满足条件,故:
stage = mFirstInputStage;
好了,接下来开始重点分析mFirstInputStage了:
①先从ViewPreImeInputStage开始(代码前面有):
onProcess()--->processKeyEvent()-->mView.dispatchKeyEventPreIme(event);
在processKeyEvent()中有mView.dispatchKeyEventPreIme(event)这么一个函数,如果mView.dispatchKeyEventPreIme(event)返回了true,则结束派发工作,我们来看下这个代码吧:
public boolean dispatchKeyEventPreIme(KeyEvent event) {
/*PFALG_HAS_BOUNDS表示控件的mLeft/mTop/mRight/mBottom已被View.setFrame()方法进行了
设置,即控件已经完成了layout操作。未经过layout操作的控件可以理解为尚未初始化完毕,控件系统
会拒绝这样的控件获取事件*/
if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS))
== (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) {
//如果ViewGroup是焦点的拥有者,ViewGroup尝试消费此事件
return super.dispatchKeyEventPreIme(event);
} else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS)
== PFLAG_HAS_BOUNDS) {
//如果mFocused不为空,则传递给mFocused
return mFocused.dispatchKeyEventPreIme(event);
}
return false;
}
View.java
public boolean dispatchKeyEventPreIme(KeyEvent event) {
return onKeyPreIme(event.getKeyCode(), event);
}
public boolean onKeyPreIme(int keyCode, KeyEvent event) {
/*返回false,表示此控件并不希望在输入法之前消费此事件,可以重写它,并通过返回true来阻止输入法获得事件*/
return false;
}
点评:
(1):从上面的代码得知,按键事件在传递给输入法之前,先传递给了控件树处理,如果控件树在此时并没有消耗按键事件,那么按键事件才会派发给输入法;
(2):此时的控件树是怎么处理按键事件的呢?事件的处理是由dispatchKeyEventPreIme方法来完成的,它在ViewGroup和View中有不同的实现:ViewGroup的实现用于将事件沿着mFocused链表向着焦点控件所在的方向进行传递。而View的实现则通过调用View.onKeyPreIme()方法尝试进行事件的消费。
综上所述,我们有两种方式可以优先于输入法进行按键事件的处理:
(1):重写ViewGroup的dispatchKeyEventPreIme;
重写dispatchKeyEventPreIme往往用于在一个ViewGroup中拦截特定的按键事件进行处理并阻止其子控件获得它,并且,在mFocused链表上的所有控件的dispatchKeyEventPreIme()都会被调用;
(2):重写View的onKeyPreIme();
仅当控件拥有焦点时才会被调用;
一般来讲,dispatchXXX()的作用是为了从根控件开始将事件传递给目标控件,而onXXX()则用于在目标控件中处理事件。
②如果没有任何控件在mView.dispatchKeyEventPreIme(event)的过程中消费输入事件,即mView.dispatchKeyEventPreIme(event)返回了false,那么输入事件便会派发给输入法;
ImeInputStage.java
protected int onProcess(QueuedInputEvent q) {
//mLastWasImTarget表示此窗口可能是输入法的输入目标
if (mLastWasImTarget && !isInLocalFocusMode()) {
InputMethodManager imm = InputMethodManager.peekInstance();
if (imm != null) {
final InputEvent event = q.mEvent;
//将事件派发给输入法
int result = imm.dispatchInputEvent(event, q, this, mHandler);
if (result == InputMethodManager.DISPATCH_HANDLED) {
return FINISH_HANDLED;
} else if (result == InputMethodManager.DISPATCH_NOT_HANDLED) {
// The IME could not handle it, so skip along to the next InputStage
return FORWARD;
} else {
return DEFER; // callback will be invoked later
}
}
}
return FORWARD;
}
点评:按键事件为什么要传递给输入法呢?原因:按键事件只会派发给处于焦点状态的窗口,而输入法仅仅是一个辅助工具,它的存在不应对目标窗口的功能或行为产生影响,所以所在的窗口是无法获取焦点的(它的LayoutParams.flags被InputMethodService放置了FLAG_NOT_FOCUSABLE标记), 然而输入法确实拥有处理按键事件的需求, 如通过BACK键将输入法关闭,或通过方向键在输入法中进行选词等。为了解决这一矛盾,ViewRootImpl将会在收到事件后首先转发给输入法,当输入法对此 事件不感兴趣时再将其发送给控件树;
派发给输入法的条件是mLastWasImTarget成员为true,即本窗口可能是输入法的输入目标。这一成员的取值来自于窗口的LayoutParams.flags中FLAG_NOT_FOCUSABLE及FLAG_ALT_FOCUSABLE_IM两个标记的存在情况。当本窗口不能作为输入法的输入目标时,便不会有输入法覆盖其上,自然不需要输入法对按键事件进行处理。
③如果输入事件被输入法消费了,则直接终止后续的派发工作;如果输入法并没有消费,那么就继续进行下一步:
EarlyPostImeInputStage.java
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);
}
}
return FORWARD;
}
private int processKeyEvent(QueuedInputEvent q) {
final KeyEvent event = (KeyEvent)q.mEvent;
/*检查此按键事件是否会退出触摸模式,一般来说,方向键,字母键的按下事件意味着用户会以
按键的方式操纵android,此时需要退出触摸模式*/
if (checkForLeavingTouchModeAndConsume(event)) {
return FINISH_HANDLED;
}
//在正式开始事件的派发之前,首先让mFallbackEventHandler对按键事件进行一些处理
mFallbackEventHandler.preDispatchKeyEvent(event);
return FORWARD;
}
点评:
(1):如果导致了触摸模式的终止,事件会被消费;
(2):mFallbackEventHandler在ViewRootImpl的构造函数中通过PolicyManager.makeNewFallbackEventHandler()创建,是一个PhoneFallbackEventHandler类的实例。当需要为某个按键定义一个系统级的功能,并允许应用程序修改此按键的功能时,可以在PhoneFallbackEventHandler类中进行实现,例如使用音量键调整系统音量的工作就在这里完成。因此应用程序可以将音量键挪作他用,例如在相机中用来调整焦距。 需要注意的是,与 PhoneWindowManager在系统中只有一个实例不同,每个ViewRootImpl都有各自的PhoneFallbackEventHandler实例,因此不适合存储一些系统级的状态;
④分析了这么久终于把前奏分析完了,终于到重头戏了;
ViewPostImeInputStage.java
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 {
//处理其他Motion事件,如游戏手柄等等
return processGenericMotionEvent(q);
}
}
}
private int processKeyEvent(QueuedInputEvent q) {
final KeyEvent event = (KeyEvent)q.mEvent;
//将输入事件派发给控件树,最重要的工作在这里
if (mView.dispatchKeyEvent(event)) {
return FINISH_HANDLED;
}
if (shouldDropInputEvent(q)) {
return FINISH_NOT_HANDLED;
}
//如果没有控件消费这一事件,则尝试将事件派发给mFallbackEventHandler
if (mFallbackEventHandler.dispatchKeyEvent(event)) {
return FINISH_HANDLED;
}
//处理方向键的按键事件,用于焦点在控件之间游走
if (event.getAction() == KeyEvent.ACTION_DOWN) {
if (groupNavigationDirection != 0) {
if (performKeyboardGroupNavigation(groupNavigationDirection)) {
return FINISH_HANDLED;
}
} else {
//焦点游走
if (performFocusNavigation(event)) {
return FINISH_HANDLED;
}
}
}
return FORWARD;
}
private boolean performFocusNavigation(KeyEvent event) {
int direction = 0;
switch (event.getKeyCode()) {
//按下左键,则表示焦点移动到当前焦点控件左侧的控件上
case KeyEvent.KEYCODE_DPAD_LEFT:
//KeyEvent.hasNoModifiers()表示此时Alt/Ctrl/Shift没有按下
if (event.hasNoModifiers()) {
direction = View.FOCUS_LEFT;
}
break;
case KeyEvent.KEYCODE_DPAD_RIGHT:
if (event.hasNoModifiers()) {
direction = View.FOCUS_RIGHT;
}
break;
.......................
.......................
.......................
}
if (direction != 0) {
//获取当前的焦点控件
View focused = mView.findFocus();
if (focused != null) {
//查找下一个应当获取焦点的控件
View v = focused.focusSearch(direction);
if (v != null && v != focused) {
focused.getFocusedRect(mTempRect);
//通过View.requestFocus方法使此控件获取焦点
if (v.requestFocus(direction, mTempRect)) {
playSoundEffect(SoundEffectConstants
.getContantForFocusDirection(direction));
return true;
}
}
}
}
return false;
}
点评:这么一大串代码里面,就包含了前言当中说到的控件焦点和下一个焦点的查找,后面会详细分析;
⑤终于要分析mView.dispatchKeyEvent(event)了,dispatchKeyEvent方法在ViewGroup中和View中有不同的实现;
ViewGroup.java
public boolean dispatchKeyEvent(KeyEvent event) {
if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS))
== (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) {
//如果此ViewGroup拥有焦点,则ViewGroup直接尝试消费该事件
if (super.dispatchKeyEvent(event)) {
return true;
}
} else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS)
== PFLAG_HAS_BOUNDS) {
//如果此ViewGroup不拥有焦点,则将事件沿着mFocus链表进行传递
if (mFocused.dispatchKeyEvent(event)) {
return true;
}
}
return false;
}
View.java
public boolean dispatchKeyEvent(KeyEvent event) {
ListenerInfo li = mListenerInfo;
//首先由OnKeyListener处理按键事件,可以通过View.setOnKeyListener进行设置
if (li != null && li.mOnKeyListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnKeyListener.onKey(this, event.getKeyCode(), event)) {
//如果OnKeyListener消费了按键事件,则返回true
return true;
}
//将事件发送View的指定回调,如onKeyDown(),onKeyUp()等
if (event.dispatch(this, mAttachInfo != null ? mAttachInfo.mKeyDispatchState : null, this)) {
//如果View的onKeyDown(),onKeyUp()等回调消费了事件则返回true
return true;
}
//此控件没有消费这个事件
return false;
}
至此,按键事件的派发流程就结束了,我们来总结一下:
(1):首先按键事件将通过View.dispatchKeyEventPreIme()派发给控件树。此时开发者可以进行事件处理的场所是View.dispatchKeyEventPreIme()或View.onKeyPreIme();
(2):然后按键事件将通过InputManager.dispatchKeyEvent()派发给输入法。此时处理事件的场所是当前输入法所在的InputMethodService的onKeyDown()/onKeyUp()等系列回调;
(3):之后按键事件将被View.checkForLeavingTouchModeAndConsume()方法用来尝试退出触摸模式;
(4):再之后按键事件将被View.dispatchKeyEvent()在此派发给控件树。此时开发者可以进行事件处理的场所是View.dispatchKeyEvent(),通过View.setOnKeyListener()方法设置的OnKeyListener,以及View.onKeyDown()/onKeyDown()/onKeyUp()等系列回调;
(5):PhoneFallbackEventHandler将在上述对象都没有消费事件时尝试对事件进行处理;
(6):最后ViewRootImpl将尝试通过按键事件使焦点在控件之间游走。
另外,按键事件是基于焦点进行派发的。事件将从根控件开始沿着mFocused链表向拥有焦点的控件进行传递,沿途的ViewGroup都将有机会通过重写其dispatchKeyEventPreIme()或dispatchKeyEvent()方法进行拦截处理。而onKeyPreIme()/onKeyDown()等系列回调仅会发生在拥有焦点的控件上
2.前面提到了控件获取焦点以及焦点控件的查找,这里面进行详细的分析
先说触摸模式的概念:
在键盘模式下,通过方向键选中一个菜单项,该菜单项便处于焦点状态,按下确认键后,事件便会派发给拥有焦点的菜单项;而在触摸模式下,菜单项不会获取焦点,而是直接响应触摸事件执行相应的动作;
点评:键盘模式下,必定有一个菜单项处于焦点状态;触摸模式下,不需要任何一个菜单项处于焦点状态,因为用户会通过点击选择自己希望的操作;他们也有共同点,如一个文本框,无论在哪种模式下,他都可以获得焦点以接受用户的输入。
可以获取焦点的控件可以分为两类:
①在任何情况下都可以获取焦点的控件,如文本框;
②仅在键盘操作下才能获取焦点的控件,如菜单项,按钮等;
android引入了触摸模式TouchMode来管理两者之间的差异,通过进入或退出触摸模式实现在二者之间的无缝切换,在非触摸模式下,文本框,按钮,菜单项等都可以获取焦点,并可以通过方向键来控制控件的焦点;而在进入触摸模式后,某些控件如菜单项,按钮等将不再可以保持或获取焦点,而文本框则仍然可以可以保持或获取焦点;
触摸模式是一个系统级别的概念,会对所有窗口产生影响,系统是否处于触摸模式取决于WMS中的一个成员变量mInTouchMode,而确定是否进入或者退出触摸模式则取决于用户对某一个窗口所执行的操作;
导致退出触摸模式的操作有:
用户按下了方向键;
用户通过键盘按下了一个字母键;
开发者执行了View.requesetFocusFromTouch();
而进入触摸模式的方式只有一个:就是用户在窗口上进行了点击操作;
窗口的ViewRootImpl会识别上述操作,然后通过WMS的接口setInTouchMode()设置WMS.mInTouchMode使得系统进入或者退出触摸模式,而当其他窗口进行relayout操作时会在WMS.relayoutWindow()的返回值中添加或删除RELAYOUT_RES_IN_TOUCH_MODE标记使得它们得知系统目前的操作模式;
只有拥有ViewRootImpl的窗口才能影响触摸模式,或对触摸模式产生响应,通过WMS的接口直接创建的窗口必须手动的维护触摸模式;
再说控件焦点:
控件的焦点影响了按键事件的派发,另外,控件的焦点还会影响控件的表现形式,拥有焦点的控件往往会高亮显示以区别其他控件;获取焦点的方式有很多,比如,通过方向键选择某个控件使其获得焦点,但是获取焦点最基本的方式:View.requesetFocus();
View.requesetFocus()的实现方式有两种,即View和ViewGroup的实现不同的;
当实例是一个View表示时,表示期望此View能够获取焦点;而当实例是一个ViewGroup时,则会根据一定的焦点选择策略选择其一个子控件或ViewGroup本身作为焦点;
requesetFocus()在View中的流程:
public final boolean requestFocus() {
return requestFocus(View.FOCUS_DOWN);
}
public final boolean requestFocus(int direction) {
return requestFocus(direction, null);
}
public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
return requestFocusNoSearch(direction, previouslyFocusedRect);
}
点评:调用requestFocus的一个重载,View.FOCUS_DOWN表示焦点的查找方向,在ViewGroup中将会从左上角开始沿着这个方向开始查找可以获取焦点的子控件,但是在View中,这个参数并没有实际意义;
ViewGroup会重写requestFocus(int direction, Rect previouslyFocusedRect)方法,previouslyFocusedRect表示的是上一个焦点控件的区域,它会在ViewGroup中发挥作用,因为View使用的是requestFocusNoSearch方法,表示不需要查找,所以,View和ViewGroup在这里开始分道扬镳了;
继续往下面看:
private boolean requestFocusNoSearch(int direction, Rect previouslyFocusedRect) {
// need to be focusable
if ((mViewFlags & FOCUSABLE) != FOCUSABLE
|| (mViewFlags & VISIBILITY_MASK) != VISIBLE) {
return false;
}
// need to be focusable in touch mode if in touch mode
if (isInTouchMode() &&
(FOCUSABLE_IN_TOUCH_MODE != (mViewFlags & FOCUSABLE_IN_TOUCH_MODE))) {
return false;
}
// need to not have any parents blocking us
if (hasAncestorThatBlocksDescendantFocus()) {
return false;
}
handleFocusGainInternal(direction, previouslyFocusedRect);
return true;
}
点评:这段代码的前面几个步骤,是用来检查此控件是否拥有焦点的条件,那哪些代码才能够拥有焦点呢?
①控件必须是可见的,并且控件必须拥有FOCUSABLE这个属性,FOCUSABLE属性可以通过View.setFocusable来设置;
②如果系统目前处于触摸模式的情况下,控件必须要拥有FOCUSABLE_IN_TOUCH_MODE标记;前面我们就讨论过,有些控件在触摸模式下是不能获得焦点的,哪些控件不能获得焦点呢,答案是没有FOCUSABLE_IN_TOUCH_MODE标记的控件无法获得焦点,要想让控件在触摸模式下有获得焦点的权利,可以使用View.setFocusableInTouchMode方法给它添加FOCUSABLE_IN_TOUCH_MODE标记;
③最终,控件能否获得焦点还取决于它的父控件的一个特性DescendantFocusability,这一特性描述了子控件与父控件之间的焦点获取策略;DescendantFocusability可以有三种取值,其中一种为FOCUS_ BLOCK_ DESCENDANTS。当父控件的这一特性取值为FOCUS_ BLOCK_ DESCENDANTS时,父控件将会阻止其子控件或子控件的子控件获取焦点。
因此控件能否获取焦点的策略如下:
当NOT_ FOCUSABLE标记位于View. mViewFlags时,无法获取焦点;
当控件的父控件的DescendantFocusability取值为FOCUS_ BLOCK_ DESCENDANTS时,无法获取焦点;
当FOCUSABL标记位于View. mViewFlags时分为两种情况:
a)位于非触摸模式时,控件可以获取焦点。
b)位于触摸模式时, View. mViewFlags中存在FOCUSABLE_ IN_ TOUCH_ MODE标记时可以获取焦点,否则不能获取焦点。
继续往下看:
void handleFocusGainInternal(@FocusRealDirection int direction, Rect previouslyFocusedRect) {
if ((mPrivateFlags & PFLAG_FOCUSED) == 0) {
mPrivateFlags |= PFLAG_FOCUSED;
View oldFocus = (mAttachInfo != null) ? getRootView().findFocus() : null;
if (mParent != null) {
mParent.requestChildFocus(this, this);
updateFocusedInCluster(oldFocus, direction);
}
if (mAttachInfo != null) {
mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, this);
}
onFocusChanged(true, direction, previouslyFocusedRect);
refreshDrawableState();
}
}
点评:上面这段代码,有几下要点需要阐述:
①将PFLAG_FOCUSED标记添加入mPrivateFlags成员中,表示此控件是焦点是所有者;
②通过mParent.requestChildFocus()将这一变化通知父控件以将焦点从上一个焦点控件中夺走,并触发一次重绘;
requestChildFocus()方法由为ViewGroup和ViewRootImpl实现,ViewGroup实现的目的之一是用于将 焦点从上一个焦点控件手中夺走,即将PFLAG_FOCUSED标记从控件的mPrivateFlags中移除,并且在ViewGroup中保存新的焦点控件,这个代码的逻辑可以在ViewGroup的requestChildFocus方法看到;而另一个目的则是将这一操作继续向控件树的根部进行回溯,直到ViewRootImpl,ViewRootImpl的requestChildFocus()会保存焦点控件,并引发一次“ 遍历”;
③接下来的代码,就是通过View. onFocusChanged()方法将焦点变化通知给感兴趣的监听者并调用自身的onFocusLost方法,另外,控件焦点决定了输入法的输入对象,因此,InputMethodManager的focusOut和focusIn方法也在此处调用;
④最后更新控件的DrawableState以使控件的绘制内容反映焦点的变化,这将会使控件在随后的绘制中高亮显示;
主要分析mParent.requestChildFocus(this, this)代码;
public void requestChildFocus(View child, View focused) {
//如果父控件阻止其子控件获取焦点,则停止
if (getDescendantFocusability() == FOCUS_BLOCK_DESCENDANTS) {
return;
}
//如果上一个焦点控件就是这个ViewGroup,则将该ViewGroup的PFLAG_FOCUSED标记清除,并释放焦点
super.unFocus(focused);
if (mFocused != child) {
//将ViewGroup中上一个焦点控件的焦点释放
if (mFocused != null) {
mFocused.unFocus(focused);
}
/*保存新的焦点控件,注意这里使用的child,有可能并不是实际拥有焦点的控件,有可能是实际
拥有焦点的控件的父控件*/
mFocused = child;
}
if (mParent != null) {
/*将该操作继续向控件树的根回溯,参数this,也就是child,在这里是ViewGroup,focused才是实际拥有焦点的控件*/
mParent.requestChildFocus(this, focused);
}
}
点评:requestChildFocus方法包含了新的焦点体系的建立过程,以及旧有焦点的销毁过程;
①新体系建立:通过ViewGroup.requestChildFocus()方法回溯过程中进行"mFocused = child"这一操作完成的,当操作完成后,mFocused = child将会建立起一个单项链表,使得从根控件开始通过mFocused成员可以沿着这一单向链表找到实际拥有焦点的控件,即实际拥有焦点的控件位于这个单向链表的尾端;
②旧体系销毁:通过在回溯过程中的mFocused.unFocus(focused)方法完成的;
分别看下ViewGroup和View的unFocus方法:
ViewGroup.java
void unFocus(View focused) {
/*如果mFocused为null,表示此ViewGroup位于mFocus单项链表的尾端,即此ViewGroup是焦点的
实际拥有者,故调用View的unFocus方法释放焦点*/
if (mFocused == null) {
super.unFocus(focused);
} else {
//将unFocus传递给链表的下一个控件
mFocused.unFocus(focused);
//最后将mFocused设置为null
mFocused = null;
}
}
View.java
void unFocus(View focused) {
clearFocusInternal(focused, false, false);
}
void clearFocusInternal(View focused, boolean propagate, boolean refocus) {
if ((mPrivateFlags & PFLAG_FOCUSED) != 0) {
//清除PFLAG_FOCUSED标签
mPrivateFlags &= ~PFLAG_FOCUSED;
//回溯到ViewRootImpl,清除ViewRootImpl中保存的焦点控件
if (propagate && mParent != null) {
mParent.clearChildFocus(this);
}
//通知焦点变化
onFocusChanged(false, 0, null);
//刷新焦点控件的状态
refreshDrawableState();
//通知焦点清除
if (propagate && (!refocus || !rootViewRequestFocus())) {
notifyGlobalFocusCleared(this);
}
}
}
点评:控件树的焦点管理分为两个部分:其一是描述个体级别的焦点状态的PFLAG_FOCUSED标记,用于表示一个控件是否有焦点;其二是描述控件树级别的焦点状态的ViewGroup.mFocus成员,用于提供一条链接控件树的根控件到实际拥有焦点的子控件的单向链表。另外,由于焦点的排他性,当一个控件获取焦点以及创建新的焦点体系时伴随着旧有体系的销毁过程;
上面分析的是View.requestFocus()的代码,接下来分析ViewGroup.requestFocus()的代码:
ViewGroup.java
public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
//获取ViewGroup的DescendantFocusability特性的取值
int descendantFocusability = getDescendantFocusability();
//根据不同的DescendantFocusability特性,产生不同的效果
switch (descendantFocusability) {
//将会阻止所有子控件获取焦点,于是调用View.requestFocus尝试自己获取焦点
case FOCUS_BLOCK_DESCENDANTS:
return super.requestFocus(direction, previouslyFocusedRect);
/*ViewGroup将有优于子控件获取获取焦点的权利,首先会调用View.requestFocus尝试自己获取焦点,倘若自己不满足获取焦点的条件,则通过onRequestFocusInDescendants方法将获取焦点的请求转发给子控件*/
case FOCUS_BEFORE_DESCENDANTS: {
final boolean took = super.requestFocus(direction, previouslyFocusedRect);
return took ? took : onRequestFocusInDescendants(direction, previouslyFocusedRect);
}
/*子控件将有优于ViewGroup获取获取焦点的权利,首先通过onRequestFocusInDescendants方法将获取焦点的请求转发给子控件,如果所有子控件都无法获取焦点,再调用View.requestFocus尝试自己获取焦点*/
case FOCUS_AFTER_DESCENDANTS: {
final boolean took = onRequestFocusInDescendants(direction, previouslyFocusedRect);
return took ? took : super.requestFocus(direction, previouslyFocusedRect);
}
}
}
点评:ViewGroup调用requestFocus()方法会根据其DescendantsFocusability特性的不同而产生三种可能的结果。可以通过ViewGroup.setDescendantFocusability()方法修改这一特性。
onRequestFocusInDescendants负责遍历所有子控件,并将requestFocus转发给它们,分析下它的代码:
ViewGroup.java
protected boolean onRequestFocusInDescendants(int direction,
Rect previouslyFocusedRect) {
int index;
int increment;
int end;
int count = mChildrenCount;
if ((direction & FOCUS_FORWARD) != 0) {
index = 0;
increment = 1;
end = count;
} else {
index = count - 1;
increment = -1;
end = -1;
}
final View[] children = mChildren;
//遍历ViewGroup中的所有子控件,依次调用requestFocus方法,直到有一个子控件获取了焦点
for (int i = index; i != end; i += increment) {
View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
//如果子控件获取了焦点,则停止继续查找
if (child.requestFocus(direction, previouslyFocusedRect)) {
return true;
}
}
}
return false;
}
点评:至此,requestFocus的代码逻辑就这么结束了,ViewGroup的requestFocus方法相比于View,它多了一个DescendantsFocusability特性的判断,以及遍历子控件直到有一个子控件获取了焦点;
与requestFocus相对应的是clearFocus,它的执行过程是销毁现有的焦点体系;
如果调用了clearFocus方法销毁了先用的焦点体系后,它还会调用ViewRootImpl.mView.requestFocus方法,重新设置一个焦点控件,根据ViewGroup.requestFocus的原理,这一行为会在控件树中寻找一个合适的控件并将焦点给她;
联想到一个问题:当一个控件树被添加到ViewRootImpl之后,我们会发现,这个控件树当中会默认有一个控件获得焦点,也是默认有一个控件会高亮显示,这是为什么?
答案是当控件树被添加到ViewRootImpl之后,它也会调用ViewRootImpl.mView.requestFocus()设置初始的焦点。
好了,现在我们知道控件树中的初始焦点是怎么来的了,当有了初始焦点之后,如果此时按下方向键,将焦点移动到另一个控件上,那么下一个焦点控件是怎么被寻找到的呢?
3.关于下一个焦点控件的查找,里面的算法比较复杂,这里就叙述下它的流程:
这一查找依赖于控件在窗口中的问题,控件系统在控件树中指定的方向上寻找距离当前控件最近的一个控件,并将焦点赋予它,该工作由View.focusSearch()完成;
View.java
public View focusSearch(int direction) {
if (mParent != null) {
//查找工作交给父控件完成,由父控件决定下一个焦点控件是谁
return mParent.focusSearch(this, direction);
} else {
//如果控件没有父控件就直接返回null,
return null;
}
}
public View focusSearch(View focused, int direction) {
if (isRootNamespace()) {
/*如果isRootNamespace()为true,表示这个ViewGroup是该控件树的根,然后使用FocusFinder工具类进行查找*/
return FocusFinder.getInstance().findNextFocus(this, focused, direction);
} else if (mParent != null) {
//如果这不是根控件,就一直控件树的根部回溯,直到找到根控件为止
return mParent.focusSearch(focused, direction);
}
return null;
}
看下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;
ViewGroup effectiveRoot = getEffectiveRoot(root, focused);
//首先将尝试按照开发者的设置选择下一个拥有焦点的控件
if (focused != null) {
next = findNextUserSpecifiedFocus(effectiveRoot, focused, direction);
}
if (next != null) {
return next;
}
//如果开发者没有无当前的焦点控件设置下一个拥有焦点的控件,将会使用系统内置的算法进行下一个焦点控件的查找
ArrayList<View> focusables = mTempList;
try {
focusables.clear();
//将控件树中所有可以获取焦点的控件存储到focusables列表中,后续的将会在这个列表中进行查找
effectiveRoot.addFocusables(focusables, direction);
if (!focusables.isEmpty()) {
//调用findNextFocus的另一个重载完成查找
next = findNextFocus(effectiveRoot, focused, focusedRect, direction, focusables);
}
} finally {
focusables.clear();
}
return next;
}
点评:
①findNextFocus方法有三个参数,其中this即root根控件,通过这个参数获取整个控件树中所有的候选控件;focused表示焦点控件,会以这个控件所在的位置开始查找;direction表示了查找的方向;
②FocusFinder.findNextFocus会首先尝试获取开发者设置的下一个焦点的控件,可以通过View.setNextFocusXXXId方法设置此控件的下一个可获取焦点的控件的Id,这个XXX可以是Left,Top等,分别用来设置不同方向下的下一个焦点控件;findNextUserSpecifiedFocus()会在focused上调用getNextFocusXXXId()方法获取对应的控件并返回;
③如果开发者在指定方向上没有设置下一个焦点控件,findNextFocus()会使用内置的算法进行查找,它的查找依据是控件在窗口上的位置,与控件在控件树中的位置无关;
最后调用的findNextFocus()方法就不贴代码了,它主要的作用就是以foucused所在的位置为起点,在focusables列表中根据不同的方向,选择不同的查找算法,选出下一个焦点控件;
总结一下下一个焦点控件查找的流程:
①倘若开发者通过View.setNextFocusXXXId()显式地指定了某一方向上下一个焦点控件的Id,使用这一Id所表示的控件作为下一个焦点控件。
②当开发者在指定的方向上没有指定下一个焦点控件时,则采用控件系统内置的焦点查找算法进行查找。
③对于FORWARD/BACKWARD两个查找方向,根据当前焦点控件在focusables列表中的位置index,将位于index-1或index+1的控件作为下一个焦点控件。
④对于LEFT、UP、RIGHT、DOWN4个查找方向,将使用FocusFinder.isBetterCandidate()方法从focusables列表中根据控件位置选择一个最佳候选作为下一个焦点控件。
在选出下一个焦点控件之后,便可以通过调用它的requestFocus()方法将其设置为焦点控件了。
至此,按键事件的派发就结束了;