android 焦点树,android tv常见问题(一)焦点查找规律

本文深入解析了RecyclerView中焦点管理的机制,包括ViewGroup焦点查找过程、RecyclerView如何处理边界情况下的焦点移动,以及LinearLayoutManager中实现的焦点查找算法。

如需转载请评论或简信,并注明出处,未经允许不得转载

bc7b3878f140

系列文章

github地址

目录

bc7b3878f140

期望结果:

Recyclerview聚焦到最后一个Item,继续按下键,焦点保持不变。

实际结果

Recyclerview聚焦到最后一个Item,继续按下键,焦点会跳出RecyclerView,跳到附近的View上。

bc7b3878f140

问题一.gif

问题分析

那么当Recyclerview滑动到最底部时,按下键,Android系统是如何找到下一个需要被聚焦的view的呢?我们把断点打在ViewGroup的focusSearch方法上,可以看到从ViewRootImp的performFocusNavigation方法开始,依次调用了如下方法。

bc7b3878f140

focusSearch.png

View#focusSearch

View并不会直接去找焦点,而是交给它的parent去找。

public View focusSearch(@FocusRealDirection int direction) {

if (mParent != null) {

//直接交给viewgroup去查找焦点

return mParent.focusSearch(this, direction);

} else {

return null;

}

}

ViewGroup#focusSearch

焦点会逐级的交给父ViewGroup的focusSearch方法去处理,直到最外层的布局,最后实际上是调用了FocusFinder的findNextFocus方法去寻找新的焦点。

public View focusSearch(View focused, int direction) {

if (isRootNamespace()) {

//如果不再viewgroup的focusSearch方法中做拦截,会一直到最顶层的DecorView

return FocusFinder.getInstance().findNextFocus(this, focused, direction);

} else if (mParent != null) {

return mParent.focusSearch(focused, direction);

}

return null;

}

但是这里要注意的是,RecyclerView和其他的ViewGroup不一样,它自己重写了focusSearch方法。所以在焦点查找委托到达到DecorView之前,会先执行RecyclerView的focusSearch方法。

那么,RecyclerView和其他ViewGroup在寻找焦点方面有什么不一样呢?为什么RecyclerView要重写ViewGroup的焦点查找机制呢?想知道这些问题的答案,那我们首先要知道ViewGroup的焦点查找机制。

FocusFinder#findNextFocus

ViewGroup的焦点查找机制的核心其实就是FocusFinder的findNextFocus方法。

主要步骤:

findNextUserSpecifiedFocus 优先从xml或者代码中指定focusId的View中找。

addFocusables 将可聚焦且可见的view加入到集合中。

findNextFocus 在集合中找到最近的一个。

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

View next = null;

ViewGroup effectiveRoot = getEffectiveRoot(root, focused);

if (focused != null) {

//从自己开始向下遍历,如果没找到则从自己的parent开始向下遍历,直到找到id匹配的视图为止。

//也许存在多个相同id的视图,这个方法只会返回在View树中节点范围最近的一个视图。

next = findNextUserSpecifiedFocus(effectiveRoot, focused, direction);

}

if (next != null) {

return next;

}

ArrayList focusables = mTempList;

try {

focusables.clear();

//找到root下所有isVisible && isFocusable的View

effectiveRoot.addFocusables(focusables, direction);

if (!focusables.isEmpty()) {

//从focusables中找到最近的一个

next = findNextFocus(effectiveRoot, focused, focusedRect, direction, focusables);

}

} finally {

focusables.clear();

}

return next;

}

ViewGroup#addFocusables

主要注意三点:

descendantFocusability属性决定了ViewGroup和其子view的聚焦优先级

FOCUS_BLOCK_DESCENDANTS:viewgroup会覆盖子类控件而直接获得焦点

FOCUS_BEFORE_DESCENDANTS:viewgroup会覆盖子类控件而直接获得焦点

FOCUS_AFTER_DESCENDANTS:viewgroup只有当其子类控件不需要获取焦点时才获取焦点

addFocusables的第一个参数views是由root决定的。在ViewGroup的focusSearch方法中传进来的root是DecorView,当然我们也可以主动调用FocusFinder的findNextFocus方法,在指定的ViewGroup中查找焦点。

view 不仅要满足focusable的条件,还要满足visiable的条件。这个条件决定了RecyclerView为什么要自己实现focusSearch,比如RecyclerView聚焦在按键方向上、当前屏幕区域内可见的最后一个item时(其实后面还有n个item),如果用ViewGroup的focusSearch方法,那么当前不可见的下一个item将无法获得焦点。这和我们正常所看到的现象 “按下键,RecyclerView向上滚动,焦点聚焦到下一个item上” 的这种现象不符。具体原因我们之后分析RecyclerView的focusSearch方法时再说。

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

//FOCUS_BLOCK_DESCENDANTS,这里只将viewgroup自身加入到focusable集合当中,所以之 后的焦点查找只能找到ViewGroup自身而不能找到它的子view

super.addFocusables(views, direction, focusableMode);

}

return;

}

if (blockFocusForTouchscreen) {

focusableMode |= FOCUSABLES_TOUCH_MODE;

}

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

//FOCUS_BEFORE_DESCENDANTS,先将ViewGroup加入到focusable集合中

super.addFocusables(views, direction, focusableMode);

}

//之后再将子View加入到focusable集合中

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

//view 不仅要满足focusable的条件,还要满足visiable的条件

children[count++] = child;

}

}

FocusFinder.sort(children, 0, count, this, isLayoutRtl());

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

children[i].addFocusables(views, direction, focusableMode);

}

if ((descendantFocusability == FOCUS_AFTER_DESCENDANTS) && focusSelf

&& focusableCount == views.size()) {

//FOCUS_AFTER_DESCENDANTS,只有当ViewGroup没有focusable的子View时,才会把ViewGroup 自身加入到focusable集合中,否则集合中只有ViewGroup的子View

super.addFocusables(views, direction, focusableMode);

}

}

FocusFInder#findNextFocus

在addFocusables之后,找到指定方向上与当前focused距离最近的view。在进行查找之前,会统一坐标系。

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

int direction, ArrayList focusables) {

if (focused != null) {

if (focusedRect == null) {

focusedRect = mFocusedRect;

}

//取得考虑scroll之后的焦点Rect,该Rect是相对focused视图本身的

focused.getFocusedRect(focusedRect);

//将当前focused视图的坐标系,转换到root的坐标系中,统一坐标,以便进行下一步的计算

root.offsetDescendantRectToMyCoords(focused, focusedRect);

} else {

...

}

switch (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);

}

}

FocusFInder#findNextFocusInAbsoluteDirection

总的来说就是根据当前focused的位置以及按键的方向,循环比较focusable集合中哪一个最适合,然后返回最合适的view,焦点查找就算完成了。

protected View findNextFocusInAbsoluteDirection(ArrayList focusables, ViewGroup root, View focused,Rect focusedRect, int direction) {

//先在当前focused的位置上虚构出一个候选Rect

mBestCandidateRect.set(focusedRect);

switch(direction) {

...

case View.FOCUS_DOWN:

//把focusedRect向上移一个"身位",按键向下,那么他肯定就是优先级最低的了

mBestCandidateRect.offset(0, -(focusedRect.height() + 1));

}

View closest = null;

int numFocusables = focusables.size();

//遍历root下所有可聚焦的view

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

View focusable = focusables.get(i);

//如果focusable是当前focused或者root,跳过继续找

if (focusable == focused || focusable == root) continue;

//将当前focusable也进行统一坐标

focusable.getFocusedRect(mOtherRect);

root.offsetDescendantRectToMyCoords(focusable, mOtherRect);

//进行比较

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

//如果focusable通过筛选条件,赋值给mBestCandidateRect,继续循环比对

mBestCandidateRect.set(mOtherRect);

closest = focusable;

}

}

return closest;

}

FocusFinder#isBetterCandidate

用于比较的方法。分别是将当前聚焦的view,当前遍历到的focusable和目前为止最合适的focusable(i = 0时是优先级最低的rect)进行比较。

/**

*@param source 当前focused

*@param rect1 当前focusable

*@param rect2 目前为止最合适的focusable

*/

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

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(

majorAxisDistance(direction, source, rect1),

minorAxisDistance(direction, source, rect1))

< getWeightedDistanceFor(

majorAxisDistance(direction, source, rect2),

minorAxisDistance(direction, source, rect2)));

}

FocusFinder#isCandidate

判断是否可以做为候选。可以看作是一个初步筛选的方法,但是到底哪个更好还需要看beamBeat方法,这个方法会将通过筛选的focusable和当前最合适的focusable进行比较,选出更合适的一个。

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

switch (direction) {

...

//这里就拿按下键举例,别的方向同理

case View.FOCUS_DOWN:

//这个判断画个图就很好理解了(见下图)

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}.");

}

到这里为止ViewGroup的focusSearch方法基本上就讲完了。那么下面来看一下RecyclerView的focusSearch方法是如何实现焦点查找的。

RecyclerView#FocusSearch

前面讲到了,该方法主要是为了解决RecyclerView聚焦在按键方向上、当前屏幕区域内可见的最后一个item时,当前不可见的下一个item将无法获得焦点。

public View focusSearch(View focused, int direction) {

//可以在LayoutManager.onInterceptFocusSearch()中做一些焦点拦截操作

View result = mLayout.onInterceptFocusSearch(focused, direction);

if (result != null) {

return result;

}

final boolean canRunFocusFailure = mAdapter != null && mLayout != null

&& !isComputingLayout() && !mLayoutFrozen;

final FocusFinder ff = FocusFinder.getInstance();

if (canRunFocusFailure

&& (direction == View.FOCUS_FORWARD || direction == View.FOCUS_BACKWARD)) {

....

} else {

result = ff.findNextFocus(this, focused, direction);

if (result == null && canRunFocusFailure) {

//result == null,说明在当前recyclerview中,当前聚焦的位置,当前按键方向上,当前屏 幕区域内,找不到下一个可以聚焦的点了。

consumePendingUpdateOperations();

final View focusedItemView = findContainingItemView(focused);

if (focusedItemView == null) {

return null;

}

startInterceptRequestLayout();

//焦点搜索失败处理

result = mLayout.onFocusSearchFailed(focused, direction, mRecycler, mState);

stopInterceptRequestLayout(false);

}

}

if (result != null && !result.hasFocusable()) {

if (getFocusedChild() == null) {

return super.focusSearch(focused, direction);

}

requestChildOnScreen(result, null);

return focused;

}

//判断result是否合适,如果不合适,调用ViewGroup的focusSearch方法

//这个方法和FocusFinder的isCandidate方法实现几乎一样

return isPreferredNextFocus(focused, result, direction)

? result : super.focusSearch(focused, direction);

}

mLayout#onFocusSearchFailed

这个方法是由LayoutManager来实现的,这就是RecyclerView的针对上面提到的情况的焦点查找方法。这里主要分析LinearLayoutManager中实现的该方法,如果在使用其他的LayoutManager时出现RecyclelerView焦点不符合预期的话,可以查看对于LayoutManager下的onFocusSearchFailed方法。

主要关注findPartiallyOrCompletelyInvisibleChildClosestToEnd方法,通过这个方法的命名我们大致就可以看出来这个方法的作用了。这个方法主要会根据当前RecyclerVIew的正逆序以及按键方向,找出最近一个部分或完全不可见的View。

public View onFocusSearchFailed(View focused, int focusDirection,

RecyclerView.Recycler recycler, RecyclerView.State state) {

resolveShouldLayoutReverse();

if (getChildCount() == 0) {

return null;

}

final int layoutDir = convertFocusDirectionToLayoutDirection(focusDirection);

if (layoutDir == LayoutState.INVALID_LAYOUT) {

return null;

}

ensureLayoutState();

ensureLayoutState();

final int maxScroll = (int) (MAX_SCROLL_FACTOR * mOrientationHelper.getTotalSpace());

updateLayoutState(layoutDir, maxScroll, false, state);

mLayoutState.mScrollingOffset = LayoutState.SCROLLING_OFFSET_NaN;

mLayoutState.mRecycle = false;

fill(recycler, mLayoutState, state, true);

// nextCandidate is the first child view in the layout direction that's partially

// within RV's bounds, i.e. part of it is visible or it's completely invisible but still

// touching RV's bounds. This will be the unfocusable candidate view to become visible onto

// the screen if no focusable views are found in the given layout direction.

final View nextCandidate;

if (layoutDir == LayoutState.LAYOUT_START) {

nextCandidate = findPartiallyOrCompletelyInvisibleChildClosestToStart(recycler, state);

} else {

//获取距离底部最近的部分或者整体不可见的item,当RecyclerView滑到最底部是会返回null

nextCandidate = findPartiallyOrCompletelyInvisibleChildClosestToEnd(recycler, state);

}

// nextFocus is meaningful only if it refers to a focusable child, in which case it

// indicates the next view to gain focus.

final View nextFocus;

if (layoutDir == LayoutState.LAYOUT_START) {

nextFocus = getChildClosestToStart();

} else {

nextFocus = getChildClosestToEnd();

}

if (nextFocus.hasFocusable()) {

if (nextCandidate == null) {

return null;

}

return nextFocus;

}

return nextCandidate;

}

RecyclerView#isPreferredNextFocus

这个方法是RecyclerView内部的方法,和FocusFinder中的isCandidate方法的逻辑可以说几乎是一摸一样的。

return false:说明最终会执行ViewGroup的FocusSearch方法去寻找焦点,这就出现了一开始demo中焦点跳出RecyclerView的现象。

return true:说明焦点查找已经完成,next就是将要被聚焦的点。

private boolean isPreferredNextFocus(View focused, View next, int direction) {

if (next == null || next == this) {

//这里就是RecyclerView聚焦在最后一个item,继续按下键,这里会return false

return false;

}

if (findContainingItemView(next) == null) {

return false;

}

if (focused == null) {

return true;

}

if (findContainingItemView(focused) == null) {

return true;

}

//下面的逻辑和FocusFinder的isCandidate方法一摸一样,只是RecyclerView内部自己又实现了一遍

mTempRect.set(0, 0, focused.getWidth(), focused.getHeight());

mTempRect2.set(0, 0, next.getWidth(), next.getHeight());

offsetDescendantRectToMyCoords(focused, mTempRect);

offsetDescendantRectToMyCoords(next, mTempRect2);

final int rtl = mLayout.getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL ? -1 : 1;

int rightness = 0;

if ((mTempRect.left < mTempRect2.left

|| mTempRect.right <= mTempRect2.left)

&& mTempRect.right < mTempRect2.right) {

rightness = 1;

} else if ((mTempRect.right > mTempRect2.right

|| mTempRect.left >= mTempRect2.right)

&& mTempRect.left > mTempRect2.left) {

rightness = -1;

}

int downness = 0;

if ((mTempRect.top < mTempRect2.top

|| mTempRect.bottom <= mTempRect2.top)

&& mTempRect.bottom < mTempRect2.bottom) {

downness = 1;

} else if ((mTempRect.bottom > mTempRect2.bottom

|| mTempRect.top >= mTempRect2.bottom)

&& mTempRect.top > mTempRect2.top) {

downness = -1;

}

switch (direction) {

case View.FOCUS_LEFT:

return rightness < 0;

case View.FOCUS_RIGHT:

return rightness > 0;

case View.FOCUS_UP:

return downness < 0;

case View.FOCUS_DOWN:

return downness > 0;

case View.FOCUS_FORWARD:

return downness > 0 || (downness == 0 && rightness * rtl >= 0);

case View.FOCUS_BACKWARD:

return downness < 0 || (downness == 0 && rightness * rtl <= 0);

}

throw new IllegalArgumentException("Invalid direction: " + direction + exceptionLabel());

}

到此为止ViewGroup的focusSearch和RecyclerVIew的focusSearch都分析完了。我们已经知道RecyclerView滑动到最底部的时候,发生了哪些焦点行为,那么解决起来就比较简单了。

focusSearch小结

结合KeyEvent事件的流转,处理焦点的时机,按照优先级(顺序)依次是:

dispatchKeyEvent

mOnKeyListener.onKey

onKeyDown/onKeyUp

focusSearch

指定nextFocusId

系统自动从所有isFocusable的视图中找下一个焦点视图,所以某些时候也可以在addFocusables方法中进行一些处理来改变焦点

以上任一处都可以指定焦点,一旦消费了就不再往下走。

比如前面说到了RecyclerView就是通过重写focusSearch方法对边界上部分可见或不可见的view的焦点查找进行了特殊处理。

解决方案

重写RecyclerView的focusSearch方法

public View focusSearch(View focused, int direction) {

//通过super.focusSearch找到的view

View realNextFocus = super.focusSearch(focused, direction);

//RecyclerView内部下一个可聚焦的点

View nextFocus = FocusFinder.getInstance().findNextFocus(this, focused, direction);

switch (direction) {

case FOCUS_RIGHT:

...

break;

case FOCUS_LEFT:

...

break;

case FOCUS_UP:

...

break;

case FOCUS_DOWN:

//canScrollVertically(1) true表示能滚动,false表示已经滚动到底部

//canScrollVertically(-1) true表示能滚动,false表示已经滚动到顶部

if (nextFocus == null && !canScrollVertically(1)) {

//如果RecyclerView内部不存在下一个可聚焦的view,屏蔽焦点移动

return null;

}

break;

}

return realNextFocus;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值