本文重点针对android TV开发的同窗,分析遥控或键盘按键事件后焦点的分发机制。尤为是刚从手机开发转向TV开发的同窗,由于在实际开发中总会出现丢焦点或者焦点给到非预期的View或者ViewGroup。但此问题实际开发状况比较复杂,本文仅限从android基本的分发机制出发,对总体流程进行梳理,并提供一些方法改变默认行为以达到特定需求。若是有机会后续还会有更偏向实战的内容更新。html
本文源码基于android 7.0java
首先咱们要知道按键事件和触屏事件同样都是从硬件经过系统驱动传递给android framework层的,固然这也不是咱们要关注的重点。事件的入口就是ViewRootImpl的processKeyEvent方法。android
private int processKeyEvent(QueuedInputEvent q) { final KeyEvent event = (KeyEvent) q.mEvent; // ①view树处理事件消费逻辑 if (mView.dispatchKeyEvent(event)) { return FINISH_HANDLED; } if (shouldDropInputEvent(q)) { return FINISH_NOT_HANDLED; } ....//处理ctrl键 也就是快捷按键相关逻辑 // 自动寻焦逻辑 if (event.getAction() == KeyEvent.ACTION_DOWN) { int direction = 0; ....//根据keycode赋值direction if (direction != 0) { // ②寻找当前界面的焦点view/viewGroup View focused = mView.findFocus(); if (focused != null) { // ③根据方向按键寻找合适的focus view View v = focused.focusSearch(direction); if (v != null && v != focused) { ... // ④请求焦点 if (v.requestFocus(direction, mTempRect)) { playSoundEffect(SoundEffectConstants .getContantForFocusDirection(direction)); return FINISH_HANDLED; } } // 没有找到焦点 给view最后一次机会处理按键事件 if (mView.dispatchUnhandledMove(focused, direction)) { return FINISH_HANDLED; } } else { // 若是当前界面没有焦点走这里 View v = focusSearch(null, direction); if (v != null && v.requestFocus(direction)) { return FINISH_HANDLED; } } } } return FORWARD; } 复制代码
如上面的标号,就是寻焦的主要流程。其余的一些判断代码因为篇幅限制就不贴出了。下面分别对上述四个节点一一分析。算法
若是你去看了ViewRootImpl的源码会发现 其中有三个内部类都有这个方法,分别为:ViewPreImeInputStage,EarlyPostImeInputStage,ViewPostImeInputStage他们都继承InputStage类,上面的代码是ViewPostImeInputStage类中的,从类名能够判断按键的处理跟输入法相关,若是输入法在前台则会将事件先分发给输入法。bash
想必你已经很熟悉android事件分发机制了(固然这也不是重点),key事件和touch事件原理都是同样。总得来讲就是由根view,这里就是DecorView它是一个FrameLayout它先分发给activity,以后顺序为activity-->PhoneWindow-->DecorView-->View树,view树中根据focusd path分发,也就是从根节点开始直至focused view 为止的树,具体流程可参看Android按键事件处理流程 -- KeyEvent。 遍历过程当中一旦有节点返回true即表示消费此事件,不然会一直传递下去。**之因此说明这些,是想提供一种拦截焦点的思路,若是按键事件传递过程当中被消费便不会走寻焦逻辑。**具体的流程后续会分享个你们。 ####2.2 findFocus 此方法的核心就是找到当前持有focus的view。 调用者mView即DecorView是FrameLayout 布局,没复写findFocus方法,因此找到ViewGroup中的findFocus方法。markdown
public View findFocus() { if (isFocused()) { return this; } if (mFocused != null) { return mFocused.findFocus(); } return null; } 复制代码
逻辑很简单若是当前view是focused的状态直接返回本身,不然调用内部间接持有focus的子view即mFocused,遍历查找focused view。可见此番查找的路径就是focused tree。app
根据开篇的核心流程,若是在上一步中找到了focused view,则会执行view的focusSearch(int direction)方法,不然执行focusSearch(View focused, int direction)。这两个方法分别来自于View和ViewGroup,但核心功能是一致的,看代码。ide
/** * 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; } } /** * 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. */ 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传递,若是这个过程上层一直没有干涉则会遍历到顶层DecorView。回到上面的分水岭,若是findFocus没有找到focused view,即把null 赋值给focused传递,整个流程不受影响。 重点方法是**FocusFinder.getInstance().findNextFocus(this, focused, direction);**来看源码。oop
/** * Find the next view to take focus in root's descendants, starting from the view * that currently is focused. * @param root Contains focused. Cannot be null. * @param focused Has focus now. * @param direction Direction to look. * @return The next focusable view, or null if none exists. */ 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; } 复制代码
若是当前焦点不为空,则先去读取上层设置的specifiedFocusId。源码分析
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; } /** * 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 int id = mID; return root.findViewByPredicateInsideOut(this, new Predicate<View>() { @Override public boolean apply(View t) { return t.mNextFocusForwardId == id; } }); } } return null; } 复制代码
有木有很熟悉findUserSetNextFocus的实现,若是上层给View/ViewGroup设置了setNextDownId/setNextLeftId/...,则android系统会从root view树中查找此id对应的view并返回,此分支寻焦逻辑结束。可见为View/ViewGroup设置了nextDownId,nextLeftId等属性可定向分配焦点。 若没有设置上面的属性,走下面的流程
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; 复制代码
@Override public void addFocusables(ArrayList<View> views, int direction, int focusableMode) { final int focusableCount = views.size(); final int descendantFocusability = getDescendantFocusability(); if (descendantFocusability != FOCUS_BLOCK_DESCENDANTS) { if (shouldBlockFocusForTouchscreen()) { focusableMode |= FOCUSABLES_TOUCH_MODE; } final int count = mChildrenCount; final View[] children = mChildren; for (int i = 0; i < count; i++) { final View child = children[i]; if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) { child.addFocusables(views, direction, focusableMode); } } } ... } 复制代码
逻辑也出来了,用一个集合存储那些focusable而且可见的view,注意到addFocusables方法调用者是root,也就是整个view树都会进行遍历。FOCUS_BLOCK_DESCENDANTS这个属性也很熟悉,若是为ViewGroup设置该属性则其子view都不会统计到focusable范围中。 最终findNextFocus方法:
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 { ... } 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); } } 复制代码
正常流程到这里focused不为空,focusedRect为空,键值通常为上下左右方向按键,所以走绝对方向。
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); 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++) { 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); root.offsetDescendantRectToMyCoords(focusable, mOtherRect); if (isBetterCandidate(direction, focusedRect, mOtherRect, mBestCandidateRect)) { mBestCandidateRect.set(mOtherRect); closest = focusable; } } return closest; } 复制代码
核心算法就在这里了,遍历focusables集合,拿出每一个view的rect属性和当前focused view的rect进行“距离”的比较,最终获得“距离”最近的候选者并返回。至此,整个寻焦逻辑结束。感兴趣的同窗可研究内部比较的算法。
在整个寻焦过程当中,咱们发现focusSearch方法是public的,所以可在view树的某个节点复写此方法并返回指望view从而达到“拦截”默认寻焦的流程。同理,addFocusables方法也是public的,复写此方法可缩小比较view的范围,提升效率。
最后一步是请求焦点,根据代码条件会出现两个分支,一个是调用两个参数的requestFocus(int direction, Rect previouslyFocusedRect),此方法来自View可是ViewGroup有override;另外一个是一个参数的requestFocus(int direction),来自View且声明为final。因此就要分上一步寻找到的focus目标是View仍是ViewGroup两种状况进行分析。
若是是View,来看View的requestFocus源码
public final boolean requestFocus(int direction) { return requestFocus(direction, null); } public boolean requestFocus(int direction, Rect previouslyFocusedRect) { return requestFocusNoSearch(direction, previouslyFocusedRect); } private boolean requestFocusNoSearch(int direction, Rect previouslyFocusedRect) { // need to be focusable if ((mViewFlags & FOCUSABLE_MASK) != 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; } 复制代码
看来最终都会走到requestFocusNoSearch方法,并且其中的核心方法一看就知道是handleFocusGainInternal。
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) { mParent.requestChildFocus(this, this); } if (mAttachInfo != null) { mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, this); } onFocusChanged(true, direction, previouslyFocusedRect); refreshDrawableState(); } } 复制代码
大体也分为几步:
再来看若是是ViewGroup,ViewGroup的requestFocus源码以下:
/** * {@inheritDoc} * * Looks for a view to give focus to respecting the setting specified by * {@link #getDescendantFocusability()}. * * Uses {@link #onRequestFocusInDescendants(int, android.graphics.Rect)} to * find focus within the children of this group when appropriate. * * @see #FOCUS_BEFORE_DESCENDANTS * @see #FOCUS_AFTER_DESCENDANTS * @see #FOCUS_BLOCK_DESCENDANTS * @see #onRequestFocusInDescendants(int, android.graphics.Rect) */ @Override public boolean requestFocus(int direction, Rect previouslyFocusedRect) { if (DBG) { System.out.println(this + " ViewGroup.requestFocus direction=" + direction); } int descendantFocusability = getDescendantFocusability(); switch (descendantFocusability) { case FOCUS_BLOCK_DESCENDANTS: return super.requestFocus(direction, previouslyFocusedRect); case FOCUS_BEFORE_DESCENDANTS: { final boolean took = super.requestFocus(direction, previouslyFocusedRect); return took ? took : onRequestFocusInDescendants(direction, previouslyFocusedRect); } case FOCUS_AFTER_DESCENDANTS: { final boolean took = onRequestFocusInDescendants(direction, previouslyFocusedRect); return took ? took : super.requestFocus(direction, previouslyFocusedRect); } default: throw new IllegalStateException("descendant focusability must be " + "one of FOCUS_BEFORE_DESCENDANTS, FOCUS_AFTER_DESCENDANTS, FOCUS_BLOCK_DESCENDANTS " + "but is " + descendantFocusability); } } 复制代码
这里必需要清楚descendantFocusability属性值 看注释结合代码逻辑可知,此属性决定requestFocus事件的传递顺序。
那这个值的默认值是什么呢?其实在ViewGroup的构造方法中调用了initViewGroup方法,在这个方法中默认设置了descendantFocusability的属性为FOCUS_BEFORE_DESCENDANTS,也就是本View先处理。 最后看下onRequestFocusInDescendants的源码:
/** * Look for a descendant to call {@link View#requestFocus} on. * Called by {@link ViewGroup#requestFocus(int, android.graphics.Rect)} * when it wants to request focus within its children. Override this to * customize how your {@link ViewGroup} requests focus within its children. * @param direction One of FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, and FOCUS_RIGHT * @param previouslyFocusedRect The rectangle (in this View's coordinate system) * to give a finer grained hint about where focus is coming from. May be null * if there is no hint. * @return Whether focus was taken. */ @SuppressWarnings({"ConstantConditions"}) 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; } 复制代码
由此可知,根据方向键决定遍历顺序,遍历过程只要有一个子View处理了焦点事件便当即返回,整个流程结束。不少经常使用的类都复写过此方法,好比 RecyclerView,ViewPager等等。
整篇文章源码分析挺多的,主要是为了找到可对寻焦逻辑有影响的关键节点,实际上也是Android系统为上层开的"口子",方便根据实际需求改变默认行为。