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

本文由mdnice多平台发布

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Android 中,焦点移动可以通过以下几种方式实现: 1. 通过代码设置焦点:可以通过调用 View 的 requestFocus() 方法来设置焦点,例如: ``` EditText editText = findViewById(R.id.editText); editText.requestFocus(); ``` 2. 通过 XML 属性设置焦点:可以在 XML 文件中使用 android:focusable 和 android:focusableInTouchMode 属性来设置 View 是否可获得焦点,例如: ``` <EditText android:id="@+id/editText" android:layout_width="match_parent" android:layout_height="wrap_content" android:focusable="true" android:focusableInTouchMode="true" /> ``` 3. 通过监听器处理焦点:可以通过设置 View 的 OnFocusChangeListener 来监听焦点变化事件,例如: ``` editText.setOnFocusChangeListener(new View.OnFocusChangeListener() { @Override public void onFocusChange(View v, boolean hasFocus) { if (hasFocus) { // 处理获取焦点事件 } else { // 处理失去焦点事件 } } }); ``` 4. 通过键盘事件处理焦点:可以在处理键盘事件时,根据当前焦点 View 的 ID 或位置,计算出下一个需要获得焦点的 View,并调用其 requestFocus() 方法来设置焦点,例如: ``` editText.setOnKeyListener(new View.OnKeyListener() { @Override public boolean onKey(View v, int keyCode, KeyEvent event) { if (event.getAction() == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER) { View nextView = v.focusSearch(View.FOCUS_DOWN); if (nextView != null) { nextView.requestFocus(); return true; } } return false; } }); ``` 以上是几种常见的焦点移动方式,可以根据自己的需求选择合适的方式来实现。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值