android tv常見問題(二)如何監(jiān)聽ViewGroup子View的焦點(diǎn)狀態(tài)

如需轉(zhuǎn)載請?jiān)u論或簡信,并注明出處,未經(jīng)允許不得轉(zhuǎn)載

系列文章

github地址

https://github.com/Geekholt/TvFocus

目錄

期望結(jié)果

只要ViewGroup的內(nèi)部或自身存在焦點(diǎn),ViewGroup就始終保持聚焦樣式。

2.1.gif

實(shí)際結(jié)果

在不做任何處理的情況下,一個(gè)頁面只會存在一個(gè)聚焦的view。


2.2.gif

問題分析

如果我們先不考慮完全重寫Android焦點(diǎn)框架的情況,我們能否做一些特殊處理,來實(shí)現(xiàn)我們期望的結(jié)果呢?從期望結(jié)果描述來看,其實(shí)實(shí)現(xiàn)邏輯還是比較清晰的,就是我們需要拿到兩個(gè)回調(diào):

  1. 當(dāng)ViewGroup自身或者內(nèi)部的View獲得焦點(diǎn)的回調(diào)。
  2. 當(dāng)ViewGroup自身或者內(nèi)部的View失去焦點(diǎn)的回調(diào)。

這就需要我們來看一下View和ViewGroup在requestFocus的過程中觸發(fā)了哪些回調(diào)。

View#requestFocus

public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
        return requestFocusNoSearch(direction, previouslyFocusedRect);
    }

View#requestFocusNoSearch

requestFocusNoSearch校驗(yàn)View的屬性,獲取焦點(diǎn)的前提條件是“可見的”和“可聚焦的”。

 private boolean requestFocusNoSearch(int direction, Rect previouslyFocusedRect) {
        // focusable且visible
        if ((mViewFlags & FOCUSABLE) != FOCUSABLE
                || (mViewFlags & VISIBILITY_MASK) != VISIBLE) {
            return false;
        }

        // 如果是觸摸屏,需要focusableInTouchMode屬性為true
        if (isInTouchMode() &&
            (FOCUSABLE_IN_TOUCH_MODE != (mViewFlags & FOCUSABLE_IN_TOUCH_MODE))) {
               return false;
        }

        // 判斷parent viewGroup是否設(shè)置了FOCUS_BLOCK_DESCENDANTS
        if (hasAncestorThatBlocksDescendantFocus()) {
            return false;
        }

        //實(shí)現(xiàn)View獲取焦點(diǎn)的具體邏輯
        handleFocusGainInternal(direction, previouslyFocusedRect);
        return true;
    }

View#handleFocusGainInternal

這個(gè)是最核心的聚焦邏輯

 void handleFocusGainInternal(@FocusRealDirection int direction, Rect previouslyFocusedRect) {
        if (DBG) {
            System.out.println(this + " requestFocus()");
        }

        if ((mPrivateFlags & PFLAG_FOCUSED) == 0) {
            //當(dāng)前view沒有被聚焦才會進(jìn)入下面的邏輯
            //將view的聚焦標(biāo)識設(shè)置為已聚焦
            mPrivateFlags |= PFLAG_FOCUSED;

            View oldFocus = (mAttachInfo != null) ? getRootView().findFocus() : null;

            if (mParent != null) {
                //通知父控件即將獲取焦點(diǎn)
                mParent.requestChildFocus(this, this);
                updateFocusedInCluster(oldFocus, direction);
            }

            if (mAttachInfo != null) {
                //觸發(fā)全局OnGlobalFocusChangeListener的回調(diào)
                mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, this);
            }

            //觸發(fā)將要被聚焦的View的OnFocusChangeListener回調(diào)
            onFocusChanged(true, direction, previouslyFocusedRect);
            //系統(tǒng)焦點(diǎn)樣式變化,比如我們在Drawable中設(shè)置了focused_state來區(qū)別聚焦或未聚焦樣式
            refreshDrawableState();
        }
    }

ViewGroup#requestChildFocus

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

       //被聚焦的ViewGroup先會調(diào)用一下View的unFocus方法
        super.unFocus(focused);

        
        if (mFocused != child) {
            if (mFocused != null) {
                //mFocused就是當(dāng)前ViewGroup下持有焦點(diǎn)的View或者ViewGroup,是串聯(lián)整個(gè)焦點(diǎn)路徑的屬性
                //注意:View的unFocu方法和ViewGroup的unFocus方法實(shí)現(xiàn)是不一樣的
                mFocused.unFocus(focused);
            }
            //把當(dāng)前最新的焦點(diǎn)child賦值給mFocused
            mFocused = child;
        }
        if (mParent != null) {
            //繼續(xù)往上通知parent
            mParent.requestChildFocus(this, focused);
        }
    }

View的unFocus方法和ViewGroup的unFocus方法實(shí)現(xiàn)是不一樣的,這里如果沒有看清楚可能就會對焦點(diǎn)事件的回調(diào)的方法出現(xiàn)一些誤會。

ViewGroup#unFocus

這個(gè)方法實(shí)際上不是失焦的邏輯,而是一個(gè)遞歸調(diào)用,最終會執(zhí)行View的unFocus方法。View的unFocus方法才是真正的失焦邏輯。

   void unFocus(View focused) {
        if (DBG) {
            System.out.println(this + " unFocus()");
        }
        if (mFocused == null) {
            super.unFocus(focused);
        } else {
            //遞歸調(diào)用,最終會執(zhí)行當(dāng)前聚集的View的unFocus方法
            mFocused.unFocus(focused);
            mFocused = null;
        }
    }

View#unFocus

有兩個(gè)地方會調(diào)用到這個(gè)方法:

  1. 在ViewGroup的unFocus方法中遞歸調(diào)用,最終執(zhí)行當(dāng)前聚焦的view的unfocus方法。
  2. 在ViewGroup中調(diào)用super.unFocus()。這個(gè)是在requestChildFocus方法中進(jìn)行調(diào)用的,用于在子View聚焦之前,先清除一下自身的焦點(diǎn)。

總的來說就是兩種情況,當(dāng)前聚焦的View失去焦點(diǎn)下一個(gè)要被聚焦的View的ViewGroup清除自身焦點(diǎn)。也就是說:

對于View來說,每次聚焦或者失焦都會觸發(fā)View的unFocus方法。

對于ViewGroup來說,當(dāng)焦點(diǎn)從ViewGroup外進(jìn)入到ViewGroup內(nèi)的子View上時(shí),會觸發(fā)View的unFocus方法。而ViewGroup內(nèi)的子View失去焦點(diǎn)時(shí),不會觸發(fā)View的unFocus方法。

這就直接關(guān)系到ViewGroup的onFocusChanged方法是否執(zhí)行,具體邏輯看View的clearFocusInternal方法。

  void unFocus(View focused) {
        if (DBG) {
            System.out.println(this + " unFocus()");
        }

        clearFocusInternal(focused, false, false);
    }

View#clearFocusInternal

clearFocusInternal方法還被clearFocus方法所調(diào)用,注意區(qū)別。clearFocus方法是通過用戶主動(dòng)調(diào)用而失去焦點(diǎn),而unFocus方法是在新的焦點(diǎn)要被聚焦之前,系統(tǒng)內(nèi)部調(diào)用的。

  void clearFocusInternal(View focused, boolean propagate, boolean refocus) {
        if ((mPrivateFlags & PFLAG_FOCUSED) != 0) {
            //view存在焦點(diǎn)才會執(zhí)行這里面的邏輯
            //將view的聚焦標(biāo)識設(shè)置為未聚焦
            mPrivateFlags &= ~PFLAG_FOCUSED;

            if (propagate && mParent != null) {
                //只有主動(dòng)調(diào)用clearfocus方法時(shí)才會執(zhí)行
                mParent.clearChildFocus(this);
            }
            //onFocusChanged回調(diào)
            onFocusChanged(false, 0, null);
            //系統(tǒng)的焦點(diǎn)樣式變化
            refreshDrawableState();

            if (propagate && (!refocus || !rootViewRequestFocus())) {
                //只有主動(dòng)調(diào)用clearfocus方法時(shí)才會執(zhí)行全局焦點(diǎn)變化監(jiān)聽的方法
                //這是由于在unFocus之后,handleFocusGainInternal方法中會繼續(xù)執(zhí)行全局焦點(diǎn)變化監(jiān)                             聽,這里沒必要重復(fù)執(zhí)行。
                notifyGlobalFocusCleared(this);
            }
        }
    }

View#clearFocus

 public void clearFocus() {
        if (DBG) {
            System.out.println(this + " clearFocus()");
        }

        clearFocusInternal(null, true, true);
    }

requestFocus小結(jié)

將要失焦的View:focused

將要失焦的View上層的所有ViewGroup:focusedParent

將要被聚焦的View:next

將要被聚焦的View上層的所有ViewGroup:nextParent

一次聚焦事件回調(diào)方法執(zhí)行的順序是這樣的:

  1. nextParent.requestChildFocus(focused , focused) ;
  2. nextParent.onFocusChanged(false, 0, null);
  3. focused.onFocusChanged(false, 0, null) ;
  4. mTreeObserver.dispatchOnGlobalFocusChange(focused , next);
  5. next.onFocusChanged(true, direction, previouslyFocusedRect)

如果我們主動(dòng)調(diào)用了clearFocus方法來失去焦點(diǎn),那么回調(diào)方法的執(zhí)行順序是這樣的:

  1. mParent.clearChildFocus(focused);
  2. focused.onFocusChanged(false, 0, null);
  3. mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(focused , null);

聚焦流程基本分析完了,回到我們的問題,我們需要監(jiān)聽ViewGroup內(nèi)的View的焦點(diǎn)變化。子View獲取焦點(diǎn)我們可以通過requestChildFocus方法,但是并沒有子View失去焦點(diǎn)的監(jiān)聽(除非我們主動(dòng)調(diào)用clearFocus方法)

或許我們只能通過ViewTreeObserve的dispatchOnGlobalFocusChange方法方法來監(jiān)聽這個(gè)變化。

ViewTreeObserve

使用方法,在ViewGroup中注冊:

 getViewTreeObserver().addOnGlobalFocusChangeListener(new ViewTreeObserver.OnGlobalFocusChangeListener() {
            @Override
            public void onGlobalFocusChanged(View oldFocus, View newFocus) {
                if (hasFocus()) {
                    //焦點(diǎn)進(jìn)入ViewGroup
                } else {
                    //焦點(diǎn)移出ViewGroup
                }
            }
        });

addOnGlobalFocusChangeListener方法

    public void addOnGlobalFocusChangeListener(OnGlobalFocusChangeListener listener) {
        checkIsAlive();

        if (mOnGlobalFocusListeners == null) {
            mOnGlobalFocusListeners = new CopyOnWriteArrayList<OnGlobalFocusChangeListener>();
        }

        mOnGlobalFocusListeners.add(listener);
    }

dispatchOnGlobalFocusChange方法

 final void dispatchOnGlobalFocusChange(View oldFocus, View newFocus) {
        final CopyOnWriteArrayList<OnGlobalFocusChangeListener> listeners = mOnGlobalFocusListeners;
        if (listeners != null && listeners.size() > 0) {
            for (OnGlobalFocusChangeListener listener : listeners) {
                listener.onGlobalFocusChanged(oldFocus, newFocus);
            }
        }
    }

這里的mOnGlobalFocusListeners是一個(gè)ArrayList,所以可以監(jiān)聽多個(gè)view的焦點(diǎn)變化。但是在使用的時(shí)候需要注意一個(gè)問題,注冊的listener在不使用的時(shí)候要及時(shí)的remove,不然會非常影響性能。

解決方案

這里提供大致的思路,具體的方案可以看我寫的demo。demo中還提供了聚焦后的焦點(diǎn)框以及放大的動(dòng)畫效果。

新建一個(gè)類繼承自ViewGroup的子類(我這里繼承了FrameLayout),分別在onAttachedToWindow方法中進(jìn)行注冊,在onDetachedFromWindow方法中進(jìn)行解綁。

@Override
protected void onAttachedToWindow() {
    super.onAttachedToWindow();
    onGlobalFocusChangeListener = new ViewTreeObserver.OnGlobalFocusChangeListener() {
        @Override
        public void onGlobalFocusChanged(View oldFocus, View newFocus) {
            //判斷是否自身被聚焦或者存在子view被聚焦
            if (hasFocus()) {
                focusEnter();
            } else {
                focusLeave();
            }
        }
    };
    getViewTreeObserver().addOnGlobalFocusChangeListener(onGlobalFocusChangeListener);
}

@Override
protected void onDetachedFromWindow() {
    super.onDetachedFromWindow();
    //主要要及時(shí)remove
    getViewTreeObserver().removeOnGlobalFocusChangeListener(onGlobalFocusChangeListener);
}

使用這種方式,mOnGlobalFocusListeners的size等于RecyclerVIew中當(dāng)前可見的繼承于該ViewGroup的item的個(gè)數(shù)。

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

友情鏈接更多精彩內(nèi)容