android requestFocus()和cleanFocus()源碼分析

在最近寫的項目中遇到由focus引起的問題,例如:

  1. 在兩個嵌套的RecyclerView中,外層滑動停止后,由于內層的RecyclerView獲取了焦點,導致外層又自動滑動了一些距離;

  2. 需要監(jiān)聽EditText的焦點變化來控制界面的顯示于隱藏,在調用EditText的cleanFocus()方法時,回調接口會調用兩次,并且EditText的焦點依然還在。

對遇到的這兩個問題的解決方法都一樣,就是在外層,或根布局中添加focusInTouchModel:true,focus:true兩個屬性,優(yōu)先獲取到焦點就可以了。

布局文件中和focus有關的屬性

//是否可獲取焦點
focusable:true|false
//觸摸模式下是否可獲取焦點
focusableInTouchMode:true|false
descendantFocusability:blocksDescendants|beforeDescendants|afterDescendants

這里解釋一下descendantFocusability的三個屬性值,分別代表的意思:

  1. blocksDescendants:ViewGroup攔截,不讓子 view獲取焦點。
  2. beforeDescendants:ViewGroup優(yōu)先嘗試(嘗試的意思是,根據(jù)View或ViewGroup當前狀態(tài)來判斷是否能得到焦點,如是否可見,是否可獲取焦點等等,在View的requestFocus方法的注釋中提到,下同)獲取焦點,若ViewGroup沒拿到焦點,再遍歷子 view(包括所有直接子 view和間接子 view),讓子 view嘗試獲取焦點。
    3.afterDescendants:先遍歷子 view,讓子 view嘗試獲取焦點,若所有子 view(包括所有直接子 view和間接子 view)都沒拿到焦點,才讓ViewGroup嘗試獲取焦點。

下面是ViewGroup的與descendantFocusability有關的源碼

    @Override
    public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
        if (DBG) {
            System.out.println(this + " ViewGroup.requestFocus direction="
                    + direction);
        }
        //獲取當前viewgroup的descendantFocusability屬性值
        int descendantFocusability = getDescendantFocusability();

        switch (descendantFocusability) {
            case FOCUS_BLOCK_DESCENDANTS:
                //直接調用超類也就是View的方法獲取焦點,忽略子 view
                return super.requestFocus(direction, previouslyFocusedRect);
            case FOCUS_BEFORE_DESCENDANTS: {
                //當前ViewGroup先嘗試獲取焦點,返回值表示是否拿到來焦點,若沒有,則讓子 view嘗試獲取  
                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);
        }
    }
requestFocus() 方法的執(zhí)行流程

上面看的是ViewGroup的requestFocus()方法,而ViewGroup的這個方法只是對ViewGroup和子 view之間獲取焦點的順序做的一個處理,要拿到焦點,最終還是要通過調用View的requestFocus()方法來拿到焦點。所以接下來主要看的是View的方法。

public final boolean requestFocus() {
        return requestFocus(View.FOCUS_DOWN);
    }
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,判斷是否有父view攔截
        if (hasAncestorThatBlocksDescendantFocus()) {
            return false;
        }

        handleFocusGainInternal(direction, previouslyFocusedRect);
        return true;
    }

可以看到,在外部調用requestFocus()這個無參方法后,經(jīng)過層層傳遞后,最終是調用requestFocusNoSearch(int direction, Rect previouslyFocusedRect)方法,這幾個方法的返回值表示是否拿到了焦點,在里面可以看到有3個if判斷語句,這就是我們上面提到的“嘗試”的意思了,即需要根據(jù)當前狀態(tài)判斷,通過判斷,最后直接返回了true,表示拿到了焦點,而在返回之前,還執(zhí)行了一個很重要的方法handleFocusGainInternal(direction, previouslyFocusedRect),來看看里面都干了些什么:

void handleFocusGainInternal(@FocusRealDirection int direction, Rect previouslyFocusedRect) {
        if (DBG) {
            System.out.println(this + " requestFocus()");
        }
        //  若當前已拿到焦點,則方法結束,所以一個已獲焦點的view,在執(zhí)行requestFocus()后,它的onFocusChanged方法是不會回調的
        if ((mPrivateFlags & PFLAG_FOCUSED) == 0) {
            //標記已拿到焦點
            mPrivateFlags |= PFLAG_FOCUSED;
        //上個拿到焦點的view
            View oldFocus = (mAttachInfo != null) ? getRootView().findFocus() : null;
            
            if (mParent != null) {
              //遞歸調用,重置ViewGroup的mFocused的值,重置oldFocus的mPrivateFlags標記位
                mParent.requestChildFocus(this, this);
            }

            if (mAttachInfo != null) {
                mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, this);
            }

            onFocusChanged(true, direction, previouslyFocusedRect);
            refreshDrawableState();
        }
    }

ViewGroup的requestChildFocus(this, this)方法

 @Override
    public void requestChildFocus(View child, View focused) {
        if (DBG) {
            System.out.println(this + " requestChildFocus()");
        }
        if (getDescendantFocusability() == FOCUS_BLOCK_DESCENDANTS) {
            return;
        }

        // Unfocus us, if necessary
        super.unFocus(focused);

        // We had a previous notion of who had focus. Clear it.
        if (mFocused != child) {
            if (mFocused != null) {
                mFocused.unFocus(focused);
            }
          //重置mFocused變量值
            mFocused = child;
        }
        if (mParent != null) {
            mParent.requestChildFocus(this, focused);
        }
    }

然后通過View的unFocus()重置標記位

void unFocus(View focused) {
        if (DBG) {
            System.out.println(this + " unFocus()");
        }
      //注意看兩個參數(shù)都是false
        clearFocusInternal(focused, false, false);
    }
/**
     * Clears focus from the view, optionally propagating the change up through
     * the parent hierarchy and requesting that the root view place new focus.
     *
     * @param propagate whether to propagate the change up through the parent
     *            hierarchy
     * @param refocus when propagate is true, specifies whether to request the
     *            root view place new focus
     */
    void clearFocusInternal(View focused, boolean propagate, boolean refocus) {
        if ((mPrivateFlags & PFLAG_FOCUSED) != 0) {
            mPrivateFlags &= ~PFLAG_FOCUSED;

            if (propagate && mParent != null) {
                mParent.clearChildFocus(this);
            }

            onFocusChanged(false, 0, null);
            refreshDrawableState();

            if (propagate && (!refocus || !rootViewRequestFocus())) {
                notifyGlobalFocusCleared(this);
            }
        }
    }
requestFocus()主要流程是判斷view當前狀態(tài)是否能拿到焦點,若能,則清除原來獲取焦點的view的標記位,然后返回結果。這里說明一下:ViewGroup的mFocused屬性,它表示當前ViewGroup的一個直接子 view獲取,而真正拿到焦點的view則是mFocused或mFocused的子 view。
再來看看CleanFocus()方法
public void clearFocus() {
        if (DBG) {
            System.out.println(this + " clearFocus()");
        }
        //注意參數(shù)
        clearFocusInternal(null, true, true);
    }
/**
     * Clears focus from the view, optionally propagating the change up through
     * the parent hierarchy and requesting that the root view place new focus.
     *
     * @param propagate whether to propagate the change up through the parent
     *            hierarchy
     * @param refocus when propagate is true, specifies whether to request the
     *            root view place new focus
     */
    void clearFocusInternal(View focused, boolean propagate, boolean refocus) {
        //若當前沒有拿到焦點,則直接返回
        if ((mPrivateFlags & PFLAG_FOCUSED) != 0) {
            mPrivateFlags &= ~PFLAG_FOCUSED;

            if (propagate && mParent != null) {
            //將ViewGroup的mFocused置空
                mParent.clearChildFocus(this);
            }

            onFocusChanged(false, 0, null);
            refreshDrawableState();
            //rootViewRequestFocus()讓根視圖重新設置focus,就是之前說的requestFocus方法,按一定順序重新給焦點的過程
            if (propagate && (!refocus || !rootViewRequestFocus())) {
                notifyGlobalFocusCleared(this);
            }
        }
    }

看了上面的代碼可以解釋上面遇到的第二個問題了,當EditText是第一個能獲得焦點的view時,執(zhí)行cleanFocus()方法,在重置焦點標記位時會調用一次onFocusChanged回調方法,在之后的requestFocus流程中,又得到了焦點,所以經(jīng)歷了兩次回調,所以在父 view或根視圖加上focusable和focusableInTouchMode為true時,就解決了這個問題。

關于focus的內容說到這里就完了,相信大家看完這個,對focus應該會有了解了,有不對的地方,歡迎大家指出,互相學習。雖然遇到focus的問題比較少,而且解決起來也很簡單,但還是研究了一下源碼,主要就是學習看源碼的方法,就是debug,跟著一步一步的走,搞清代碼執(zhí)行的流程。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容