Android·Focus機(jī)制解析和常見問題

一、TouchMode

Android支持多種交互方式:D-pad、滾動球、觸摸屏等等。這些交互方式可以分為兩類:TouchMode和非TouchMode。TouchMode對應(yīng)觸摸屏;非TouchMode對應(yīng)實(shí)體鍵盤。兩種模式中對Focus的需求也不同,例如點(diǎn)擊button,在非TouchMode中需要先focus到button,再確認(rèn)觸發(fā)button的點(diǎn)擊事件;在TouchMode中不需要先focus這個button,觸摸按鍵直接觸發(fā)button的點(diǎn)擊事件。兩種模式的對應(yīng)的Event和Event分發(fā)模式也不同,TouchMode產(chǎn)生TouchEvent,對于[TouchEvent分發(fā)機(jī)制],非TouchMode產(chǎn)生KeyEvent,對應(yīng)分發(fā)機(jī)制較為簡單,直接分發(fā)給當(dāng)前獲取了焦點(diǎn)的View。

判斷是否是TouchMode

boolean isInTouchMode=mView.isInTouchMode();

設(shè)置TouchMode

ViewRootImpl viewRoot = getViewRootImpl();
if (viewRoot != null) {
    viewRoot.ensureTouchMode(false); //設(shè)置為非TouchMode
    viewRoot.ensureTouchMode(true);  //設(shè)置為TouchMode
}

二、focus相關(guān)屬性和方法

1. focusable/focusableInTouchModel

含義

focusable:view屬性,表示view是否可以獲取到焦點(diǎn)

focusableInTouchModel: view屬性,表示在TouchMode時view是否可以獲取到焦點(diǎn)。

使用

focusableInTouchModel和focusable屬性并不是單獨(dú)設(shè)置的,當(dāng)調(diào)用setFocusable()setFocusableInTouchMode()設(shè)置時,會根據(jù)屬性值同時修改focusableInTouchModel和focusable。

方法 focusable focusableInTouchMode
setFocusable(true) 設(shè)置為true 不影響
setFocusable(false) 設(shè)置為false 設(shè)置為false
setFocusableInTouchMode(true) 設(shè)置為true 設(shè)置為true
setFocusableInTouchMode(false) 不影響 設(shè)置為false

影響

對于非TouchMode,只有focusable等于true的view可以獲取到焦點(diǎn),獲取到焦點(diǎn)才可以在之后獲取到KeyEvent事件。

對于TouchMode,focusableInTouchModel等于true的view接受到TouchEvent后,會先嘗試獲取焦點(diǎn)。即點(diǎn)擊控件時,不會立即執(zhí)行相應(yīng)的點(diǎn)擊邏輯,而是先顯示焦點(diǎn)(即控件被選中),再點(diǎn)擊才執(zhí)行邏輯。

屏幕快照 2018-06-02 下午3.56.43.png

原理

public boolean onTouchEvent(MotionEvent event) {

    ...
    
        switch (action) {
            case MotionEvent.ACTION_UP:
                boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                    
                    //檢查是否需要獲取焦點(diǎn)并嘗試獲取焦點(diǎn),獲取焦點(diǎn)成功后設(shè)置focusTaken=true
                    boolean focusTaken = false;
                    if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                        focusTaken = requestFocus();
                    }
    
                    if (prepressed) {
                        setPressed(true, x, y);
                   }
    
                    if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                        removeLongPressCallback();
    
                        //當(dāng)focusTaken=true時不觸發(fā)performClick,即不觸發(fā)點(diǎn)擊事件
                        if (!focusTaken) {
                            if (mPerformClick == null) {
                                mPerformClick = new PerformClick();
                            }
                            if (!post(mPerformClick)) {
                                performClick();
                            }
                        }
                    }
    
    ...
}

2. descendantFocusability

含義

descendantFocusability:ViewGroup屬性,定義了當(dāng)ViewGroup需要獲取焦點(diǎn)時執(zhí)行的策略,默認(rèn)值為afterDescendants。

  • beforeDescendants:viewgroup會優(yōu)先其子類控件而獲取到焦點(diǎn)
  • afterDescendants:viewgroup只有當(dāng)其子類控件不需要獲取焦點(diǎn)時才獲取焦點(diǎn)
  • blocksDescendants:viewgroup子類控件不獲得焦點(diǎn)。

實(shí)現(xiàn)原理

ViewGroup中重寫了requestFocus()方法,根據(jù)descendantFocusability執(zhí)行不同的策略

    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:
                //當(dāng)FOCUS_BLOCK_DESCENDANTS時,僅ViewGroup自身嘗試獲取焦點(diǎn)
                return super.requestFocus(direction, previouslyFocusedRect);
            case FOCUS_BEFORE_DESCENDANTS: {
                //當(dāng)FOCUS_BEFORE_DESCENDANTS時,先ViewGroup自身嘗試獲取焦點(diǎn),獲取焦點(diǎn)失敗后遍歷子view嘗試獲取焦點(diǎn)
                final boolean took = super.requestFocus(direction, previouslyFocusedRect);
                return took ? took : onRequestFocusInDescendants(direction, previouslyFocusedRect);
            }
            case FOCUS_AFTER_DESCENDANTS: {
                //當(dāng)FOCUS_AFTER_DESCENDANTS時,先遍歷子view嘗試獲取焦點(diǎn),當(dāng)沒有子view獲取焦點(diǎn)時ViewGroup自身嘗試獲取焦點(diǎn)
                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);
        }
    }

3. 獲取焦點(diǎn)

嘗試獲取焦點(diǎn),獲取條件:

  • TouchMode:focusable=true, focusableInTouchMode=true,
  • 非TouchMode:focusable=true
    private boolean requestFocusNoSearch(int direction, Rect previouslyFocusedRect) {
        //檢查focusable屬性
        if ((mViewFlags & FOCUSABLE_MASK) != FOCUSABLE ||
                (mViewFlags & VISIBILITY_MASK) != VISIBLE) {
            return false;
        }

        //檢查TouchMode和focuableInTouchMode屬性
        if (isInTouchMode() &&
            (FOCUSABLE_IN_TOUCH_MODE != (mViewFlags & FOCUSABLE_IN_TOUCH_MODE))) {
               return false;
        }

        //檢查是否有ViewParent設(shè)置了blocksDescendants
        if (hasAncestorThatBlocksDescendantFocus()) {
            return false;
        }

        //獲取焦點(diǎn)
        handleFocusGainInternal(direction, previouslyFocusedRect);
        return true;
    }

4. isFocused/hasFocus

  • hasFocus:Returns true if this view has focus itself, or is the ancestor of the view that has focus.
  • isFocused:Returns true if this view has focus

三、focus機(jī)制

1. View的focus相關(guān)屬性

focusable=false, focusableInTouchMode=false

說明:TouchMode和非TouchMode都不獲取焦點(diǎn)

舉例:TextView、LinearLayoutView、RelativeLayoutView等,在TouchMode和非TouchMode中都不需要獲取焦點(diǎn)

focusable=true, focusableInTouchMode=false

說明:非TouchMode需要獲取焦點(diǎn),TouchMode不需要獲取焦點(diǎn)

舉例:Button,在非TouchMode時,需要獲取到焦點(diǎn)才能觸發(fā)對應(yīng)事件,在TouchMode時,不需要獲取焦點(diǎn)就能觸發(fā)對應(yīng)事件

focusable=true, focusableInTouchMode=true

說明:TouchMode和非TouchMode都需要獲取焦點(diǎn)

舉例:EditText,不管在哪種模式下,都需要先獲取到焦點(diǎn)才能進(jìn)行輸入

2. focus初始化焦點(diǎn)

當(dāng)Activity創(chuàng)建成功后,初始化focus,遍歷view并嘗試設(shè)置焦點(diǎn),根據(jù)descendantFocusability默認(rèn)為afterDescendants,會優(yōu)先遍歷子view,當(dāng)子view滿足focus設(shè)置條件時設(shè)置為焦點(diǎn)view。(此時為TouchMode,focus設(shè)置條件:focusable=true, focusableInTouchMode=true)

ViewRootImpl

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

3. focus自定義初始化焦點(diǎn)

<RequestFocus/>

適用于:xml布局文件,容器控件和非容器控件都可以使用,需要focusable等于true并且focusableInTouchMode等于true

<EditText
    android:id="@+id/text"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" >
    <requestFocus />
</EditText> 

實(shí)現(xiàn)原理:

LayoutInflater中處理,rInflate方法中處理<RequestFocus/>標(biāo)簽

void rInflate(XmlPullParser parser, View parent, Context context,
        AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {

    final int depth = parser.getDepth();
    int type;

    while (((type = parser.next()) != XmlPullParser.END_TAG ||
            parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {

        if (type != XmlPullParser.START_TAG) {
            continue;
        }

        final String name = parser.getName();
        
        //requestFocus標(biāo)簽
        if (TAG_REQUEST_FOCUS.equals(name)) {
            parseRequestFocus(parser, parent);
        } else if (TAG_TAG.equals(name)) {
            parseViewTag(parser, parent, attrs);
        } else if (TAG_INCLUDE.equals(name)) {
            if (parser.getDepth() == 0) {
                throw new InflateException("<include /> cannot be the root element");
            }
            parseInclude(parser, context, parent, attrs);
        } else if (TAG_MERGE.equals(name)) {
            throw new InflateException("<merge /> must be the root element");
        } else {
            final View view = createViewFromTag(parent, name, context, attrs);
            final ViewGroup viewGroup = (ViewGroup) parent;
            final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
            rInflateChildren(parser, view, attrs, true);
            viewGroup.addView(view, params);
        }
    }

    if (finishInflate) {
        parent.onFinishInflate();
    }
}

private void parseRequestFocus(XmlPullParser parser, View view)
       throws XmlPullParserException, IOException {
    //view獲取標(biāo)簽
    view.requestFocus();
    consumeChildElements(parser);
}

調(diào)用時機(jī):

setContentView()

requestFocus()

適用于:java代碼

四、focus常見問題分析

事件1: ListView中自定義包含ImageButton,Button,CheckBox等控件的子Item,點(diǎn)擊時這些子控件會將焦點(diǎn)獲取到,所以常常當(dāng)點(diǎn)擊item時變化的是子控件,item本身的點(diǎn)擊沒有響應(yīng)。

OnItemClick事件只有在ListView子view都是不可聚焦的情況下觸發(fā)。如果view可以設(shè)置focusable為false,則可以直接設(shè)置為false解決;如果view要求focusable必須為true,可以為每個子控件添加onClickListener,放棄使用OnItemClickListener。

事件2: 有一個ViewGroup A, 包含一個View B, 然后 A, B的focusableInTouchMode都是true, 第一次點(diǎn)擊B, onClickListener不會觸發(fā), 而后續(xù)點(diǎn)擊都可以。

focusable和focusableInTouchMode為true的view,在處理Touch事件時,會優(yōu)先獲取焦點(diǎn),只有在已獲取到焦點(diǎn)時才去觸發(fā)onClick事件。

事件3: EditText自動獲取焦點(diǎn)。

在焦點(diǎn)初始化中,focusable和focusableInTouchMode為true的view會獲取焦點(diǎn)??梢酝ㄟ^指定初始化焦點(diǎn)、設(shè)置優(yōu)先級更高的的可聚焦view解決。

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

相關(guān)閱讀更多精彩內(nèi)容

  • ¥開啟¥ 【iAPP實(shí)現(xiàn)進(jìn)入界面執(zhí)行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開一個線程,因...
    小菜c閱讀 7,388評論 0 17
  • 一、上節(jié)回顧: (一)、三大表單控件中需要記憶的核心方法: 1、RadioButton: RadioGroup類中...
    白話徐文濤閱讀 2,258評論 1 7
  • 一、 Android分發(fā)機(jī)制概述: Android如此受歡迎,就在于其優(yōu)秀的交互性,這其中,Android優(yōu)秀...
    IT楓閱讀 2,673評論 2 9
  • 哦!今天我太高興了!因?yàn)榘俸匣ㄩ_了,她雖然不太香,但她潔白無瑕,很討人喜歡。 可只開了兩朵,誰知道其她的為什么不開...
    鄉(xiāng)下女孩閱讀 333評論 0 0
  • 古人說:“月是故鄉(xiāng)明”,但那年這異鄉(xiāng)的月也是這般明朗。秋天,樹葉枯黃的季節(jié),但唯有那桂花開得那么馥郁,那么絢麗。好...
    醉夕顏閱讀 344評論 0 2

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