android 焦点分发源码解析

本文深入探讨了Android中焦点(Focus)的概念及其在不同输入模式下的行为变化,特别是TouchMode下焦点分配机制的实现细节。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

TouchMode

android焦点的概念其实一直都在,我们平时做android开发的时候往往只有EditText有焦点,其他都不需要关心焦点。但是做ott开发,焦点就是经常要考虑的问题。
早期的android不支持触屏,是通过trackball来控制的,后来多了触屏,android 增加了一种概念,叫TouchMode。当用户通过触屏操作手机的时候自动进入叫TouchMode,当用户通过trackball或者遥控器操作手机或者android tv的时候,就会自动退出叫TouchMode。
在TouchMode的时候,大部分控件是不需要焦点的(EditText除外)。在非TouchMode的时候,控件首先要获得焦点,然后才能点击。

focusable与EditText

如果想要一个控件可以获取到焦点,往往用如下代码

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

focusable代表的是能不能获取到焦点,focusableInTouchMode代表的是TouchMode下能否获取到焦点。

默认情况下大部分的view都是无法获取焦点的,但是EditText可以,为什么?看下面的代码就明白了,构造的时候设置了这个参数。

    public EditText(Context context, AttributeSet attrs) {
        this(context, attrs, com.android.internal.R.attr.editTextStyle);
    }
//themes.xml
<item name="editTextStyle">@style/Widget.EditText</item>
//styles.xml
    <style name="Widget.EditText">
        <item name="focusable">true</item>
        <item name="focusableInTouchMode">true</item>
        <item name="clickable">true</item>
        ...
    </style>

获取焦点

对应工程Focus

焦点从上往下分发

activity第一次performTraversals的时候会请求焦点,如下所示,下边的mView是DecorView,调用DecorView的requestFocus,这个方法会逐渐往下去寻找可以获取焦点的view,找到一个可以获取焦点的view,就返回true。

//performTraversals:2023, ViewRootImpl (android.view)
            if (mView != null) {
                if (!mView.hasFocus()) {
                    mView.requestFocus(View.FOCUS_FORWARD);
                } 
            }

这里的requestFocus会调用ViewGroup的requestFocus,主要根据descendantFocusability来分发焦点,DecorView是FOCUS_AFTER_DESCENDANTS的(一般的ViewGropup是FOCUS_BEFORE_DESCENDANTS),所以DecorView焦点分发的顺序如下所示,先子后父,如果子view里处理了,就不用给父view了。注意下边的requestFocus和super.requestFocus是完全不一样的,requestFocus是ViewGroup的方法,super.requestFocus是View的方法(是不是跟事件分发非常像?)。

requestFocus->1、onRequestFocusInDescendants
                          2super.requestFocus  

onRequestFocusInDescendants的流程如下,可以看到主要内容就是让visible的子view去requestFocus.这个子view的requestFocus,又会进行焦点分发,直到某个view处理了焦点。

    protected boolean onRequestFocusInDescendants(int direction,
            Rect previouslyFocusedRect) {
        ...
        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;
    }

焦点请求

焦点分发最后,可能有个view拿到了焦点,他的View::requestFocus就会被调用。requestFocus()->requestFocusNoSearch->handleFocusGainInternal

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

handleFocusGainInternal代码如下,重点是L12,可以看到会调用mParent.requestChildFocus。requestChildFocus会往上一直调用此方法。

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

requestChildFocus的调用大概是这样,第二个参数就是获得焦点的那个view,第一个参数是this的子view,是第二个参数的祖先。

p.requestChildFocus(c,c)
p1.requestChildFocus(p,c)
....
ViewRootImpl.requestChildFocus(pN,c)

requestChildFocus的代码如下所示,可以看到这里主要做的就是mFocused的赋值,还有旧的mFocused的unFocus,以及调用上层的requestChildFocus

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

最终会调用到ViewRootImpl的requestChildFocus,如下所示,主要就是scheduleTraversals,这个会触发一次doTraversal->performTraversals。

    public void requestChildFocus(View child, View focused) {
        if (DEBUG_INPUT_RESIZE) {
            Log.v(TAG, "Request child focus: focus now " + focused);
        }
        checkThread();
        scheduleTraversals();
    }

onFocusChanged

整个过程中只有得到焦点的那个view会被回调onFocusChanged(true,,)。当然这里是首次获取焦点,如果焦点转移,那又不一样了,肯定会有2个onFocusChanged被调用。

流程总结

可以看到之前的,requestFocus流程可以分为以下二部。

//performTraversals:2023, ViewRootImpl (android.view)
            if (mView != null) {
                if (!mView.hasFocus()) {
                    mView.requestFocus(View.FOCUS_FORWARD);
                } 
            }

第一步,将focus分发到某个具体的view,触发这个view的View::requestFocus

DecorView.requestFocus(ViewGroup)->...->AL.requestFocus(ViewGroup)->BButton.requestFocus(View)

第二步,最终拿到focus的view触发 handleFocusGainInternal。mParent.requestChildFocus是递归朝上的,会导致各个ViewGroup的mFocused被设置。

handleFocusGainInternal->1、mParent.requestChildFocus
                         2、onFocusChange

ViewGroup内焦点分发

刚才提到descendantFocusability这个参数,这个参数决定了,子view和父view如何处理焦点,代码如下,简单易懂。

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

onRequestFocusInDescendants是子view来处理焦点,super.requestFocus是让父view自己处理焦点,这里注意区分View的requestFocus和ViewGroup的requestFocus。

FOCUS_BLOCK_DESCENDANTS只有父view可以处理焦点,子view资格被剥夺
FOCUS_BEFORE_DESCENDANTS父view先处理,如果没处理好,那交由子view
FOCUS_AFTER_DESCENDANTS子view先处理,如果处理不好交友父view

点击获取焦点

//View
    public boolean onTouchEvent(MotionEvent event) {
       。。。
        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        // take focus if we don't have it already and we should in
                        // touch mode.
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值