android tv常見問題(一)焦點查找規律

如需轉載請評論或簡信,并注明出處,未經允許不得轉載

系列文章

github地址

https://github.com/Geekholt/TvFocus

目錄

期望結果:

Recyclerview聚焦到最后一個Item,繼續按下鍵,焦點保持不變。

實際結果

Recyclerview聚焦到最后一個Item,繼續按下鍵,焦點會跳出RecyclerView,跳到附近的View上。


問題一.gif

問題分析

那么當Recyclerview滑動到最底部時,按下鍵,Android系統是如何找到下一個需要被聚焦的view的呢?我們把斷點打在ViewGroup的focusSearch方法上,可以看到從ViewRootImp的performFocusNavigation方法開始,依次調用了如下方法。

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方法。

主要步驟:

  1. findNextUserSpecifiedFocus 優先從xml或者代碼中指定focusId的View中找。
  2. addFocusables可聚焦可見的view加入到集合中。
  3. 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

主要注意三點:

  1. descendantFocusability屬性決定了ViewGroup和其子view的聚焦優先級
  • FOCUS_BLOCK_DESCENDANTS:viewgroup會覆蓋子類控件而直接獲得焦點
  • FOCUS_BEFORE_DESCENDANTS:viewgroup會覆蓋子類控件而直接獲得焦點
  • FOCUS_AFTER_DESCENDANTS:viewgroup只有當其子類控件不需要獲取焦點時才獲取焦點
  1. addFocusables的第一個參數views是由root決定的。在ViewGroup的focusSearch方法中傳進來的root是DecorView,當然我們也可以主動調用FocusFinder的findNextFocus方法,在指定的ViewGroup中查找焦點
  2. 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事件的流轉,處理焦點的時機,按照優先級(順序)依次是:

  1. dispatchKeyEvent
  2. mOnKeyListener.onKey
  3. onKeyDown/onKeyUp
  4. focusSearch
  5. 指定nextFocusId
  6. 系統自動從所有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源碼畫了一些圖,比較的形象,可以幫助理解

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
禁止轉載,如需轉載請通過簡信或評論聯系作者。

推薦閱讀更多精彩內容