在市场上,手机硬件基本上占领android设备的绝大部分市场,而在TV上,由于人机交互的方式不同,并且当前主流的TV并不具备触摸屏(虽然目前的触屏电视已经面市,但是该类商显产品主要还是2B。),传统TV还是通过遥控器的方向按键进行操控,在android系统中则是通过焦点的移动标识来展示给用户当前的控制点。下面就从接收到遥控器的按键事件开始,一步步分析下系统中的焦点机制是如何响应工作的。(本文基于API 27源码进行分析)

首先,从底层驱动接收到遥控器按键或者触摸屏触摸事件后,通过一步步的转换到android framework中的用户界面层,会回调给ViewRootImpl中的ViewPostImeInputStage,这个内部类的代码稍长,因为不论是触屏还是按键,都是在这里进行初始的分发处理,在此,我们只重点关注按键事件以及焦点的处理:

/**

* 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);

}

}

}

@Override

protected void onDeliverToNext(QueuedInputEvent q) {

...

}

private boolean performFocusNavigation(KeyEvent event) {

...

}

private boolean performKeyboardGroupNavigation(int direction) {

...

}

private int processKeyEvent(QueuedInputEvent q) {

...

}

private int processPointerEvent(QueuedInputEvent q) {

...

}

private void maybeUpdatePointerIcon(MotionEvent event) {

...

}

private int processTrackballEvent(QueuedInputEvent q) {

...

}

private int processGenericMotionEvent(QueuedInputEvent q) {

...

}

}

首先我们来看下onProcess回调中的参数QueuedInputEvent:

private static final class QueuedInputEvent {

public static final int FLAG_DELIVER_POST_IME = 1 << 0;

public static final int FLAG_DEFERRED = 1 << 1;

public static final int FLAG_FINISHED = 1 << 2;

public static final int FLAG_FINISHED_HANDLED = 1 << 3;

public static final int FLAG_RESYNTHESIZED = 1 << 4;

public static final int FLAG_UNHANDLED = 1 << 5;

public QueuedInputEvent mNext;

public InputEvent mEvent;

public InputEventReceiver mReceiver;

public int mFlags;

...

}

// InputEvent的两个子类

public class KeyEvent extends InputEvent implements Parcelable {}

public final class MotionEvent extends InputEvent implements Parcelable {}

触摸或者按键都是一系列的接收事件,QueuedInputEvent实际上是类似Message的一个队列,mNext变量指向的是下一个事件(单向链表的结构)。mEvent变量标记了该事件的类型,我们可以看到android中,InputEvent只有两个子类,一个是KeyEvent按键事件,另一个是MotionEvent触摸事件。回到上面的onProcess方法,很明显我们TV端的是KeyEvent事件,进入processKeyEvent进行按键事件的处理。

@Override

protected int onProcess(QueuedInputEvent q) {

if (q.mEvent instanceof KeyEvent) {// 接收到的事件是按键事件

return processKeyEvent(q);// 进入这个分支,按键事件处理

} else {

final int source = q.mEvent.getSource();// 手指触摸的touch事件

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;// 获取到该按键事件信息,我们常见的KeyCode,Acton,RepeatCount等信息都包含在里面

// Deliver the key to the view hierarchy.

if (mView.dispatchKeyEvent(event)) {// mView实际上就是DecorView,这里看到如果dispatchKeyEvent返回true,会直接返回,这里的按键事件分发后面单独一篇讲解,对比touch事件分发要简单不少

return FINISH_HANDLED;

}

if (shouldDropInputEvent(q)) {// 是否抛弃该事件,里面主要是判断View是否初始化或者还未add进来,window失去焦点(window失去焦点也就是说该window无法交互,所以接收事件也没用,直接返回)

return FINISH_NOT_HANDLED;

}

int groupNavigationDirection = 0;

// 根据tab和shift按键判断导航方向

if (event.getAction() == KeyEvent.ACTION_DOWN

&& event.getKeyCode() == KeyEvent.KEYCODE_TAB) {

if (KeyEvent.metaStateHasModifiers(event.getMetaState(), KeyEvent.META_META_ON)) {

groupNavigationDirection = View.FOCUS_FORWARD;

} else if (KeyEvent.metaStateHasModifiers(event.getMetaState(),

KeyEvent.META_META_ON | KeyEvent.META_SHIFT_ON)) {

groupNavigationDirection = View.FOCUS_BACKWARD;

}

}

// 设置了快捷键

// If a modifier is held, try to interpret the key as a shortcut.

if (event.getAction() == KeyEvent.ACTION_DOWN

&& !KeyEvent.metaStateHasNoModifiers(event.getMetaState())

&& event.getRepeatCount() == 0

&& !KeyEvent.isModifierKey(event.getKeyCode())

&& groupNavigationDirection == 0) {

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) {

if (groupNavigationDirection != 0) {

if (performKeyboardGroupNavigation(groupNavigationDirection)) {

return FINISH_HANDLED;

}

} else {// 真正开始焦点导航的地方

if (performFocusNavigation(event)) {

return FINISH_HANDLED;

}

}

}

return FORWARD;

}

上面经过一系列判断,包括Tab,Shift和快捷键的处理,我们这里重点关注最后的方向键导航处理performFocusNavigation(event):

private boolean performFocusNavigation(KeyEvent event) {

int direction = 0;

switch (event.getKeyCode()) {// 将按键事件的键值转换为View的焦点导航方向值

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();// 从decorview中查找当前的焦点

if (focused != null) {

View v = focused.focusSearch(direction);// 根据方向查找下一个焦点,调用parent的focusSearch查找

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)) {// 对查找到的焦点view调用requestFocus,清除oldFocus的焦点状态

playSoundEffect(SoundEffectConstants// 播放焦点移动音效,处理结束

.getContantForFocusDirection(direction));

return true;

}

}

// Give the focused view a last chance to handle the dpad key.

if (mView.dispatchUnhandledMove(focused, direction)) {// 查找焦点失败,再提供一个机会去处理该次按键事件下view的移动

return true;

}

} else {// 如果当前都没有焦点

if (mView.restoreDefaultFocus()) {// 重新初始化默认焦点,处理完毕

return true;

}

}

}

return false;

}

这里面首先将按键的键值转换为焦点导航方向,主要有6个:FOCUS_BACKWARD,FOCUS_FORWARD,FOCUS_LEFT,FOCUS_UP,FOCUS_RIGHT,FOCUS_DOWN,接着通过findFocus查找到当前视图中的焦点。然后通过focusSearch方法(这个方法是查找焦点的关键方法,一些定制化逻辑可以通过修改此方法实现),根据当前焦点根据导航方向,去寻找下一个应该聚焦的View:

public View focusSearch(@FocusRealDirection int direction) {

if (mParent != null) {

return mParent.focusSearch(this, direction);// 通过parent父View去查找下一个焦点

} else {

return null;

}

}

@Override

public View focusSearch(View focused, int direction) {

if (isRootNamespace()) {// 当前view==decorView,一般我们最终会走到这个分支

// 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;

}

View.focusSearch实际上是调用了parent的focusSearch,一层一层往上,最终也就是走到根布局DecorView的focusSearch,通过FocusFinder来进行焦点查找:

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);// 当前焦点不为null,首先判断用户对当前焦点的view在该方向上是否指定id,也就是我们通常xml中写的nextFocusLeft这种

}

if (next != null) {

return next;// 如果用户指定了下个焦点id,直接返回该id对应的view

}

ArrayList focusables = mTempList;// 这个集合是用来装所有可获得焦点的View

try {

focusables.clear();

effectiveRoot.addFocusables(focusables, direction);// 查找可获得焦点的view,添加进集合

if (!focusables.isEmpty()) {// 存在可获得焦点的view

next = findNextFocus(effectiveRoot, focused, focusedRect, direction, focusables);// 继续在所有可获得焦点的view集合中查找下一个焦点

}

} finally {

focusables.clear();// 查找完毕后清理数据,释放内存

}

return next;// 返回下一个焦点

}

首先会去判断用户有没有手动在xml中指定该方向的下一个焦点view的id,如果指定了直接返回该view作为下一个焦点,流程结束。对于findNextUserSpecifiedFocus方法逻辑还是比较好理解,在此不做展开分析。

接着会查找所有可获得焦点的view,将它们添加到focusables集合中,缩小焦点查找范围。这里有个关键方法:addFocusables,这个方法在平时定制化开发中可以用于焦点记忆,例如leanback视图中每一行recyclerView中的焦点记忆。

public void addFocusables(ArrayList views, @FocusDirection int direction) {

addFocusables(views, direction, isInTouchMode() ? FOCUSABLES_TOUCH_MODE : FOCUSABLES_ALL);

}

public void addFocusables(ArrayList views, @FocusDirection int direction,

@FocusableMode int focusableMode) {

if (views == null) {

return;

}

if (!isFocusable()) {// 不可聚焦,直接返回

return;

}

if ((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE

&& !isFocusableInTouchMode()) {// 触摸模式下,但是focusInTouchMode设置为false,直接返回

return;

}

views.add(this);// 将自己添加到集合中

}

@Override

public void addFocusables(ArrayList 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) {// 如果设置了拦截焦点

if (focusSelf) {

super.addFocusables(views, direction, focusableMode);// 调用View的addFocusables将自己添加进集合

}

return;// 直接返回,不再添加自己view数结构下面的子View

}

if (blockFocusForTouchscreen) {

focusableMode |= FOCUSABLES_TOUCH_MODE;

}

if ((descendantFocusability == FOCUS_BEFORE_DESCENDANTS) && focusSelf) {

super.addFocusables(views, direction, focusableMode);// 调用View的addFocusables将自己添加进集合

}

int count = 0;

final View[] children = new View[mChildrenCount];

for (int i = 0; i < mChildrenCount; ++i) {// 遍历当前viewGroup下的所有子View

View child = mChildren[i];

if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {// view处于可见状态

children[count++] = child;// 赋值给children数组

}

}

FocusFinder.sort(children, 0, count, this, isLayoutRtl());// 根据方向排序

for (int i = 0; i < count; ++i) {

children[i].addFocusables(views, direction, focusableMode);// 如果children[i]这个子view是viewGroup的话,递归调用继续查找该child viewGroup下的子View,直到查找所有最下层的子view,最终调用View.addFocusables判断是否可聚焦,可聚焦则添加进集合

}

// 走到这里,views中已经保存了所有可聚焦的子View

// 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()) {// 如果是FOCUS_AFTER_DESCENDANTS,除了子view判断外,最后将自己也添加进去

super.addFocusables(views, direction, focusableMode);

}

}

经过上面的addFocusables已经将所有可见状态并且可以聚焦的view全部收集到了focusables这个集合中,接着在该集合中去查找下一个焦点:

private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) {

...

if (!focusables.isEmpty()) {// 存在可获得焦点的view

next = findNextFocus(effectiveRoot, focused, focusedRect, direction, focusables);// 继续在所有可获得焦点的view集合中查找下一个焦点

}

...

return next;// 返回下一个焦点

}

private View findNextFocus(ViewGroup root, View focused, Rect focusedRect,

int direction, ArrayList focusables) {

if (focused != null) {// 当前焦点不为null

if (focusedRect == null) {

focusedRect = mFocusedRect;

}

// fill in interesting rect from focused

focused.getFocusedRect(focusedRect);// 获取到当前焦点的rect区域

root.offsetDescendantRectToMyCoords(focused, focusedRect);// 考虑scroll滑动状态,即把视框拉伸至滑动到屏幕外的视图也可见状态,统一坐标系便于下面焦点查找计算

} 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);// 当前焦点为null,将滑动后的左上角作为寻找起始点(scrollX,scrollY),走到这里的话这个focusedRect实际上是个点

break;

case View.FOCUS_FORWARD:

if (root.isLayoutRtl()) {// 会根据rtl区分(某些国家语言是从右往左书写习惯)

setFocusBottomRight(root, focusedRect);

} else {

setFocusTopLeft(root, focusedRect);

}

break;

case View.FOCUS_LEFT:

case View.FOCUS_UP:

setFocusBottomRight(root, focusedRect);// 当前焦点为null,将滑动后的右下角作为寻找起始点(scrollX,scrollY),走到这里的话这个focusedRect实际上是个点

break;

case View.FOCUS_BACKWARD:

if (root.isLayoutRtl()) {// 会根据rtl区分是否将坐标反转

setFocusTopLeft(root, focusedRect);

} else {

setFocusBottomRight(root, focusedRect);

break;

}

}

}

}

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:// 我们重点只关注这方向键的焦点查找算法

return findNextFocusInAbsoluteDirection(focusables, root, focused,

focusedRect, direction);

default:

throw new IllegalArgumentException("Unknown direction: " + direction);

}

}

如果当前焦点不为null,先获取当前焦点的rect视图区域,考虑到scroll状态,将当前焦点的rect坐标系进行转换。

如果当前焦点为null,根据导航方向,设置一个左上角或者右下角的rect为默认的起始参考点,根据这个点再结合方向去计算下一个焦点。

这里我们重点看下上下左右移动的这个方法findNextFocusInAbsoluteDirection,大致看下它内部查找焦点算法:

View findNextFocusInAbsoluteDirection(ArrayList 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);// 将当前焦点的rect赋值给mBestCandidateRect

switch(direction) {// 在反方向上偏移一个width或者height+1个像素点,虚构出来的下一个候补焦点(优先级应该是最低的,因为是反方向平移)

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++) {// 开始遍历所有可聚焦的子view

View focusable = focusables.get(i);

// only interested in other non-root views

if (focusable == focused || focusable == root) continue;// 如果集合中的view是当前的焦点或者viewGroup,直接跳过继续查找下一个

// get focus bounds of other view in same coordinate system

focusable.getFocusedRect(mOtherRect);

root.offsetDescendantRectToMyCoords(focusable, mOtherRect);// 将该view也进行坐标系转换,和当前焦点在同一个坐标系进行计算

if (isBetterCandidate(direction, focusedRect, mOtherRect, mBestCandidateRect)) {

mBestCandidateRect.set(mOtherRect);// 如果找到一个符合的,则将其的区域赋值给虚构的候补焦点,参照物变了之后,继续遍历看有没有更优的

closest = focusable;// 这个closest会不断刷新,因为每次进入该分支,最新的focusable符合条件都会优于上一个候补焦点

}

}

return closest;

}

先获取当前焦点的视图区域rect,然后将该区域按照导航方向的反方向偏移1个像素+当前焦点的width或者height,得到一个虚构的焦点区域mBestCandidateRect。接着就开始遍历之前收集到的所有可见可聚焦的view集合,如果当前遍历的view就是当前焦点或者rootView,直接忽略跳过继续往下遍历查找。遍历的时候,会将遍历的view坐标转换,只有转换坐标后和当前焦点在同一个坐标系,这样才能为下面算法提供准确参数:

// 几个参数含义: direction方向,source当前焦点,rect1当前对比的view,rect2虚构的候补焦点(如果有符合的,rect2会刷新为当前符合条件的view区域,即如果成立,rect1会赋值给下次该方法的rect2)

// 这几个参数命名比较容易弄混,尤其是下面调用算法的时候又改名了,要区分清楚

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;

}

// 走到这里说明上面的isCandidate返回true,也就是当前遍历的rect1符合条件。例如direction为左,说明rect1在当前焦点的左侧,符合条件,加入候选,进行下一步判断

// we know that rect1 is a candidate.. if rect2 is not a candidate,

// rect1 is better

if (!isCandidate(source, rect2, direction)) {// 第一次走到这的话这个isCandidate肯定返回false,因为rect2第一次是我们之前虚构的候补焦点,是在导航的反方向,肯定为false,直接返回true。再后面的话,相当于上一个候补和当前焦点进行比较,肯定返回true,继续下一步判断

return true;

}

// if rect1 is better by beam, it wins

if (beamBeats(direction, source, rect1, rect2)) {// 当前遍历的view也符合条件,将它和上一个候补进行比较

return true;// 当前遍历的view优于上一个候补,将当前遍历的赋值给最新的closest,也就是目前遍历过程中最优焦点

}

// 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(// 计算rect1和rect2相对于当前焦点的距离

majorAxisDistance(direction, source, rect1),

minorAxisDistance(direction, source, rect1))

< getWeightedDistanceFor(

majorAxisDistance(direction, source, rect2),

minorAxisDistance(direction, source, rect2)));

}

重点算法1,计算是否在导航的那侧,在导航方向上允许有重叠。这个算法都是比较xy方向的边界大小,相对于下面的算法2真的是容易理解很多,稍微画几个图就能理解了。

// srcRect当前焦点,destRect比较的view,direction方向

boolean isCandidate(Rect srcRect, Rect destRect, int direction) {

switch (direction) {

case View.FOCUS_LEFT:// 向左:只比较left和right,就是dest是否整体在src的左侧,这里说的是整体,dest可以与src有交集,但是dest的左右边界都不能超过src的右边界

return (srcRect.right > destRect.right || srcRect.left >= destRect.right)

&& srcRect.left > destRect.left;

case View.FOCUS_RIGHT:// 向右:只比较left和right,就是dest是否整体在src的右侧,这里说的是整体,dest可以与src有交集,但是src的左右边界都不能超过dest的右边界

return (srcRect.left < destRect.left || srcRect.right <= destRect.left)

&& srcRect.right < destRect.right;

case View.FOCUS_UP:// 向上:只比较top和bottom,就是dest是否整体在src的上面,这里说的是整体,dest可以与src有交集,但是dest的上下边界都不能超过src的下边界

return (srcRect.bottom > destRect.bottom || srcRect.top >= destRect.bottom)

&& srcRect.top > destRect.top;

case View.FOCUS_DOWN:// 向下:只比较top和bottom,就是dest是否整体在src的下面,这里说的是整体,dest可以与src有交集,但是src的上下边界都不能超过dest的下边界

return (srcRect.top < destRect.top || srcRect.bottom <= destRect.top)

&& srcRect.bottom < destRect.bottom;

}

throw new IllegalArgumentException("direction must be one of "

+ "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");

}

重点算法2,这算法看着真的很乱,官方的注释也不好理解,不好描述,还是自己画几张图按流程跑一下去理解吧。

// direction方向,source当前焦点,rect1比较的view1,rect2比较的view2(rect1和rect2具体看上面算法调用的顺序)

// 第一次调用:rect1当前遍历的view,rect2上一次符合条件的候补焦点

// 第二次调用:rect1上一次符合条件的候补焦点,rect2当前遍历的view

boolean beamBeats(int direction, Rect source, Rect rect1, Rect rect2) {

final boolean rect1InSrcBeam = beamsOverlap(direction, source, rect1);// rect1和当前焦点在相对于导航方向的垂直方向是否有重叠,导航方向为左右x轴时比较y轴重叠

final boolean rect2InSrcBeam = beamsOverlap(direction, source, rect2);// rect2和当前焦点在相对于导航方向的垂直方向是否有重叠,导航方向为上下y轴时比较x轴重叠

// if rect1 isn't exclusively in the src beam, it doesn't win

if (rect2InSrcBeam || !rect1InSrcBeam) {// rect2有重叠,或者rect1没有重叠

// 第一次调用:上一次符合条件的候补焦点与当前焦点有重叠,或者当前遍历的view与当前焦点没有重叠

return false;// 如果第一次进入此return false,下次进来肯定跳过这里

}

// we know rect1 is in the beam, and rect2 is not

// if rect1 is to the direction of, and rect2 is not, rect1 wins.

// for example, for direction left, if rect1 is to the left of the source

// and rect2 is below, then we always prefer the in beam rect1, since rect2

// could be reached by going down.

if (!isToDirectionOf(direction, source, rect2)) {

return true;

}

// for horizontal directions, being exclusively in beam always wins

if ((direction == View.FOCUS_LEFT || direction == View.FOCUS_RIGHT)) {

return true;

}

// for vertical directions, beams only beat up to a point:

// now, as long as rect2 isn't completely closer, rect1 wins

// e.g for direction down, completely closer means for rect2's top

// edge to be closer to the source's top edge than rect1's bottom edge.

return (majorAxisDistance(direction, source, rect1)

< majorAxisDistanceToFarEdge(direction, source, rect2));

}

计算相对于导航方向的垂直方向上是否有重叠

// direction方向,rect1当前焦点,rect2待比较的view

boolean beamsOverlap(int direction, Rect rect1, Rect rect2) {

switch (direction) {

case View.FOCUS_LEFT:

case View.FOCUS_RIGHT:

return (rect2.bottom > rect1.top) && (rect2.top < rect1.bottom);// 左右按键时比较y方向是否重叠

case View.FOCUS_UP:

case View.FOCUS_DOWN:

return (rect2.right > rect1.left) && (rect2.left < rect1.right);// 上下按键时比较x方向是否重叠

}

throw new IllegalArgumentException("direction must be one of "

+ "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");

}

计算是否完全在当前src的一侧

// src当前焦点,dest待比较的view

boolean isToDirectionOf(int direction, Rect src, Rect dest) {// 比较dest是否完全在当前焦点的左/右/上/下

switch (direction) {

case View.FOCUS_LEFT:

return src.left >= dest.right;

case View.FOCUS_RIGHT:

return src.right <= dest.left;

case View.FOCUS_UP:

return src.top >= dest.bottom;

case View.FOCUS_DOWN:

return src.bottom <= dest.top;

}

throw new IllegalArgumentException("direction must be one of "

+ "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");

}

计算主轴方向距离

// 计算主轴方向距离

static int majorAxisDistance(int direction, Rect source, Rect dest) {

return Math.max(0, majorAxisDistanceRaw(direction, source, dest));

}

static int majorAxisDistanceRaw(int direction, Rect source, Rect dest) {

switch (direction) {

case View.FOCUS_LEFT:

return source.left - dest.right;

case View.FOCUS_RIGHT:

return dest.left - source.right;

case View.FOCUS_UP:

return source.top - dest.bottom;

case View.FOCUS_DOWN:

return dest.top - source.bottom;

}

throw new IllegalArgumentException("direction must be one of "

+ "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");

}

计算相对于主轴方向的垂直方向距离

// 计算次轴方向距离

static int minorAxisDistance(int direction, Rect source, Rect dest) {

switch (direction) {

case View.FOCUS_LEFT:

case View.FOCUS_RIGHT:

// the distance between the center verticals

return Math.abs(

((source.top + source.height() / 2) -

((dest.top + dest.height() / 2))));

case View.FOCUS_UP:

case View.FOCUS_DOWN:

// the distance between the center horizontals

return Math.abs(

((source.left + source.width() / 2) -

((dest.left + dest.width() / 2))));

}

throw new IllegalArgumentException("direction must be one of "

+ "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");

}

计算相对距离,以FOCUS_LEFT为例,majorAxisDistance相当于当前焦点左侧与比较view的右侧的x轴距离,minorAxisDistance相当于在y轴方向上,当前焦点中心点与比较view的中心点的距离。计算13 * x² * y²,这个13的权重系数不知道google是如何制定的,这里就理解为主轴的权重优先级更高吧。(如果是我设计的话,应该会直接计算x和y的距离平方根进行比较了。)

int getWeightedDistanceFor(int majorAxisDistance, int minorAxisDistance) {

return 13 * majorAxisDistance * majorAxisDistance

+ minorAxisDistance * minorAxisDistance;

}

920f0a905d0a

majorAxisDistance.jpg

唉,这方法又得和上面的majorAxisDistance进行区分,以FOCUS_LEFT为例,同样是计算x轴方向,但是majorAxisDistance计算的是souce的左侧和待比较view的右侧距离,这个方法计算的是source的左侧和待比较view的左侧的距离:

static int majorAxisDistanceToFarEdge(int direction, Rect source, Rect dest) {

return Math.max(1, majorAxisDistanceToFarEdgeRaw(direction, source, dest));

}

// 也是计算主轴方向,但是和majorAxisDistance有区别

static int majorAxisDistanceToFarEdgeRaw(int direction, Rect source, Rect dest) {

switch (direction) {

case View.FOCUS_LEFT:

return source.left - dest.left;

case View.FOCUS_RIGHT:

return dest.right - source.right;

case View.FOCUS_UP:

return source.top - dest.top;

case View.FOCUS_DOWN:

return dest.bottom - source.bottom;

}

throw new IllegalArgumentException("direction must be one of "

+ "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");

}

920f0a905d0a

majorAxisDistanceToFarEdge.jpg

遍历过程中,每次进入isBetterCandidate成立后,closest都会更新为下一个焦点的最优解,遍历结束后,这个closest就是计算出来的下一个焦点,直接返回给上面的ViewRootImpl.performFocusNavigation,至此寻焦结束,接着用该查找出来的焦点view调用requestFocus,requestFocus之前已经分析过,主要就是清除上一个焦点的状态,刷新当前焦点,流程结束。

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐