Android焦點分發(fā)和移動的原理

如果Activity里有EditText玖姑,那么打開Activity后端朵,EditText會自動獲取焦點社付。

為什么呢承疲,很多時候我們不想要這個效果,參照網(wǎng)上的方法將father layout設(shè)置成獲取焦點就解決問題鸥咖。知其然知其所以然燕鸽,翻了一下代碼,答案隱藏在ViewRootImpl.performTraversals方法中啼辣,就是那個view繪制的核心方法啊研,中間有一段:

private void performTraversals() {
   //...
    if (mFirst) {
        // handle first focus request
        if (DEBUG_INPUT_RESIZE) Log.v(mTag, "First: mView.hasFocus()="
                + mView.hasFocus());
        if (mView != null) {
            if (!mView.hasFocus()) {
                mView.requestFocus(View.FOCUS_FORWARD);
                if (DEBUG_INPUT_RESIZE) Log.v(mTag, "First: requested focused view="
                        + mView.findFocus());
            } else {
                if (DEBUG_INPUT_RESIZE) Log.v(mTag, "First: existing focused view="
                        + mView.findFocus());
            }
        }
    }
  //...
}

當(dāng)是第一個view時,會調(diào)用requestFocus獲取焦點熙兔。ViewRootImpl相關(guān)內(nèi)容自行看android的窗口機制悲伶,這個不是今日的目標(biāo),本文要講的是:

  1. requestFocus和背后的焦點分發(fā)機制住涉;
  2. clearFocus真的無效嗎麸锉?
  3. 如果讓焦點按意志移動。

demo

寫了個測試用的demo舆声,上面很多EditText啦花沉,還有上下左右前后等焦點的控制鍵柳爽。

View是否能獲取焦點

讓View獲取焦點,直接調(diào)用requestFocus碱屁,最終會調(diào)用到requestFocusNoSearch:

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
    if (hasAncestorThatBlocksDescendantFocus()) {
        return false;
    }

    handleFocusGainInternal(direction, previouslyFocusedRect);
    return true;
}

requestFocusNoSearch校驗View的屬性磷脯,獲取焦點的前提條件是“可見的”和“可聚焦的”,并且“可聚焦的”需要同時符合:

android:focusable="true"
android:focusableInTouchMode="true"

接著調(diào)用了hasAncestorThatBlocksDescendantFocus娩脾,這個需要了解View的descendantFocusability屬性赵誓。這對我來說是新概念,以前沒有用過柿赊,后文還會涉及俩功,現(xiàn)在先儲備知識。

  • beforeDescendants:ViewGroup會優(yōu)先其子view而獲取到焦點
  • afterDescendants:ViewGroup只有當(dāng)其子view不需要獲取焦點時才獲取焦點
  • blocksDescendants:ViewGroup會覆蓋子view而直接獲得焦點
private boolean hasAncestorThatBlocksDescendantFocus() {
    final boolean focusableInTouchMode = isFocusableInTouchMode();
    ViewParent ancestor = mParent;
    while (ancestor instanceof ViewGroup) {
        final ViewGroup vgAncestor = (ViewGroup) ancestor;
        if (vgAncestor.getDescendantFocusability() == ViewGroup.FOCUS_BLOCK_DESCENDANTS
                || (!focusableInTouchMode && vgAncestor.shouldBlockFocusForTouchscreen())) {
            return true;
        } else {
            ancestor = vgAncestor.getParent();
        }
    }
    return false;
}

hasAncestorThatBlocksDescendantFocus就很好理解碰声,如果有祖先ViewGroup設(shè)置成blocksDescendants诡蜓,那么它的子孫View都不能獲取焦點。

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

    if ((mPrivateFlags & PFLAG_FOCUSED) == 0) {
        mPrivateFlags |= PFLAG_FOCUSED;

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

        if (mParent != null) {
            mParent.requestChildFocus(this, this);
        }

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

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

handleFocusGainInternal實現(xiàn)View獲取焦點的具體邏輯胰挑,所以requestFocusNoSearch默認(rèn)返回true蔓罚。handleFocusGainInternal里面最重要的是調(diào)用了mParent.requestChildFocus,通知它的父view處理焦點瞻颂。mParent的類型是ViewParent豺谈,每一個view都會保存它的父view,基本上實現(xiàn)類就是ViewGroup蘸朋。

然后觸發(fā)onFocusChanged這個listener核无,最后觸發(fā)invalidate進行ui更新。

在繼續(xù)探究requestChildFocus的代碼前团南,先認(rèn)真講講焦點的分發(fā)過程。

焦點分發(fā)過程

有個大家族吐根,已經(jīng)經(jīng)歷多代,族人角色可以這樣定義:

  • 成員:View
  • 有子女的成員:ViewGroup
  • 輩分最高的長老:DecorView

家族中有一件寶貝拷橘,持有在一名成員手上。別的家族想?yún)⒂^冗疮,首先需要找長老。

長老不會一個個成員問檩帐,而是先找大兒子問,再找二兒子問湃密,如此類推四敞。兒子們也是這樣問自己的兒子拔妥,過程也是如此類推忿危。一層層地問,直到最后找到寶貝的持有人没龙,再一層層向上通知铺厨。

寶貝就是焦點,尋找寶貝的過程就是焦點分發(fā)的過程兜畸。

ViewGroup對焦點的處理

看回handleFocusGainInternal里的requestChildFocus努释,view如果需要獲取焦點,需要通知它的父view處理咬摇,所以我們來看ViewGroup的requestChildFocus:

@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 = child;
    }
    if (mParent != null) {
        mParent.requestChildFocus(this, focused);
    }
}

首先會調(diào)用unFocus清除自己的焦點,mFocused表示ViewGroup內(nèi)部是否持有焦點煞躬,如果mFocused不是目標(biāo)獲取焦點的child肛鹏,那么再清除當(dāng)前mFocused的焦點,并將child賦給mFocused恩沛。

最后繼續(xù)通過mParent遞歸調(diào)用requestChildFocus在扰,直到頂層view,保證焦點唯一雷客。

ViewGroup也可以獲取焦點芒珠,和上面View的requestFocus方法不同:

@Override
public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
    if (DBG) {
        System.out.println(this + " ViewGroup.requestFocus direction="
                + direction);
    }
    int descendantFocusability = getDescendantFocusability();

    switch (descendantFocusability) {
        case FOCUS_BLOCK_DESCENDANTS:
            return super.requestFocus(direction, previouslyFocusedRect);
        case FOCUS_BEFORE_DESCENDANTS: {
            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);
    }
}

有了前面descendantFocusability屬性的鋪墊,ViewGroup的requestFocus很容易理解搅裙。block狀態(tài)時皱卓,焦點查找交還給父View;before狀態(tài)時部逮,優(yōu)先自己獲取焦點娜汁;after狀態(tài)時,優(yōu)先子view獲取焦點兄朋。

protected boolean onRequestFocusInDescendants(int direction,
        Rect previouslyFocusedRect) {
    int index;
    int increment;
    int end;
    int count = mChildrenCount;
    if ((direction & FOCUS_FORWARD) != 0) {
        index = 0;
        increment = 1;
        end = count;
    } else {
        index = count - 1;
        increment = -1;
        end = -1;
    }
    final View[] children = mChildren;
    for (int i = index; i != end; i += increment) {
        View child = children[i];
        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
            if (child.requestFocus(direction, previouslyFocusedRect)) {
                return true;
            }
        }
    }
    return false;
}

onRequestFocusInDescendants方法就是向子view詢問焦點的邏輯掐禁,區(qū)分正反兩種查找方向。只要有一個view成功獲取到焦點颅和,就返回true傅事。

清除焦點

上面沒有講view失去焦點的處理,現(xiàn)在來看下ViewGroup的unFocus峡扩,還要探究一下clearFocus“無效”的背后原理蹭越。

@Override
void unFocus(View focused) {
   if (DBG) {
       System.out.println(this + " unFocus()");
   }
   if (mFocused == null) {
       super.unFocus(focused);
   } else {
       mFocused.unFocus(focused);
       mFocused = null;
   }
}

ViewGroup的unFocus,最終調(diào)用了View的unFocus有额。

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

    clearFocusInternal(focused, false, false);
}

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);
        }
    }
}

clearFocusInternal是真正操作焦點失去的地方般又,通過mParent調(diào)用ViewGroup的clearChildFocus。

@Override
public void clearChildFocus(View child) {
    if (DBG) {
        System.out.println(this + " clearChildFocus()");
    }

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

clearChildFocus將當(dāng)前mFocused置空寄悯,通過遞歸向上處理直到頂層view猜旬,保證整顆view樹失去焦點洒擦。

注意熟嫩,unFocus我們并不能調(diào)用掸茅,View提供clearFocus柠逞,內(nèi)部同樣調(diào)用clearFocusInternal板壮,它們不同的地方是refocus傳入不同。

boolean rootViewRequestFocus() {
   final View root = getRootView();
   return root != null && root.requestFocus();
}

refocus的不同撒璧,決定是否會觸發(fā)rootViewRequestFocus沪悲,因此clearFocus“無效”的問題很好理解殿如。如果一個頁面只有一個EditText涉馁,使用clearFocus清除焦點烤送,馬上地糠悯,焦點又被設(shè)置上啦,所以會有清除無效的錯覺试和。因此阅悍,讓父view自動獲取焦點是很好的解決方法节视。

焦點查找

@Override
public View focusSearch(View focused, int direction) {
   if (isRootNamespace()) {
       // root namespace means we should consider ourselves the top of the
       // tree for focus searching; otherwise we could be focus searching
       // into other tabs.  see LocalActivityManager and TabHost for more info
       return FocusFinder.getInstance().findNextFocus(this, focused, direction);
   } else if (mParent != null) {
       return mParent.focusSearch(focused, direction);
   }
   return null;
}

View和ViewGroup提供了focusSearch方法進行焦點查找寻行,入?yún)⑹钱?dāng)前獲取焦點的view和目標(biāo)查找方向拌蜘,返回下一個應(yīng)該獲取焦點的view。focusSearch調(diào)用的是FocusFinder類,直接來看FocusFinder最常用的findNextFocus:

private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) {
    //1
    View next = null;
    if (focused != null) {
        next = findNextUserSpecifiedFocus(root, focused, direction);
    }
    if (next != null) {
        return next;
    }
    //2
    ArrayList<View> focusables = mTempList;
    try {
        focusables.clear();
        root.addFocusables(focusables, direction);
        if (!focusables.isEmpty()) {
            next = findNextFocus(root, focused, focusedRect, direction, focusables);
        }
    } finally {
        focusables.clear();
    }
    return next;
}
1贞滨、預(yù)設(shè)焦點

看標(biāo)記1晓铆,調(diào)用了findNextUserSpecifiedFocus骄噪,查找用戶預(yù)設(shè)不同方向獲取焦點的View链蕊。

private View findNextUserSpecifiedFocus(ViewGroup root, View focused, int direction) {
    // check for user specified next focus
    View userSetNextFocus = focused.findUserSetNextFocus(root, direction);
    if (userSetNextFocus != null && userSetNextFocus.isFocusable()
            && (!userSetNextFocus.isInTouchMode()
                    || userSetNextFocus.isFocusableInTouchMode())) {
        return userSetNextFocus;
    }
    return null;
}

里面調(diào)用了View.findUserSetNextFocus滔韵,在xml文件中陪蜻,我們可以使用android:nextFocusLeft贱鼻、android:nextFocusRight滋将、android:nextFocusUp随闽、android:nextFocusDown齿兔、android:nextFocusForward指定對應(yīng)的View分苇。

2医寿、自動查找焦點

如果沒有預(yù)設(shè),就由程序自動查找须眷。標(biāo)記2收集root下所有能獲取焦點的view沟突,調(diào)用重載版本的findNextFocus方法。

private View findNextFocus(ViewGroup root, View focused, Rect focusedRect,
        int direction, ArrayList<View> focusables) {
   //1    
   //...

   //2
    switch (direction) {
        case View.FOCUS_FORWARD:
        case View.FOCUS_BACKWARD:
            return findNextFocusInRelativeDirection(focusables, root, focused, focusedRect,
                    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);
    }
}

這里我省略了標(biāo)記1一大段代碼扩劝,大約邏輯是計算焦點的矩形范圍职辅,如果當(dāng)前已經(jīng)有view得到焦點域携,直接通過view計算即可秀鞭;如果沒有,那么通過root和方向計算拆内,比較簡單麸恍,就不貼出來占地方。

標(biāo)記2根據(jù)查找方向使用不同算法刻肄,前項和后項使用findNextFocusInRelativeDirection融欧,上下左右使用findNextFocusInAbsoluteDirection噪馏。

對于前項和后項這種按序的查找欠肾,很容易想到需要對view進行排序,這里使用了內(nèi)部類SequentialFocusComparator粹淋,根據(jù)view矩形的高低左右比較桃移。

對于上下左右方向借杰,需要在能獲取焦點view中比較出最適合的一個进泼。首先會設(shè)置一個差的結(jié)果,然后對每一個可以獲取焦點的view調(diào)用isBetterCandidate,找到方向上離自己最近最合適的一個刷袍。算法比較復(fù)雜樊展,有興趣自行研究专缠。


private fun doFocusUp() {
    currentFocus?.let {
        currentFocus.focusSearch(View.FOCUS_UP)?.requestFocus()
    }
}

private fun doFocusDown() {
    currentFocus?.let {
        currentFocus.focusSearch(View.FOCUS_DOWN)?.requestFocus()
    }
}

private fun doFocusLeft() {
    currentFocus?.let {
        currentFocus.focusSearch(View.FOCUS_LEFT)?.requestFocus()
    }
}

private fun doFocusRight() {
    currentFocus?.let {
        currentFocus.focusSearch(View.FOCUS_RIGHT)?.requestFocus()
    }
}

private fun doFocusForward() {
    val focusView = currentFocus ?: return
    FocusFinder.getInstance().findNextFocus(rv_list, focusView, View.FOCUS_BACKWARD)?.requestFocus()
}

private fun doFocusNext() {
    val focusView = currentFocus ?: return
    FocusFinder.getInstance().findNextFocus(rv_list, focusView, View.FOCUS_FORWARD)?.requestFocus()
}

demo里上下左右前后六個方向就是使用FocusFinder實現(xiàn)涝婉。focusSearch限制了只能使用上下左右四個方向,前后兩個方向直接調(diào)用FocusFinder吩跋。

小結(jié)

本文總結(jié)了android焦點常用的方法和原理锌钮,有建議或疑問可以交流一下。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市氛谜,隨后出現(xiàn)的幾起案子混蔼,更是在濱河造成了極大的恐慌,老刑警劉巖遵湖,帶你破解...
    沈念sama閱讀 211,884評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異槽地,居然都是意外死亡,警方通過查閱死者的電腦和手機集畅,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,347評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來窗宦,“玉大人赴涵,你說我怎么就攤上這事髓窜。” “怎么了祈餐?”我有些...
    開封第一講書人閱讀 157,435評論 0 348
  • 文/不壞的土叔 我叫張陵帆阳,是天一觀的道長屋吨。 經(jīng)常有香客問我至扰,道長敢课,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,509評論 1 284
  • 正文 為了忘掉前任濒募,我火速辦了婚禮,結(jié)果婚禮上筝野,老公的妹妹穿的比我還像新娘。我一直安慰自己挥唠,他們只是感情好猛遍,可當(dāng)我...
    茶點故事閱讀 65,611評論 6 386
  • 文/花漫 我一把揭開白布梯醒。 她就那樣靜靜地躺著茸习,像睡著了一般畜隶。 火紅的嫁衣襯著肌膚如雪浸遗。 梳的紋絲不亂的頭發(fā)上跛锌,一...
    開封第一講書人閱讀 49,837評論 1 290
  • 那天,我揣著相機與錄音届惋,去河邊找鬼髓帽。 笑死,一個胖子當(dāng)著我的面吹牛脑豹,可吹牛的內(nèi)容都是我干的郑藏。 我是一名探鬼主播,決...
    沈念sama閱讀 38,987評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼瘩欺,長吁一口氣:“原來是場噩夢啊……” “哼必盖!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起俱饿,我...
    開封第一講書人閱讀 37,730評論 0 267
  • 序言:老撾萬榮一對情侶失蹤歌粥,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后稍途,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,194評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,525評論 2 327
  • 正文 我和宋清朗相戀三年芹敌,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片辞嗡。...
    茶點故事閱讀 38,664評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡挺狰,死狀恐怖蔑祟,靈堂內(nèi)的尸體忽然破棺而出苛败,到底是詐尸還是另有隱情篇亭,我是刑警寧澤柔昼,帶...
    沈念sama閱讀 34,334評論 4 330
  • 正文 年R本政府宣布乙嘀,位于F島的核電站,受9級特大地震影響曹货,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,944評論 3 313
  • 文/蒙蒙 一蝴韭、第九天 我趴在偏房一處隱蔽的房頂上張望庆尘。 院中可真熱鬧,春花似錦、人聲如沸抒抬。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,764評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至岩四,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間茉兰,已是汗流浹背燃辖。 一陣腳步聲響...
    開封第一講書人閱讀 31,997評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留蛋欣,地道東北人。 一個月前我還...
    沈念sama閱讀 46,389評論 2 360
  • 正文 我出身青樓踪少,卻偏偏與公主長得像锉罐,于是被迫代替她去往敵國和親绢陌。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,554評論 2 349

推薦閱讀更多精彩內(nèi)容