如需轉載請評論或簡信,并注明出處,未經允許不得轉載
系列文章
- android tv常見問題(一)焦點查找規律
- android tv常見問題(二)如何監聽ViewGroup子View的焦點狀態
- android tv常見問題(三)RecyclerView的焦點記憶
- android tv常見問題(四)焦點變化時,Recyclerview是如何進行滾動的
github地址
https://github.com/Geekholt/TvFocus
目錄
期望結果:
Recyclerview聚焦到最后一個Item,繼續按下鍵,焦點保持不變。
實際結果
Recyclerview聚焦到最后一個Item,繼續按下鍵,焦點會跳出RecyclerView,跳到附近的View上。
問題分析
那么當Recyclerview滑動到最底部時,按下鍵,Android系統是如何找到下一個需要被聚焦的view的呢?我們把斷點打在ViewGroup的focusSearch方法上,可以看到從ViewRootImp的performFocusNavigation方法開始,依次調用了如下方法。
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<View> 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<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) {
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<View> 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<View> 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;
最后分享一篇文章:http://www.lxweimin.com/p/c0475bd1e806,這是我一個同事寫的,針對isBetterCandidate
源碼畫了一些圖,比較的形象,可以幫助理解