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

如需轉(zhuǎn)載請評論或簡信从撼,并注明出處嘶炭,未經(jīng)允許不得轉(zhuǎn)載

系列文章

github地址

https://github.com/Geekholt/TvFocus

目錄

期望結(jié)果

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

2.1.gif

實際結(jié)果

在不做任何處理的情況下宁改,一個頁面只會存在一個聚焦的view。


2.2.gif

問題分析

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

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

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

View#requestFocus

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

View#requestFocusNoSearch

requestFocusNoSearch校驗View的屬性嗡载,獲取焦點的前提條件是“可見的”和“可聚焦的”。

 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是否設置了FOCUS_BLOCK_DESCENDANTS
        if (hasAncestorThatBlocksDescendantFocus()) {
            return false;
        }

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

View#handleFocusGainInternal

這個是最核心的聚焦邏輯

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

        if ((mPrivateFlags & PFLAG_FOCUSED) == 0) {
            //當前view沒有被聚焦才會進入下面的邏輯
            //將view的聚焦標識設置為已聚焦
            mPrivateFlags |= PFLAG_FOCUSED;

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

            if (mParent != null) {
                //通知父控件即將獲取焦點
                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)焦點樣式變化洼滚,比如我們在Drawable中設置了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就是當前ViewGroup下持有焦點的View或者ViewGroup,是串聯(lián)整個焦點路徑的屬性
                //注意:View的unFocu方法和ViewGroup的unFocus方法實現(xiàn)是不一樣的
                mFocused.unFocus(focused);
            }
            //把當前最新的焦點child賦值給mFocused
            mFocused = child;
        }
        if (mParent != null) {
            //繼續(xù)往上通知parent
            mParent.requestChildFocus(this, focused);
        }
    }

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

ViewGroup#unFocus

這個方法實際上不是失焦的邏輯,而是一個遞歸調(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í)行當前聚集的View的unFocus方法
            mFocused.unFocus(focused);
            mFocused = null;
        }
    }

View#unFocus

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

  1. 在ViewGroup的unFocus方法中遞歸調(diào)用摆霉,最終執(zhí)行當前聚焦的view的unfocus方法。
  2. 在ViewGroup中調(diào)用super.unFocus()奔坟。這個是在requestChildFocus方法中進行調(diào)用的携栋,用于在子View聚焦之前,先清除一下自身的焦點咳秉。

總的來說就是兩種情況婉支,當前聚焦的View失去焦點下一個要被聚焦的View的ViewGroup清除自身焦點。也就是說:

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

對于ViewGroup來說滋迈,當焦點從ViewGroup外進入到ViewGroup內(nèi)的子View上時霎奢,會觸發(fā)View的unFocus方法。而ViewGroup內(nèi)的子View失去焦點時饼灿,不會觸發(fā)View的unFocus方法幕侠。

這就直接關系到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方法是通過用戶主動調(diào)用而失去焦點,而unFocus方法是在新的焦點要被聚焦之前舞箍,系統(tǒng)內(nèi)部調(diào)用的舰褪。

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

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

            if (propagate && (!refocus || !rootViewRequestFocus())) {
                //只有主動調(diào)用clearfocus方法時才會執(zhí)行全局焦點變化監(jiān)聽的方法
                //這是由于在unFocus之后,handleFocusGainInternal方法中會繼續(xù)執(zhí)行全局焦點變化監(jiān)                             聽疏橄,這里沒必要重復執(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)

如果我們主動調(diào)用了clearFocus方法來失去焦點,那么回調(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的焦點變化。子View獲取焦點我們可以通過requestChildFocus方法窄绒,但是并沒有子View失去焦點的監(jiān)聽(除非我們主動調(diào)用clearFocus方法)

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

ViewTreeObserve

使用方法,在ViewGroup中注冊:

 getViewTreeObserver().addOnGlobalFocusChangeListener(new ViewTreeObserver.OnGlobalFocusChangeListener() {
            @Override
            public void onGlobalFocusChanged(View oldFocus, View newFocus) {
                if (hasFocus()) {
                    //焦點進入ViewGroup
                } else {
                    //焦點移出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是一個ArrayList彰导,所以可以監(jiān)聽多個view的焦點變化蛔翅。但是在使用的時候需要注意一個問題,注冊的listener在不使用的時候要及時的remove螺戳,不然會非常影響性能搁宾。

解決方案

這里提供大致的思路,具體的方案可以看我寫的demo倔幼。demo中還提供了聚焦后的焦點框以及放大的動畫效果盖腿。

新建一個類繼承自ViewGroup的子類(我這里繼承了FrameLayout),分別在onAttachedToWindow方法中進行注冊损同,在onDetachedFromWindow方法中進行解綁翩腐。

@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();
    //主要要及時remove
    getViewTreeObserver().removeOnGlobalFocusChangeListener(onGlobalFocusChangeListener);
}

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

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
禁止轉(zhuǎn)載茂卦,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。
  • 序言:七十年代末组哩,一起剝皮案震驚了整個濱河市等龙,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌伶贰,老刑警劉巖蛛砰,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異黍衙,居然都是意外死亡泥畅,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進店門琅翻,熙熙樓的掌柜王于貴愁眉苦臉地迎上來位仁,“玉大人柑贞,你說我怎么就攤上這事∧羟溃” “怎么了钧嘶?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長琳疏。 經(jīng)常有香客問我康辑,道長,這世上最難降的妖魔是什么轿亮? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任疮薇,我火速辦了婚禮,結(jié)果婚禮上我注,老公的妹妹穿的比我還像新娘按咒。我一直安慰自己,他們只是感情好但骨,可當我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布励七。 她就那樣靜靜地躺著,像睡著了一般奔缠。 火紅的嫁衣襯著肌膚如雪掠抬。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天校哎,我揣著相機與錄音两波,去河邊找鬼。 笑死闷哆,一個胖子當著我的面吹牛腰奋,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播抱怔,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼劣坊,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了屈留?” 一聲冷哼從身側(cè)響起局冰,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎灌危,沒想到半個月后康二,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡乍狐,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年赠摇,在試婚紗的時候發(fā)現(xiàn)自己被綠了固逗。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片浅蚪。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡藕帜,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出惜傲,到底是詐尸還是另有隱情洽故,我是刑警寧澤,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布盗誊,位于F島的核電站时甚,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏哈踱。R本人自食惡果不足惜荒适,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望开镣。 院中可真熱鬧刀诬,春花似錦、人聲如沸邪财。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽树埠。三九已至糠馆,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間怎憋,已是汗流浹背又碌。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留绊袋,地道東北人赠橙。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓,卻偏偏與公主長得像愤炸,于是被迫代替她去往敵國和親期揪。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,037評論 2 355