基于Android11
概述
Android View的焦点机制对于触摸屏的设备不是那么重要,但是对于TV设备,View的焦点机制至关重要,所以对于Tv开发者这是必备的技能。
这段时间火遍海内外的网红李子柒出了很多“一生系列”,什么南瓜的一生,葡萄的一生,大蒜的一生.....
这篇文章也借鉴这种方式,讨论一下Android View焦点的一生。
故事要从long long ago 讲起......, 不,故事要从你打开你的Android电视,手拿遥控器按下你神圣的一键开始。
按下一个向下键
在讨论这个操作之前,先说一下Android支持的输入设备,除了我们常见的触摸屏外,还支持键盘、鼠标、游戏手柄、遥控器等。在Linux有一种说法:一切皆文件,同样外接设备对于Linux系统也是一种文件,它们一般被创建在/dev/目录下,而输入设备则创建在/dev/input目录下:
chengfangpeng@ubuntu:/dev/input$ ls
by-id event0 event10 event12 event14 event16 event2 event4 event6 event8 mice
by-path event1 event11 event13 event15 event17 event3 event5 event7 event9 mouse0
上面是一台Linux台式机的/dev/input目录。
Android系统是基于Linux系统,所以设备管理都是由Linux内核完成的。下面是一台Android Tv设备的/dev/input目录
marconi:/dev/input # ls
event0 event1 event2 event3 event4 mice mouse0
当设备可用时,Linux内核会在/dev/input目录下创建event0-n或者其他的名称的节点文件,当设备不可用时则会删除该节点。当我们操作一个红外的遥控器,按下向下键时,Tv设备收到红外信号,触发一次硬件中断,Linux内核则会相应的接收到硬件中断,然后将中断加工成原始的输入事件数据写入到对应的设备节点中。用户空间可以通过read()函数将这个事件读取出来。
Android的输入系统就是不停的监听这些输入节点,当发现某个节点有输入时,就将节点的事件读取出来,分发给对应的接受者。
读取/dev/input事件
假如我们想看看/dev/input节点中这些事件长什么样子,可以通过getevent这个工具看到
chengfangpeng@ubuntu:~$ adb root
chengfangpeng@ubuntu:~$ adb shell getevent -lt /dev/input/event0
[ 3378.901913] EV_KEY KEY_DOWN DOWN
[ 3378.901913] EV_SYN SYN_REPORT 00000000
[ 3379.104075] EV_KEY KEY_DOWN UP
[ 3379.104075] EV_SYN SYN_REPORT 00000000
可以监听/dev/input/event0
这个节点下的输入事件,当我按下遥控器的向下键,监听到的事件。事件类型是KEY_DOWN 值是DOWN和UP,咦!不是还有个SY_REPORT,它是干什么的?EV_SYN是一个特别的事件类型,它用来把同一时刻产生的多个输入数据分割为多个数据包。我们看到了从/dev/input/event0中读出的数据,但是真正的Android是怎么处理的呢,这就涉及到Android的输入系统。
Android输入系统
输入系统的内容比较多,这里不做详细的介绍,后面会专门介绍这一块的逻辑,先挖个坑(不知道什么时候填),借用一张<<深入理解Android卷3>>的一张图,非常的清楚。
![image-20201015154906543](/home/chengfangpeng/Nutstore Files/Nutstore/res/image-20201015154906543.png)
我们输入的事件,如同穿天猴一样,蹭蹭的来到了我们当前所在的Window,这里除了输入系统的,又新加了WMS的逻辑,继续挖坑(之后填...),我们默认我们发的事件已经找到了当前的Window,或者说当前的Activity. 如果你对Activity、Window、PhoneWIndow、WindowMnager、WindowManagerService、ViewRootImpl、DecorView 这些概念也不太熟悉的话,那就继续挖坑(后面会出一篇Activity的一生)。但是在介绍View的焦点机制的时候,上面的一些概念是绕不开的。如果阅读过程中有疑问请查找相应的文章或源码。
ViewRootImpl事件分发
上面的章节提到,遥控器发的事件被传入到了Window中,那到底传到window哪里了,下面是通过点击向下键触发crash的堆栈信息,可以比较清晰的获取事件传递的路径,也验证了上面的结论。
2020-10-15 16:52:59.634 1378-1378/com.xray.focus.sample E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.xray.focus.sample, PID: 1378
java.lang.RuntimeException: dispatchKeyEvent crash
at com.xray.focus.sample.widget.TraceKevEventView.dispatchKeyEvent(TraceKevEventView.java:25)
at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1912)
at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1912)
at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1912)
at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1912)
at com.android.internal.policy.DecorView.superDispatchKeyEvent(DecorView.java:436)
at com.android.internal.policy.PhoneWindow.superDispatchKeyEvent(PhoneWindow.java:1830)
at android.app.Activity.dispatchKeyEvent(Activity.java:3827)
at com.android.internal.policy.DecorView.dispatchKeyEvent(DecorView.java:350)
at android.view.ViewRootImpl$ViewPostImeInputStage.processKeyEvent(ViewRootImpl.java:5337)
at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:5205)
at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4726)
at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4779)
at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4745)
at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:4885)
at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4753)
at android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:4942)
at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4726)
at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4779)
at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4745)
at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4753)
at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4726)
at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4779)
at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4745)
at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:4918)
at android.view.ViewRootImpl$ImeInputStage.onFinishedInputEvent(ViewRootImpl.java:5079)
at android.view.inputmethod.InputMethodManager$PendingEvent.run(InputMethodManager.java:2844)
at android.view.inputmethod.InputMethodManager.invokeFinishedInputEventCallback(InputMethodManager.java:2427)
at android.view.inputmethod.InputMethodManager.finishedInputEvent(InputMethodManager.java:2418)
at android.view.inputmethod.InputMethodManager$ImeInputEventSender.onInputEventFinished(InputMethodManager.java:2821)
at android.view.InputEventSender.dispatchInputEventFinished(InputEventSender.java:143)
at android.os.MessageQueue.nativePollOnce(Native Method)
at android.os.MessageQueue.next(MessageQueue.java:327)
at android.os.Looper.loop(Looper.java:169)
at android.app.ActivityThread.main(ActivityThread.java:7021)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:486)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:872)
从ViewRootImpl开始
ViewRootImpl是整个View树的最上层,在Activity中DecorView是最顶层的View,但是它的mParent是ViewRootImpl,所以严格意义上说ViewRootImpl是整个View树的最上层。遥控器事件会被分发到ViewPostImeInputStage的onProcess方法中。
#ViewRootImpl.java
/**
* Delivers post-ime input events to the view hierarchy.
*/
final class ViewPostImeInputStage extends InputStage {
public ViewPostImeInputStage(InputStage next) {
super(next);
}
@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);
}
}
}
...
}
执行事件分发
private int processKeyEvent(QueuedInputEvent q) {
final KeyEvent event = (KeyEvent)q.mEvent;
...
// Handle automatic focus changes.
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:
if (event.hasNoModifiers()) {
direction = View.FOCUS_LEFT;
}
break;
case KeyEvent.KEYCODE_DPAD_RIGHT:
if (event.hasNoModifiers()) {
direction = View.FOCUS_RIGHT;
}
...
break;
}
if (direction != 0) {
//这个mView就是DecorView,也是我们Viwe树最底层的View,
//然后通过findFoucs去查找整个View树中已经获取焦点的那个View.
View focused = mView.findFocus();
if (focused != null) {//focused不为空,那么就通过这个focused去查找下一个要获取焦点的View
View v = focused.focusSearch(direction);
if (v != null && v != focused) {//找到了下一个要获取焦点的View
// 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)) {//让这个将要获取焦点的View真正获取焦点,也有可能获取不到
playSoundEffect(SoundEffectConstants
.getContantForFocusDirection(direction));//执行获取到焦点后的音效
return true;
}
}
// 没有找到焦点 给view最后一次机会消费按键事件的机会
if (mView.dispatchUnhandledMove(focused, direction)) {
return true;
}
} else {
// 如果当前界面没有焦点走这里,设置一个默认焦点
if (mView.restoreDefaultFocus()) {
return true;
}
}
}
return false;
}
从View树中查找当前拥有焦点的View
遍历整个View树,找到当前那个正拥有焦点的View , findFocus方法在View和ViewGroup中有不同的实现,这种查找方式和findViewById的实现方式非常的类似。先看View中的实现,其实就是判断mPrivateFlags中是否设置了PFLAG_FOCUSED,无意中说出了焦点的本质,View焦点就是这个PFLAG_FOCUSED属性。
#View.java
public View findFocus() {
return (mPrivateFlags & PFLAG_FOCUSED) != 0 ? this : null;
}
ViewGroup中的实现:
#ViewGroup.java
public View findFocus() {
if (DBG) {
System.out.println("Find focus in " + this + ": flags="
+ isFocused() + ", child=" + mFocused);
}
if (isFocused()) {
return this;
}
if (mFocused != null) {//mFocused这个成员变量代表它自己有焦点或者它包含了有焦点的View
return mFocused.findFocus();//继续向下查找
}
return null;
}
查找下一个将要获取焦点的View
回到上面ViewRootImpl的performFocusNavigation方法,我们find到了当前拥有焦点的View,接下来,通过focused去查找下一个将要获取焦点的View.这个过程是通过focusSearch这个方法实现的,当然它在View和ViewGroup中都有实现。
我们先看View中的实现。逻辑比较简单, 就是如果有父布局,就一直往上找。
#View.java
/**
* Find the nearest view in the specified direction that can take focus.
* This does not actually give focus to that view.
*
* @param direction One of FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, and FOCUS_RIGHT
*
* @return The nearest focusable in the specified direction, or null if none
* can be found.
*/
public View focusSearch(@FocusRealDirection int direction) {
if (mParent != null) {
return mParent.focusSearch(this, direction);
} else {
return null;
}
}
我们的布局一般情况都不只一层,而mParent就是一个ViewGroup,所以现在看看ViewGroup中focusSearch的实现。
#ViewGroup.java
/**
* Find the nearest view in the specified direction that wants to take
* focus.
*
* @param focused The view that currently has focus
* @param direction One of FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, and
* FOCUS_RIGHT, or 0 for not applicable.
*/
@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;
}
首先判断当前ViewGroup是不是在View树的最上层,如果不是就继续往上找,如果是,则执行如下逻辑:
return FocusFinder.getInstance().findNextFocus(this, focused, direction);
FocusFinder是查找FoucsView的一个工具类,现在看一下findNextFocus方法
#FocusFinder.java
private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) {
View next = null;
ViewGroup effectiveRoot = getEffectiveRoot(root, focused);
if (focused != null) {//如果当前的焦点不为空,从用户指定的view中查找,focused设置了nextFoucs***Id属性
next = findNextUserSpecifiedFocus(effectiveRoot, focused, direction);
}
if (next != null) {
return next;
}
ArrayList<View> focusables = mTempList;
try {
focusables.clear();
effectiveRoot.addFocusables(focusables, direction);//将可以获取焦点的备选view放入focusables集合中
if (!focusables.isEmpty()) {//候选view的集合不为空时,从候选集合中继续查找
next = findNextFocus(effectiveRoot, focused, focusedRect, direction, focusables);
}
} finally {
focusables.clear();
}
return next;
}
首先,如果focused不为空,从用户指定nextFocus***Id来获取下一个获取焦点的View。通常我们可以在xml中指定下一个获取焦点的View的id,如下:
android:nextFocusLeft=""
android:nextFocusRight=""
android:nextFocusDown=""
android:nextFocusUp=""
findNextUserSpecifiedFocus就是从设置的这些id中查找下一个获取焦点的View.
#FocusFinder.java
private View findNextUserSpecifiedFocus(ViewGroup root, View focused, int direction) {
// check for user specified next focus
View userSetNextFocus = focused.findUserSetNextFocus(root, direction);//从当前已经获取焦点的View中查找用户指定的下一个获取焦点的View
View cycleCheck = userSetNextFocus;
boolean cycleStep = true; // we want the first toggle to yield false
while (userSetNextFocus != null) {
if (userSetNextFocus.isFocusable()
&& userSetNextFocus.getVisibility() == View.VISIBLE
&& (!userSetNextFocus.isInTouchMode()
|| userSetNextFocus.isFocusableInTouchMode())) {
return userSetNextFocus;
}
userSetNextFocus = userSetNextFocus.findUserSetNextFocus(root, direction);
if (cycleStep = !cycleStep) {
cycleCheck = cycleCheck.findUserSetNextFocus(root, direction);
if (cycleCheck == userSetNextFocus) {
// found a cycle, user-specified focus forms a loop and none of the views
// are currently focusable.
break;
}
}
}
return null;
}
#View.java
/**
* If a user manually specified the next view id for a particular direction,
* use the root to look up the view.
* @param root The root view of the hierarchy containing this view.
* @param direction One of FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT, FOCUS_FORWARD,
* or FOCUS_BACKWARD.
* @return The user specified next view, or null if there is none.
*/
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 View rootView = root;
final View startView = this;
// Since we have forward links but no backward links, we need to find the view that
// forward links to this view. We can't just find the view with the specified ID
// because view IDs need not be unique throughout the tree.
return root.findViewByPredicateInsideOut(startView,
t -> findViewInsideOutShouldExist(rootView, t, t.mNextFocusForwardId)
== startView);
}
}
return null;
}
如果用户没有指定下一个获取焦点的View, 接下来就会走Android内部的寻焦算法,首先把可以获取焦点的候选View,放置到一个View列表focusables中,而执行这个添加任务是通过这个addFocusables方法,它会同样也是从上到下遍历整个View树。
effectiveRoot.addFocusables(focusables, direction);
所以addFocusables也分View和ViewGroup,先看View的逻辑,如果这个View符合获取焦点的资格,那么将其添加到views中。先看View中的实现:
# View.java
public void addFocusables(ArrayList<View> views, @FocusDirection int direction,
@FocusableMode int focusableMode) {
if (views == null) {
return;
}
if (!canTakeFocus()) {
return;
}
if ((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE
&& !isFocusableInTouchMode()) {
return;
}
views.add(this);//符合获取焦点的资格,将自己添加到views中
}
在ViewGroup逻辑稍微复杂一些.
#ViewGroup.java
@Override
public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
final int focusableCount = views.size();
final int descendantFocusability = getDescendantFocusability();
final boolean blockFocusForTouchscreen = shouldBlockFocusForTouchscreen();
final boolean focusSelf = (isFocusableInTouchMode() || !blockFocusForTouchscreen);
if (descendantFocusability == FOCUS_BLOCK_DESCENDANTS) {//当descendantFocusability为FOCUS_BLOCK_DESCENDANTS时,
//禁止让自己的子View获取焦点
if (focusSelf) {
super.addFocusables(views, direction, focusableMode);//验证自己是否可以被添加的views中,如果符合就添加之
}
return;
}
if (blockFocusForTouchscreen) {
focusableMode |= FOCUSABLES_TOUCH_MODE;
}
if ((descendantFocusability == FOCUS_BEFORE_DESCENDANTS) && focusSelf) {//当descendantFocusability
//为FOCUS_BEFORE_DESCENDANTS优先子View获取焦点时
super.addFocusables(views, direction, focusableMode);//验证自己是否可以被添加的views中,如果符合就添加之
}
int count = 0;
final View[] children = new View[mChildrenCount];
for (int i = 0; i < mChildrenCount; ++i) {
View child = mChildren[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
children[count++] = child;
}
}
FocusFinder.sort(children, 0, count, this, isLayoutRtl());
for (int i = 0; i < count; ++i) {//将自己符合条件的子View添加到views中
children[i].addFocusables(views, direction, focusableMode);
}
// When set to FOCUS_AFTER_DESCENDANTS, we only add ourselves if
// there aren't any focusable descendants. this is
// to avoid the focus search finding layouts when a more precise search
// among the focusable children would be more interesting.
if ((descendantFocusability == FOCUS_AFTER_DESCENDANTS) && focusSelf
&& focusableCount == views.size()) {
super.addFocusables(views, direction, focusableMode);
}
}
讲到这里出现了ViewGroup获取焦点的3种策略,
- FOCUS_BEFORE_DESCENDANTS: 在子View之前优先获取焦点。
- FOCUS_AFTER_DESCENDANTS: 当子View都不获取焦点时,才获取焦点
- FOCUS_BLOCK_DESCENDANTS: 禁止子View获取焦点
而在ViewGroup中默认的descendantFocusability是FOCUS_BEFORE_DESCENDANTS:
private void initViewGroup() {
...
setDescendantFocusability(FOCUS_BEFORE_DESCENDANTS);
...
}
上面就是addFocusables的流程,这个方法很有用,通过复写它,可以简化focusables的大小,从而提高寻焦的效率。Tv开发Leanback框架中GridLayoutManager类的onAddFocusables方法,就是实现的RecyclerView的addFocusables方法。
下一个获取焦点的候选名单已经找好了,但是我们选哪个呢,继续我们寻焦逻辑,findNextFocus还有一个同名重用方法,在这个方法将从这些候选名单中找出那个最优者,当然也有可能找不到。
#FoucsFinder.java
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);//获取focused所在的区域
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);//如果focused为空,那么FOCUS_RIGHT和FOCUS_DOWN对于的
//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;
}
}
}
}
//上面的逻辑就是通过focused计算出它所占据的区域
switch (direction) {
case View.FOCUS_FORWARD:
case View.FOCUS_BACKWARD:
return findNextFocusInRelativeDirection(focusables, root, focused, focusedRect,
direction);
case View.FOCUS_UP:
case View.FOCUS_DOWN:
case View.FOCUS_LEFT:
case View.FOCUS_RIGHT:
//上下左右方向寻找下个获取焦点view的算法
return findNextFocusInAbsoluteDirection(focusables, root, focused,
focusedRect, direction);
default:
throw new IllegalArgumentException("Unknown direction: " + direction);
}
}
上面的逻辑就是通过focused计算出它所占据的区域,一个Rect,然后通过这个focusedRect去和所有的候选者的Rect去做对比,找出“最亲近”的那个,以FOCUS_UP、FOCUS_DOWN、FOCUS_LEFT、FOCUS_RIGHT为例,它的算法实现在findNextFocusInAbsoluteDirection方法中,离真相越来越近了,是不是很鸡冻!!!
#FoucsFinder.java
View findNextFocusInAbsoluteDirection(ArrayList<View> focusables, ViewGroup root, View focused,
Rect focusedRect, int direction) {
// initialize the best candidate to something impossible
// (so the first plausible view will become the best choice)
mBestCandidateRect.set(focusedRect);//mBestCandidateRect为最佳候选View所在的区域,初始化为focusedRect
switch(direction) {
case View.FOCUS_LEFT:
mBestCandidateRect.offset(focusedRect.width() + 1, 0);
break;
case View.FOCUS_RIGHT:
mBestCandidateRect.offset(-(focusedRect.width() + 1), 0);
break;
case View.FOCUS_UP:
mBestCandidateRect.offset(0, focusedRect.height() + 1);
break;
case View.FOCUS_DOWN:
mBestCandidateRect.offset(0, -(focusedRect.height() + 1));
}
View closest = null;
int numFocusables = focusables.size();
for (int i = 0; i < numFocusables; i++) {//遍历整个focusables,寻找下一个最和是的获取焦点的View
View focusable = focusables.get(i);
// only interested in other non-root views
if (focusable == focused || focusable == root) continue;
// get focus bounds of other view in same coordinate system
focusable.getFocusedRect(mOtherRect);//将focusable的位置信息保存在mOtherRect中
root.offsetDescendantRectToMyCoords(focusable, mOtherRect);//统一坐标
if (isBetterCandidate(direction, focusedRect, mOtherRect, mBestCandidateRect)) {
//比较mOtherRect和mBestCandidateRect,如果mOtherRect比mBestCandidateRect更靠近focusedRect则
//将mOtherRect赋值给mBestCandidateRect
mBestCandidateRect.set(mOtherRect);
closest = focusable;
}
}
//当循环完毕,closest就是哪个下一个获取焦点的最佳View
return closest;
}
说明一下上面的逻辑: 给出一个mBestCandidateRect变量,它里面保存了当前那个“最亲近”的最优解,通过遍历focusables,和mBestCandidateRect做对比,如果有比mBestCandidateRect更优的解,则替换这个mBestCandidateRect,直到遍历完成,最后的这个mBestCandidateRect就是我们找的那个最优解。过程类似选择排序。所以还有最后的一个问题了,就是两个Rect的对比,哪个会胜出?哪个更"亲近"?
比较两个View的区域哪个是最好的候选者
#FoucsFinder.java
/**
* Is rect1 a better candidate than rect2 for a focus search in a particular
* direction from a source rect? This is the core routine that determines
* the order of focus searching.
* @param direction the direction (up, down, left, right)
* @param source The source we are searching from
* @param rect1 The candidate rectangle
* @param rect2 The current best candidate.
* @return Whether the candidate is the new best.
*/
boolean isBetterCandidate(int direction, Rect source, Rect rect1, Rect rect2) {
// to be a better candidate, need to at least be a candidate in the first
// place :)
if (!isCandidate(source, rect1, direction)) {//判断是否是候选者,候选view需要在指定的方向里
return false;
}
// we know that rect1 is a candidate.. if rect2 is not a candidate,
// rect1 is better
if (!isCandidate(source, rect2, direction)) {
return true;
}
// if rect1 is better by beam, it wins
if (beamBeats(direction, source, rect1, rect2)) {//判断两个候选区域哪个在光束里,哪个获胜,啥叫光束?后面会介绍,图文介绍
return true;
}
// if rect2 is better, then rect1 cant' be :)
if (beamBeats(direction, source, rect2, rect1)) {
return false;
}
// otherwise, do fudge-tastic comparison of the major and minor axis
return (getWeightedDistanceFor(//比重权重距离,与direction有关
majorAxisDistance(direction, source, rect1),
minorAxisDistance(direction, source, rect1))
< getWeightedDistanceFor(
majorAxisDistance(direction, source, rect2),
minorAxisDistance(direction, source, rect2)));
}
主要代码都贴了,但是如果你没有很大的耐心的话,弄清楚上面的这几段算法代码还是比较困难的(所以一些分支代码就不贴了),我现在直接给出结论,然后你可以再看代码来验证这些结论,当然我会给出一个demo,方便你来验证这些结论,是不是很贴心。上面的代码总结出来有3条算法规则,一图胜千言,我将用3幅图解释这3条算法规则:
-
第一条规则: isCandidate
首先是比较传入的两个Rect,哪个在候选区域,那么啥叫候选区域?候选区域要位于焦点方向上,也就是direction所在的方向里。举个例子,假如你的direction是FOCUS_LEFT, 那么候选区域必须在Focused区域的左边,如下图: A就在候选区域,而B不是。
-
第二条规则:beamBeats
还是以FOCUS_LEFT为例,如下图,A和BEAM(这个BEAM就是那个光束,看图一下就明白了吧)有重合的部分而B没有,则AB比拼中,A胜出。
-
第三条规则:getWeightedDistanceFor
如果beamBeats中,A和B都没有和BEAM有重合的区域,或者都有重合的区域,那该怎么办?这就需要第三条规则, 比较AB区域距离Focused的权重距离,这个权重距离怎么算呢?还是以direction为FOCUS_LEFT为例, 为什么要强调这个direction,因为它会影响权重距离的计算,比如FOCUS_LEFT,他会增加dx的权重,比如
A距离的平方: 13 * dx2^2 + dy2^2, B距离的平方: 13 * dx1^2 + dy2^2. 如果是FOCUS_DOWN则会加大dy的权重。
到现在focusSearch的逻辑已经走完了,有两种结果,一种是我们找到了下一个获取焦点的View,另一种是没找到。如果没找到,让View处理Event事件怎么消费,或者不消费。如果找到了呢?回到ViewRootImpl.performFocusNavigation方法,接下来就是使用找到的View去requestFocus. 它的作用是通知自己的七大姑八大姨说我拿到焦点了,也就是整个View树,同时,拿到焦点后更新一下自己的状态,要不用户怎么知道你这个View拿到焦点了呢,对吧。好,上代码,先看View的requestFocus方法,当然ViewGroup也复写了这个方法。
public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
return requestFocusNoSearch(direction, previouslyFocusedRect);
}
private boolean requestFocusNoSearch(int direction, Rect previouslyFocusedRect) {
// need to be focusable
if (!canTakeFocus()) {//判断当前View有没有获取焦点的资格
return false;
}
// need to be focusable in touch mode if in touch mode
if (isInTouchMode() &&
(FOCUSABLE_IN_TOUCH_MODE != (mViewFlags & FOCUSABLE_IN_TOUCH_MODE))) {//如果是在touch mode,需要
//focusableInTouchMode为true
return false;
}
// need to not have any parents blocking us
if (hasAncestorThatBlocksDescendantFocus()) {//父布局是不是禁止我们获取焦点
return false;
}
if (!isLayoutValid()) {
mPrivateFlags |= PFLAG_WANTS_FOCUS;
} else {
clearParentsWantFocus();
}
//获取焦点
handleFocusGainInternal(direction, previouslyFocusedRect);
return true;
}
获取焦点
void handleFocusGainInternal(@FocusRealDirection int direction, Rect previouslyFocusedRect) {
if (DBG) {
System.out.println(this + " requestFocus()");
}
if ((mPrivateFlags & PFLAG_FOCUSED) == 0) {
mPrivateFlags |= PFLAG_FOCUSED;
View oldFocus = (mAttachInfo != null) ? getRootView().findFocus() : null;
if (mParent != null) {
//层层上报,告诉父布局获取当前View获取焦点,并且将旧的焦点清除
//当传入到最上层的ViewRootImpl会触发一次View树重绘
mParent.requestChildFocus(this, this);
updateFocusedInCluster(oldFocus, direction);
}
if (mAttachInfo != null) {
mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, this);
}
//通知监听焦点变化的listener
onFocusChanged(true, direction, previouslyFocusedRect);
refreshDrawableState();//更新drawble状态,高亮显示等
}
}
层层上报,告诉父布局获取当前View获取焦点,并且将旧的焦点清除
#ViewGroup.java
@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);
}
}
requestChildFocus方法层层上报,到上传到View树的最上层,也就是ViewRootImpl中,会触发一次View树的重绘。这一点在UI效率优化时,应该会用到。
#ViewRootImpl.java
@Override
public void requestChildFocus(View child, View focused) {
if (DEBUG_INPUT_RESIZE) {
Log.v(mTag, "Request child focus: focus now " + focused);
}
checkThread();//检查线程
scheduleTraversals();//触发View重绘
}
总结一下View的requestFoucs做的工作:
- 首先判断当前View是否有资格获取焦点
- 如果符合获取焦点的资格,View添加PFLAG_FOCUSED,并且层层的通知父布局,确保只有一个View获取焦点,当调用到View的最上层ViewRootImpl时会触发整个View树的重绘。
- 调用onFoucsChange,通知监听者。
- 更新drawable状态。
上面是View的requestFocus流程,下面看一下ViewGroup的requestFoucs流程,会比View稍微复杂一些。
public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
if (DBG) {
System.out.println(this + " ViewGroup.requestFocus direction="
+ direction);
}
int descendantFocusability = getDescendantFocusability();
boolean result;
switch (descendantFocusability) {
case FOCUS_BLOCK_DESCENDANTS://禁止子View获取焦点
result = super.requestFocus(direction, previouslyFocusedRect);
break;
case FOCUS_BEFORE_DESCENDANTS: {//优先子View获取焦点
final boolean took = super.requestFocus(direction, previouslyFocusedRect);
result = took ? took : onRequestFocusInDescendants(direction,
previouslyFocusedRect);
break;
}
case FOCUS_AFTER_DESCENDANTS: {//子View优先获取焦点
final boolean took = onRequestFocusInDescendants(direction, previouslyFocusedRect);
result = took ? took : super.requestFocus(direction, previouslyFocusedRect);
break;
}
default:
throw new IllegalStateException(
"descendant focusability must be one of FOCUS_BEFORE_DESCENDANTS,"
+ " FOCUS_AFTER_DESCENDANTS, FOCUS_BLOCK_DESCENDANTS but is "
+ descendantFocusability);
}
if (result && !isLayoutValid() && ((mPrivateFlags & PFLAG_WANTS_FOCUS) == 0)) {
mPrivateFlags |= PFLAG_WANTS_FOCUS;
}
return result;
}
onRequestFocusInDescendants 负责让子View获取焦点,开发者可以复写该方法,自定义子View获取焦点的策略。
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;
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;
}
总结
好了,总结一下,从用遥控器按下键开始,到设备接收到按键信号,然后通过Android输入系统转化为Event事件,分发给当前的窗口的ViewRootImpl,然后通过View的focusSearch查找的下一个获取焦点的View,再通过requestFocus,让它真生得到焦点。这篇文章主要在讨论焦点,所以涉及到Android的输入系统、WMS系统等都比较简单的略过,但是既然是焦点的一生,所以把其来龙去脉都提及了一下,但是涉及的这些模块又非常的重要且复杂,希望后面会单独出文章介绍。完!
参考资料
关于我
- 公众号: CodingDev