Android焦点requestFocus 源码分析一

1.前言

在 Android 设备里,点击上下左右按键的时候,UI 会随着焦点的改变在底部显示一个阴影,当点击 Enter 键的时候会触发对应 View 的点击事件。本文从源码角度分析一下整个流程,焦点是如何移动的以及点击是如何触发的。

2.Android 方向按键处理

2.1 方向按键

以下是点击了方向按键时相关的调用栈

java.lang.Exception: requestFocusNoSearch direction:130
at android.view.View.requestFocusNoSearch(View.java:13542)
at android.view.View.requestFocus(View.java:13538)
at android.view.ViewGroup.onRequestFocusInDescendants(ViewGroup.java:3374)
at android.view.ViewGroup.requestFocus(ViewGroup.java:3323)
at android.view.ViewGroup.onRequestFocusInDescendants(ViewGroup.java:3374)
at android.view.ViewGroup.requestFocus(ViewGroup.java:3323)
at android.view.ViewGroup.onRequestFocusInDescendants(ViewGroup.java:3374)
at android.view.ViewGroup.requestFocus(ViewGroup.java:3323)
at android.view.ViewGroup.onRequestFocusInDescendants(ViewGroup.java:3374)
at android.view.ViewGroup.requestFocus(ViewGroup.java:3323)
at android.view.ViewGroup.onRequestFocusInDescendants(ViewGroup.java:3374)
at android.view.ViewGroup.requestFocus(ViewGroup.java:3323)
at android.view.ViewGroup.onRequestFocusInDescendants(ViewGroup.java:3374)
at android.view.ViewGroup.requestFocus(ViewGroup.java:3323)
at android.view.ViewGroup.onRequestFocusInDescendants(ViewGroup.java:3374)
at android.view.ViewGroup.requestFocus(ViewGroup.java:3328)
at android.view.View.requestFocus(View.java:13505)
at android.view.View.restoreDefaultFocus(View.java:13483)
at android.view.ViewGroup.restoreDefaultFocus(ViewGroup.java:3390)
at android.view.ViewRootImpl.leaveTouchMode(ViewRootImpl.java:5687)
at android.view.ViewRootImpl.ensureTouchModeLocally(ViewRootImpl.java:5620)
at android.view.ViewRootImpl.ensureTouchMode(ViewRootImpl.java:5602)
at android.view.ViewRootImpl.checkForLeavingTouchModeAndConsume(ViewRootImpl.java:7626)
at android.view.ViewRootImpl.access$2400(ViewRootImpl.java:225)
at android.view.ViewRootImpl$EarlyPostImeInputStage.processKeyEvent(ViewRootImpl.java:6139)
at android.view.ViewRootImpl$EarlyPostImeInputStage.onProcess(ViewRootImpl.java:6123)
at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:5729)
at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:5786)
at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:5752)
at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:5950)
at android.view.ViewRootImpl$ImeInputStage.onFinishedInputEvent(ViewRootImpl.java:6108)
at android.view.inputmethod.InputMethodManager$PendingEvent.run(InputMethodManager.java:3159)
at android.view.inputmethod.InputMethodManager.invokeFinishedInputEventCallback(InputMethodManager.java:2723)
at android.view.inputmethod.InputMethodManager.finishedInputEvent(InputMethodManager.java:2714)
at android.view.inputmethod.InputMethodManager$ImeInputEventSender.onInputEventFinished(InputMethodManager.java:3136)
at android.view.InputEventSender.dispatchInputEventFinished(InputEventSender.java:154)
at android.os.MessageQueue.nativePollOnce(Native Method)
at android.os.MessageQueue.next(MessageQueue.java:335)
at android.os.Looper.loopOnce(Looper.java:161)
at android.os.Looper.loop(Looper.java:288)
at android.app.ActivityThread.main(ActivityThread.java:7870)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003)

从调用栈可以看到,按键是 InputMethodManager$ImeInputEventSender 接收到,然后交由 ViewRootImpl 处理,ViewRootImpl 经过一系类的 State 的链式调用,到了 ViewGroup 的 requestFocus 方法,又最终调用的 View 的 requestFocus 方法。这里我们发现按键事件是从输入法(ImeInputEventSender 是应用用于接收输入法的按键事件的)传过来的,而不是 ViewRootImpl 接收到处理的。

2.2 普通按键分发流程

我们知道 ViewRootImpl 中 WindowInputEventReceiver 能够接受键盘的输入事件,然后交给 View.dispatchKeyEvent 进行分发,一般会分发到 Activity 的 onKeyDown 与 onKeyUp 方法。调用栈如下:

at com.example.myapplication.MainActivity.onKeyDown(MainActivity.kt:106)
    at android.view.KeyEvent.dispatch(KeyEvent.java:2875)
    at android.app.Activity.dispatchKeyEvent(Activity.java:4250)
    at androidx.core.app.ComponentActivity.superDispatchKeyEvent(ComponentActivity.java:122)
    at androidx.core.view.KeyEventDispatcher.dispatchKeyEvent(KeyEventDispatcher.java:84)
    at androidx.core.app.ComponentActivity.dispatchKeyEvent(ComponentActivity.java:140)
    at androidx.appcompat.app.AppCompatActivity.dispatchKeyEvent(AppCompatActivity.java:599)
    at androidx.appcompat.view.WindowCallbackWrapper.dispatchKeyEvent(WindowCallbackWrapper.java:59)
    at androidx.appcompat.app.AppCompatDelegateImpl$AppCompatWindowCallback.dispatchKeyEvent(AppCompatDelegateImpl.java:3090)
    at com.android.internal.policy.DecorView.dispatchKeyEvent(DecorView.java:411)
    at android.view.ViewRootImpl$ViewPostImeInputStage.processKeyEvent(ViewRootImpl.java:6381)
                、、、
    at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:5733)
    at android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:8700)
    at android.view.ViewRootImpl.doProcessInputEvents(ViewRootImpl.java:8651)
    at android.view.ViewRootImpl.enqueueInputEvent(ViewRootImpl.java:8620)
    at android.view.ViewRootImpl$WindowInputEventReceiver.onInputEvent(ViewRootImpl.java:8823)
    at android.view.InputEventReceiver.dispatchInputEvent(InputEventReceiver.java:259)

2.3 系统功能按键处理

实际上并不是所有的按键都会分发给应用,大部分的例如 a、b、c 之类的字母按键会分发给 APP 的 Activity 处理,但是部分功能按键(例如亮度调节按键、音量键、媒体按键等)系统会直接处理,不交给 View 分发。还有些按键(例如上下左右按键、Enter 按键)系统会先交给输入法,输入法处理后再传给与输入法绑定的 window。这部分定义在 InputMethodManager 中。

[InputMethodManager.java]

    private final class ImeInputEventSender extends InputEventSender {
        public ImeInputEventSender(InputChannel inputChannel, Looper looper) {
            super(inputChannel, looper);
        }

        @Override
        public void onInputEventFinished(int seq, boolean handled) {
            finishedInputEvent(seq, handled, false);
        }
    }

   void finishedInputEvent(int seq, boolean handled, boolean timeout) {
        final PendingEvent p;
        synchronized (mH) {
            int index = mPendingEvents.indexOfKey(seq);
            、、、
        }
        invokeFinishedInputEventCallback(p, handled);
    }

    void invokeFinishedInputEventCallback(PendingEvent p, boolean handled) {
        p.mHandled = handled;
        if (p.mHandler.getLooper().isCurrentThread()) {
            p.run();
        } else {
            、、、
        }
    }

    private final class PendingEvent implements Runnable {
         、、、
        @Override
        public void run() {
            mCallback.onFinishedInputEvent(mToken, mHandled);
            、、、
        }
    }

此处 mCallback 的实现类是 ViewRootImpl$ImeInputStage
[ViewRootImpl.java]

    final class ImeInputStage extends AsyncInputStage
            implements InputMethodManager.FinishedInputEventCallback {
        @Override
        public void onFinishedInputEvent(Object token, boolean handled) {
            QueuedInputEvent q = (QueuedInputEvent)token;
            if (handled) {
                finish(q, true);
                return;
            }
            forward(q);
        }
    }

此时如果 handled 为 true 表示输入法已经处理过,走到 ViewRootImpl.finish,否则走到 ViewRootImpl.forward 方法。这两个方法都会触发 ViewRootImpl 内的调用链进行处理,一般不会分发给 Activity。

3.View 焦点获取

3.1 View.requestFocus

[View.java]

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

我们发现无论是通过按键移动焦点还是手动调用 requestFocus 方法进行抢占焦点其实都会走到同一个地方 requestFocusNoSearch。

3.2 View.requestFocusNoSearch

[View.java]

    private boolean requestFocusNoSearch(int direction, Rect previouslyFocusedRect) {
        // 检查当前View是否有焦点,灭有焦点不处理了,直接返回false
        if (!canTakeFocus()) {
            return false;
        }

        // 如果当前是处在触摸模式下,还要检查focusableInTouchMode ,如果为false。
        // 说明 该View不能通过触摸获取焦点
        if (isInTouchMode() &&
            (FOCUSABLE_IN_TOUCH_MODE != (mViewFlags & FOCUSABLE_IN_TOUCH_MODE))) {
               return false;
        }

        // 检查父View是不是阻拦当前View获取焦点
        if (hasAncestorThatBlocksDescendantFocus()) {
            return false;
        }

        if (!isLayoutValid()) {
            mPrivateFlags |= PFLAG_WANTS_FOCUS;
        } else {
            clearParentsWantFocus();
        }

        handleFocusGainInternal(direction, previouslyFocusedRect);
        return true;
    }
    
    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方法会循环向上查找父View,检查DescendantFocusability属性,该属性有三个值。

  • FOCUS_BLOCK_DESCENDANTS:会阻止其子View获取焦点,哪怕子View是可获取焦点的。
  • FOCUS_AFTER_DESCENDANTS:当所有子View都不获取焦点时,该View才获取焦点,也就是最后才获取焦点。
  • FOCUS_BEFORE_DESCENDANTS:在所有子View之前获取焦点。

接下来继续看handleFocusGainInternal实现。

3.3 handleFocusGainInternal

ViewGroup重写了handleFocusGainInternal方法。

[ViewGroup.java]

    @Override
    void handleFocusGainInternal(int direction, Rect previouslyFocusedRect) {
        if (mFocused != null) {
            mFocused.unFocus(this);
            mFocused = null;
            mFocusedInCluster = null;
        }
        super.handleFocusGainInternal(direction, previouslyFocusedRect);
    }

mFocused 记录了ViewGroup中当前以获取到焦点的子View,首先清理掉mFocused ,然后再调用super.handleFocusGainInternal,也就是View的handleFocusGainInternal方法。

[View.java]

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

        if ((mPrivateFlags & PFLAG_FOCUSED) == 0) {
            mPrivateFlags |= PFLAG_FOCUSED;
            //找到之前已经获取到焦点的View,用于清理其焦点。找不到为null,无需清理。
            View oldFocus = (mAttachInfo != null) ? getRootView().findFocus() : null;
            if (mParent != null) {
                mParent.requestChildFocus(this, this);
                updateFocusedInCluster(oldFocus, direction);
            }

            if (mAttachInfo != null) {
                mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, this);
            }
            //调用焦点变更的监听
            onFocusChanged(true, direction, previouslyFocusedRect);
            refreshDrawableState();//更新drawable,突出一下焦点View
        }
    }

首先会更新当前 View 的标记位 mPrivateFlags 记录自己的 isFocused 状态,PFLAG_FOCUSED表示当前View获取焦点,接着通过 rootView 查找到当前的焦点赋值给 oldFocus,以用于后续逐层清理旧的焦点View的焦点,然后调用 parent 的 requestChildFocus 方法告知 parent 自己当前获取到焦点。

[ViewGroup.java]

@Override
public void requestChildFocus(View child, View focused) {
   、、、
    //3.2提到过,判断是FOCUS_BLOCK_DESCENDANTS如果是FOCUS_BLOCK_DESCENDANTS,拦截焦点。
    if (getDescendantFocusability() == FOCUS_BLOCK_DESCENDANTS) {
        return;
    }
    // Unfocus us, if necessary
    super.unFocus(focused);
    // mFocused 记录了当前已经获取到焦点的View,清理掉mFocused 的焦点,并将child赋值给mFocused 。
    if (mFocused != child) {
        if (mFocused != null) {
            //清理旧的焦点View的焦点
            mFocused.unFocus(focused);
        }
        mFocused = child;//更新新的焦点View
    }
    if (mParent != null) {
        mParent.requestChildFocus(this, focused);
    }
}

[View.java]

void clearFocusInternal(View focused, boolean propagate, boolean refocus) {
    if ((mPrivateFlags & PFLAG_FOCUSED) != 0) {
        mPrivateFlags &= ~PFLAG_FOCUSED;//清理焦点标记为
        if (propagate && mParent != null) {
        // 通知 parent 清除自己(当前的焦点)的 mFocus 值,因为焦点已经不在该 View 树节点下
            mParent.clearChildFocus(this);
        }
        // 回调焦点状态变更的通知
        onFocusChanged(false, 0, null);
        // 刷新失去焦点后的 drawable 状态
        refreshDrawableState();
        if (propagate && (!refocus || !rootViewRequestFocus())) {
            notifyGlobalFocusCleared(this);
        }
    }
}

requestChildFocus先进行状态判断,然后通过调用自己以及焦点View的unFocus方法清理焦点,unFocus调用了View的clearFocusInternal方法,同时将当前申请焦点child子View记录到mFocused。在清理完焦点后继续向上层父View请求焦点。因为上层也是ViewGroup,往上递归调用,最终整个View树都将旧的焦点View清理一遍,并将新的焦点View更新到mFocused 记录下来。等到整个View树更新完成,继续回到handleFocusGainInternal内,onFocusChanged回调设置的OnFocusChangeListener监听方法,通知焦点变更。refreshDrawableState刷新背景状态,突出焦点View。

这里有一点要注意一下,当某一个ViewGroup有一个子view获取到了焦点时,该ViewGroup是没有焦点的,有焦点的只有子View。因为焦点的判断是根据标记位mPrivateFlags 判断的,只有子View的标记位被赋值为PFLAG_FOCUSED。isFocused与hasFocus不是一回事。

4.descendantFocusability标记位作用

在前面3.2中提到descendantFocusability有三种行为,决定ViewGroup对焦点的处理方式。这里看一下源码时如何实现的。

[ViewGroup.java]

@Override
public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
    if (DBG) {
        System.out.println(this + " ViewGroup.requestFocus direction="
                + direction);
    }
    int descendantFocusability = getDescendantFocusability();
    // 主要还是看 ViewGroup 设置的焦点拦截模式
    switch (descendantFocusability) {
        case FOCUS_BLOCK_DESCENDANTS:
        // 拦截掉了焦点,前面有提到View中FOCUS_BLOCK_DESCENDANTS直接返回不处理了
        //也就是拦截了焦点
            return super.requestFocus(direction, previouslyFocusedRect);
        case FOCUS_BEFORE_DESCENDANTS: {
        // 首先调用 super 的逻辑在自己中 requestFocus,如果自己请求焦点失败再遍历子 View 进行 requestFocus
            final boolean took = super.requestFocus(direction, previouslyFocusedRect);
            return took ? took : onRequestFocusInDescendants(direction, previouslyFocusedRect);
        }
        case FOCUS_AFTER_DESCENDANTS: {
        // 与 FOCUS_BEFORE_DESCENDANTS 相反,先遍历子 View 进行 requestFocus,如果子 View 都请求焦点失败后再调用 super 的逻辑在自己中 requestFocus
            final boolean took = onRequestFocusInDescendants(direction, previouslyFocusedRect);
            return took ? took : super.requestFocus(direction, previouslyFocusedRect);
        }
        、、、
    }
}

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;
    // mChildren 数组中保存了所有的 childView
    for (int i = index; i != end; i += increment) {
        View child = children[i];
        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
        // 遍历子 View,并且 View 可见
            if (child.requestFocus(direction, previouslyFocusedRect)) {
            // 该子 View 请求焦点
                return true;// 请求焦点成功,直接返回
            }
        }
    }
    return false;
}

onRequestFocusInDescendants 主要功能就是遍历该 ViewGroup 下所有子 View,然后对可见的子 View 调用 requestFocus,如果请求焦点成功,则直接返回 true,至此,ViewGroup.requestFocus 也处理完毕了。

5. findFocus

有些时候我们会通过findFocus方法查询当前获取到焦点的View,该方法在View中定义,View的子类ViewGroup重写了该方法。View的实现比较简单,就是查询一下自己的mPrivateFlags标记位,如果获取到了焦点就将自己返回,否则返回null。下面是ViewGroup 的实现。

[ViewGroup.java]

    @Override
    public View findFocus() {
        if (DBG) {
            System.out.println("Find focus in " + this + ": flags="
                    + isFocused() + ", child=" + mFocused);
        }
        //如果自己获取到了焦点,返回自己
        if (isFocused()) {
            return this;
        }
        //如果是某一层级的子View获取到了焦点,返回子View。
        if (mFocused != null) {
            return mFocused.findFocus();
        }
        return null;
    }
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,002评论 6 509
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,777评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,341评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,085评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,110评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,868评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,528评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,422评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,938评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,067评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,199评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,877评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,540评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,079评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,192评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,514评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,190评论 2 357

推荐阅读更多精彩内容