美文网首页
Android焦点流程梳理

Android焦点流程梳理

作者: 艾瑞败类 | 来源:发表于2023-05-14 13:38 被阅读0次

    作者:Cy13er

    前言

    最近在看一些焦点处理的问题,认真处理起来发现不跟着源码自己走一遍焦点相关的流程,对于问题的分析上会比较困难。所以本文主要对焦点流程进行一次梳理,在处理类似问题时也可以作为手册阅读。

    起源

    一切都要从ViewRootImpl收到输入事件开始

    // ViewRootImpl.ViewPostImeInputStage.java
    @Override
    protected int onProcess(QueuedInputEvent q) {
        if (q.mEvent instanceof KeyEvent) {
            return processKeyEvent(q); // KeyEvent会在此处处理
        } else {
            final int source = q.mEvent.getSource();
            if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
                return processPointerEvent(q);
            } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
                return processTrackballEvent(q);
            } else {
                return processGenericMotionEvent(q);
            }
        }
    }
    
    private int processKeyEvent(QueuedInputEvent q) {
        final KeyEvent event = (KeyEvent)q.mEvent;
    
        。。。
    
        // 1\. KeyEvent事件分发
        if (mView.dispatchKeyEvent(event)) {
            return FINISH_HANDLED;
        }
    
        。。。
    
        if (event.getAction() == KeyEvent.ACTION_DOWN) {
            if (groupNavigationDirection != 0) {
                if (performKeyboardGroupNavigation(groupNavigationDirection)) {
                    return FINISH_HANDLED;
                }
            } else {
                // 2\. ACTION_DOWN时,寻找下一个获取焦点的View
                if (performFocusNavigation(event)) {
                    return FINISH_HANDLED;
                }
            }
        }
        return FORWARD;
    }
    

    焦点流程主要分成两部分问题:

    • KeyEvent事件分发,寻找当前焦点需要消费事件的子View,并消费事件,见注释1处
    • 若事件没有消费,而且当前是KeyEvent.ACTION_DOWN时,需要寻找下一个焦点,见注释2处

    寻找下一个焦点

    // ViewRootImpl.ViewPostImeInputStage.java
    private boolean performFocusNavigation(KeyEvent event) {
        int direction = 0;
        switch (event.getKeyCode()) {
            case KeyEvent.KEYCODE_DPAD_LEFT:
                if (event.hasNoModifiers()) {
                    direction = View.FOCUS_LEFT;
                }
                break;
            case KeyEvent.KEYCODE_DPAD_RIGHT:
                if (event.hasNoModifiers()) {
                    direction = View.FOCUS_RIGHT;
                }
                break;
            case KeyEvent.KEYCODE_DPAD_UP:
                if (event.hasNoModifiers()) {
                    direction = View.FOCUS_UP;
                }
                break;
            case KeyEvent.KEYCODE_DPAD_DOWN:
                if (event.hasNoModifiers()) {
                    direction = View.FOCUS_DOWN;
                }
                break;
            case KeyEvent.KEYCODE_TAB:
                if (event.hasNoModifiers()) {
                    direction = View.FOCUS_FORWARD;
                } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
                    direction = View.FOCUS_BACKWARD;
                }
                break;
        }
        if (direction != 0) {
            // 1\. 获取目前拥有焦点的View
            View focused = mView.findFocus();
            if (focused != null) {
                // 2\. 查找到下一个获取焦点的View
                View v = focused.focusSearch(direction);
                if (v != null && v != focused) {
                    // do the math the get the interesting rect
                    // of previous focused into the coord system of
                    // newly focused view
                    focused.getFocusedRect(mTempRect);
                    if (mView instanceof ViewGroup) {
                        ((ViewGroup) mView).offsetDescendantRectToMyCoords(
                                focused, mTempRect);
                        ((ViewGroup) mView).offsetRectIntoDescendantCoords(
                                v, mTempRect);
                    }
                    // 3\. 让View获取焦点
                    if (v.requestFocus(direction, mTempRect)) {
                        boolean isFastScrolling = event.getRepeatCount() > 0;
                        playSoundEffect(
                                SoundEffectConstants.getConstantForFocusDirection(direction,
                                        isFastScrolling));
                        return true;
                    }
                }
    
                // Give the focused view a last chance to handle the dpad key.
                if (mView.dispatchUnhandledMove(focused, direction)) {
                    return true;
                }
            } else {
                if (mView.restoreDefaultFocus()) {
                    return true;
                }
            }
        }
        return false;
    }
    

    大概就是3步:

    1. 通过findFocus获取到当前拥有焦点的View
    2. 通过focusSearch获取到下一个获取焦点的View
    3. 让下一个获取焦点的View获取焦点

    大概流程

    这里大概流程如下

    按键事件分发

    dispatchKeyEvent

    KeyEvent事件分发与触摸事件有类似,都是从顶层的DecorView开始寻找消费事件的子View。不同的是,它没有过多复杂的机制,譬如事件拦截。

    ViewGroup#dispatchKeyEvent

    // ViewGroup.java
    @Override
    public boolean dispatchKeyEvent(KeyEvent event) {
        。。。
    
        // 1\. 如果父View自己拥有焦点,则判断自己是否消费事件
        if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS))
                == (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) {
            if (super.dispatchKeyEvent(event)) {
                return true;
            }
        // 2\. 如果该父View包含拥有焦点的子View,将事件分发到对应子View方向    
        } else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS)
                == PFLAG_HAS_BOUNDS) {
            if (mFocused.dispatchKeyEvent(event)) {
                return true;
            }
        }
    
        。。。
        return false;
    }
    

    ViewGroup#dispatchKeyEvent分两种情况:

    • 注释1,如果父View自己拥有焦点,则判断自己是否消费事件,实际是调用了父类View中定义的dispatchKeyEvent判断。
    • 注释2,如果父View当前包含了拥有焦点的子View,则将事件往拥有焦点的子View方向传递

    也就是说,KeyEvent事件分发只会往拥有焦点的子View方向传递。也就有以下两种情况:

    ps:红色部分是事件传递的方向。

    View#dispatchKeyEvent

    // View.java
    public boolean dispatchKeyEvent(KeyEvent event) {
        。。。
    
        // Give any attached key listener a first crack at the event.
        //noinspection SimplifiableIfStatement
        ListenerInfo li = mListenerInfo;
        // 1
        if (li != null && li.mOnKeyListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnKeyListener.onKey(this, event.getKeyCode(), event)) {
            return true;
        }
    
        // 2
        if (event.dispatch(this, mAttachInfo != null
                ? mAttachInfo.mKeyDispatchState : null, this)) {
            return true;
        }
    
        。。。
        return false;
    }
    

    View#dispatchKeyEvent就与dispatchTouchEvent类似了,第一步

    • 判断OnKeyListener是否为null
    • 判断OnKeyListener.onKey是否返回true
    • View是否是Enable状态

    三者成立则认为View消费事件,否则通过View#onKeyDown或者View#onKeyUp判断。事件的调用通过KeyEvent#dispatch,也就是注释2。

    ps:与Touch事件类似,DecorView会先将事件传递给Activity,然后经过PhoneWindow回传给DecorView进行事件分发

    获取当前拥有焦点的View

    findFocus

    findFocus的作用就是获取到当前拥有焦点的View,View的实现就是通过判断mPrivateFlags来确定自己是否拥有焦点。如果自己拥有焦点则返回自己。

    // View.java
    public View findFocus() {
        return (mPrivateFlags & PFLAG_FOCUSED) != 0 ? this : null;
    }
    

    ViewGroup的实现会在View的基础上扩展:

    • 如果自己拥有焦点则返回自己。
    • 如果自己包含了拥有焦点的子View,则往拥有焦点的子View方向寻找。
    // ViewGroup.java
    @Override
    public View findFocus() {
        if (DBG) {
            System.out.println("Find focus in " + this + ": flags="
                    + isFocused() + ", child=" + mFocused);
        }
    
        if (isFocused()) {
            return this;
        }
    
        if (mFocused != null) {
            return mFocused.findFocus();
        }
        return null;
    }
    

    也就是下图中,红色箭头表示findFocus的调用方向,红色区域表示当前拥有焦点的View

    下一个获取焦点的View

    focusSearch

    focusSearch寻找下一个获取焦点的View,它是以当前拥有焦点的View作为起点,往上传递。View的实现中,会调用父View的focusSearch,将自己也就是拥有焦点的View传递上去,同时携带当前需要移动的方向direction

    // View.java
    public View focusSearch(@FocusRealDirection int direction) {
        if (mParent != null) {
            return mParent.focusSearch(this, direction);
        } else {
            return null;
        }
    }
    

    ViewGroup中,会分为两种情况:

    • 没有到达根视图,那么与View相同直接传递上去。
    • 如果到达根视图,则会通过FocusFinder这一单例来寻找下一个获取焦点的View。ps:这里的根视图可以理解成是View树的最上层。
    // ViewGroup.java
    @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;
    }
    

    ps:FocusFinder.getInstance().findNextFocus(this, focused, direction)中的this的意思是以当前View作为起点寻找下一个获取焦点的View。

    方法的调用方向如下图红色部分:

    焦点搜索控制技巧

    可能会有这样一个需求:使用自定义RecyclerView做一个Tabbar效果时,不想在移动到最左或者最右时,焦点移出RecyclerView,那么可以从focusSearch入手,拦截它的搜索过程

    override fun focusSearch(focused: View?, direction: Int): View? {
        if (focused == null || layoutManager == null || adapter == null || adapter?.itemCount == 0)
            return super.focusSearch(focused, direction)
    
        val view = super.focusSearch(focused, direction)
    
        if (view != null) {
            //  findContainingItemView获取到该view所在的父View,如果不在RecyclerView内则返回null
            val nextFocusItemView = findContainingItemView(view)
            //  为null证明焦点已经移出了RecyclerView
            if (nextFocusItemView == null) {    
                //  左右限制移出RecyclerView
                if (direction == View.FOCUS_LEFT || direction == View.FOCUS_RIGHT) {
                    return focused
                }
            }
        }
    
        return view
    }
    

    这里如果是左右移动到边界,则直接返回当前拥有焦点的View,使焦点还在当前View上,不会被移动到别处。

    FocusFinder

    // FocusFinder.java
    private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) {
        View next = null;
        ViewGroup effectiveRoot = getEffectiveRoot(root, focused);
        if (focused != null) {
            // 1\. 获取开发者指定的下一个获取焦点的View
            next = findNextUserSpecifiedFocus(effectiveRoot, focused, direction);
        }
        if (next != null) {
            return next;
        }
        ArrayList<View> focusables = mTempList;
        try {
            focusables.clear();
            // 2\. 通过遍历子View,可以获取焦点的备选View放入focusables集合中
            effectiveRoot.addFocusables(focusables, direction);
            if (!focusables.isEmpty()) {
                // 3\. 通过可以获取焦点的备选view中获取到下一个获取焦点的View
                next = findNextFocus(effectiveRoot, focused, focusedRect, direction, focusables);
            }
        } finally {
            focusables.clear();
        }
        return next;
    }
    

    FocusFinder.getInstance().findNextFocus中主要做了以下3件事:

    • 尝试获取用户在xml中指定的下一个获取焦点的View
    • 通过遍历子View,将可以获取到下一个焦点的子View加入到备选集合focusables
    • 通过上述的备选集合focusables筛选出下一个获取焦点的View

    用户在XML指定下一个获取焦点的View

    用户可以在XML中指定不同方向下,基于该View的下一个获取焦点的View。

    android:nextFocusLeft="@id/xxx"
    android:nextFocusRight="@id/xxx"
    android:nextFocusDown="@id/xxx"
    android:nextFocusUp="@id/xxx"
    

    获取焦点获取备选集合

    如果用户未指定下一个获取焦点的View,则会通过备选集合的方式筛选出下一个获取焦点的View。首先需要获取到这个备选集合。

    • 先来看看View的实现:
        // View.java
        public void addFocusables(ArrayList<View> views, @FocusDirection int direction) {
            addFocusables(views, direction, isInTouchMode() ? FOCUSABLES_TOUCH_MODE : FOCUSABLES_ALL);
        }
    
        public void addFocusables(ArrayList<View> views, @FocusDirection int direction,
                @FocusableMode int focusableMode) {
            if (views == null) {
                return;
            }
            if (!canTakeFocus()) {
                return;
            }
            if ((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE
                    && !isFocusableInTouchMode()) {
                return;
            }
            views.add(this);
        }
    

    这里比较简单,如果符合获取焦点的资格,就会将自己添加到集合当中。ps:views即为获取焦点的备选集合。

    • 再来看看ViewGroup的实现:
    // ViewGroup.java
    @Override
    public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
        final int focusableCount = views.size();
    
        final int descendantFocusability = getDescendantFocusability();
        final boolean blockFocusForTouchscreen = shouldBlockFocusForTouchscreen();
        final boolean focusSelf = (isFocusableInTouchMode() || !blockFocusForTouchscreen);
    
        // 1\. 若descendantFocusability为FOCUS_BLOCK_DESCENDANTS,则会拦截子View获取焦点
        if (descendantFocusability == FOCUS_BLOCK_DESCENDANTS) {
            if (focusSelf) {
                super.addFocusables(views, direction, focusableMode);
            }
            return;
        }
    
        if (blockFocusForTouchscreen) {
            focusableMode |= FOCUSABLES_TOUCH_MODE;
        }
    
        // 2\. 若descendantFocusability为FOCUS_BEFORE_DESCENDANTS,则会优先将自己加入到集合
        if ((descendantFocusability == FOCUS_BEFORE_DESCENDANTS) && focusSelf) {
            super.addFocusables(views, direction, focusableMode);
        }
    
        int count = 0;
        final View[] children = new View[mChildrenCount];
        for (int i = 0; i < mChildrenCount; ++i) {
            View child = mChildren[i];
            // 3\. 只允许添加当前是Visible的子View
            if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
                children[count++] = child;
            }
        }
        FocusFinder.sort(children, 0, count, this, isLayoutRtl());
        for (int i = 0; i < count; ++i) {
            children[i].addFocusables(views, direction, focusableMode);
        }
    
        // 4\. 若descendantFocusability为FOCUS_AFTER_DESCENDANTS,
        // 则如果没有子View符合条件时将自己添加到集合
        if ((descendantFocusability == FOCUS_AFTER_DESCENDANTS) && focusSelf
                && focusableCount == views.size()) {
            super.addFocusables(views, direction, focusableMode);
        }
    }
    

    首先需要先科普一下ViewGroup获取焦点的3种策略

    • FOCUS_BEFORE_DESCENDANTS: 优先于子View获取焦点
    • FOCUS_AFTER_DESCENDANTS: 当子View都不获取焦点时,才获取焦点
    • FOCUS_BLOCK_DESCENDANTS: 禁止子View获取焦点

    同样可以通过XML设置,譬如:

    android:descendantFocusability="beforeDescendants"
    

    而这里就是根据不同的策略对集合的添加进行控制:

    • descendantFocusability = FOCUS_BLOCK_DESCENDANTS,则会拦截子View获取焦点,见注释1处
    • descendantFocusability = FOCUS_BEFORE_DESCENDANTS,则会优先将自己加入到集合,见注释2处
    • 遍历子View,并调用它的addFocusables,如果是ViewGroup的话就会起到往下递归目的,见注释3处
    • descendantFocusability = FOCUS_AFTER_DESCENDANTS的话,则如果没有子View符合条件时将自己添加到集合,见注释4处

    焦点记忆技巧

    如果需要做焦点记忆的需求,就可以考虑在addFocusables这里下手了,譬如在RecyclerView中记录当前选中的位置,在addFocusables只将该位置对应的View添加到集合,这样就能将焦点重新给到这个View。

    override fun addFocusables(views: ArrayList<View>?, direction: Int, focusableMode: Int) {
        // 根据选中位置,重新获取到View
        val view: View? = layoutManager?.findViewByPosition(currentSelectedPosition)
        if (hasFocus() || currentSelectedPosition < 0 || view == null) {
            super.addFocusables(views, direction, focusableMode)
        } else if (view.isFocusable && view.visibility == View.VISIBLE) {
            // 只将这个View添加到集合,并终止方法往下递归
            views?.add(view)
        } else {
            super.addFocusables(views, direction, focusableMode)
        }
    }
    

    寻找下一个焦点

    // FocusFinder.java
    private View findNextFocus(ViewGroup root, View focused, Rect focusedRect,
            int direction, ArrayList<View> focusables) {
        if (focused != null) {
            if (focusedRect == null) {
                focusedRect = mFocusedRect;
            }
    
            // 1\. 获取到focused所在区域,并将其换算成与根视图相同的坐标系
            focused.getFocusedRect(focusedRect);
            root.offsetDescendantRectToMyCoords(focused, focusedRect);
        } else {
            if (focusedRect == null) {
                focusedRect = mFocusedRect;
                // make up a rect at top left or bottom right of root
                switch (direction) {
                    case View.FOCUS_RIGHT:
                    case View.FOCUS_DOWN:
                        setFocusTopLeft(root, focusedRect);
                        break;
                    case View.FOCUS_FORWARD:
                        if (root.isLayoutRtl()) {
                            setFocusBottomRight(root, focusedRect);
                        } else {
                            setFocusTopLeft(root, focusedRect);
                        }
                        break;
    
                    case View.FOCUS_LEFT:
                    case View.FOCUS_UP:
                        setFocusBottomRight(root, focusedRect);
                        break;
                    case View.FOCUS_BACKWARD:
                        if (root.isLayoutRtl()) {
                            setFocusTopLeft(root, focusedRect);
                        } else {
                            setFocusBottomRight(root, focusedRect);
                        break;
                    }
                }
            }
        }
    
        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:
                // 2\. 根据focused所在区域获取到下一个获取焦点的View
                return findNextFocusInAbsoluteDirection(focusables, root, focused,
                        focusedRect, direction);
            default:
                throw new IllegalArgumentException("Unknown direction: " + direction);
        }
    }
    

    焦点寻找是就近原则,默认会根据对比两块Rect区域的位置,来确定焦点位置。所以findNextFocus做了两件事:

    • 求出当前拥有焦点的View即focused的Rect区域,并将它转换成与根视图root相同的坐标系。ps:这样是为了后续比较时能在同一坐标系下比较。
    • 根据focused所在区域获取到下一个获取焦点的View
    // FocusFinder.java
    View findNextFocusInAbsoluteDirection(ArrayList<View> focusables, ViewGroup root, View focused,
            Rect focusedRect, int direction) {
        // 1\. 最佳候选View所在的区域,初始值为focused的区域
        mBestCandidateRect.set(focusedRect);
        switch(direction) {
            case View.FOCUS_LEFT:
                mBestCandidateRect.offset(focusedRect.width() + 1, 0);
                break;
            case View.FOCUS_RIGHT:
                mBestCandidateRect.offset(-(focusedRect.width() + 1), 0);
                break;
            case View.FOCUS_UP:
                mBestCandidateRect.offset(0, focusedRect.height() + 1);
                break;
            case View.FOCUS_DOWN:
                mBestCandidateRect.offset(0, -(focusedRect.height() + 1));
        }
    
        View closest = null;
    
        // 2\. 遍历候选集合,筛选出最佳候选区域
        int numFocusables = focusables.size();
        for (int i = 0; i < numFocusables; i++) {
            View focusable = focusables.get(i);
    
            // only interested in other non-root views
            if (focusable == focused || focusable == root) continue;
    
            // 3\. 获取候选的View所在区域,并将它转换为与root相同坐标系
            focusable.getFocusedRect(mOtherRect);
            root.offsetDescendantRectToMyCoords(focusable, mOtherRect);
            // 4\. 对比两个区域,如果可行,就更新最佳候选View所在的区域
            if (isBetterCandidate(direction, focusedRect, mOtherRect, mBestCandidateRect)) {
                mBestCandidateRect.set(mOtherRect);
                closest = focusable;
            }
        }
        return closest;
    }
    

    mBestCandidateRect最佳候选View的区域,这里大概分几步:

    • mBestCandidateRect初始化为当前拥有焦点的区域focusedRect
    • 通过遍历候选集合,筛选出最佳候选区域,与其对应的View对象
    • 在对比前需要将候选的View所在区域转换成与root相同坐标系
    • 对比区域可行后,会更新mBestCandidateRect,以及对应的View对象,整个过程类似求最大值的过程。

    至此,下一个获取焦点的View就得出来了。

    获取焦点

    requestFocus

    View#requestFocus

    View获取焦点的流程

    // View.java
    public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
        return requestFocusNoSearch(direction, previouslyFocusedRect);
    }
    
    private boolean requestFocusNoSearch(int direction, Rect previouslyFocusedRect) {
        // need to be focusable
        if (!canTakeFocus()) {
            return false;
        }
    
        // 在TouchMode下需要声明focusableInTouchMode为true才能获取焦点
        if (isInTouchMode() &&
            (FOCUSABLE_IN_TOUCH_MODE != (mViewFlags & FOCUSABLE_IN_TOUCH_MODE))) {
               return false;
        }
    
        // 如果存在descendantFocusability为FOCUS_BLOCK_DESCENDANTS的父View则不能获取焦点
        if (hasAncestorThatBlocksDescendantFocus()) {
            return false;
        }
    
        if (!isLayoutValid()) {
            mPrivateFlags |= PFLAG_WANTS_FOCUS;
        } else {
            clearParentsWantFocus();
        }
    
        // 获取焦点
        handleFocusGainInternal(direction, previouslyFocusedRect);
        return true;
    }
    
    void handleFocusGainInternal(@FocusRealDirection int direction, Rect previouslyFocusedRect) {
        if (DBG) {
            System.out.println(this + " requestFocus()");
        }
    
        if ((mPrivateFlags & PFLAG_FOCUSED) == 0) {
            mPrivateFlags |= PFLAG_FOCUSED;  // 1\. 标记为获取了焦点
    
            View oldFocus = (mAttachInfo != null) ? getRootView().findFocus() : null;
    
            if (mParent != null) {
                // 2\. 通知自己的父View自己获取了焦点
                mParent.requestChildFocus(this, this);
                updateFocusedInCluster(oldFocus, direction);
            }
    
            if (mAttachInfo != null) {
                // 3\. 回调viewTreeObserver的OnGlobalFocusChange
                mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, this);
            }
    
            // 4\. 如果有注册OnFocusChangeListener,会有回调
            onFocusChanged(true, direction, previouslyFocusedRect);
            refreshDrawableState();
        }
    }
    

    View#requestFocus最后会调用到handleFocusGainInternal方法

    • 更新mPrivateFlags,标记为自己已经获取了焦点
    • 调用ViewGroup#requestChildFocus,通知自己的父View自己获取了焦点
    • 回调相关监听
      • viewTreeObserver的OnGlobalFocusChange
      • OnFocusChangeListener.onFocusChange

    ViewGroup#requestChildFocus

    // ViewGroup.java
    @Override
    public void requestChildFocus(View child, View focused) {
        if (DBG) {
            System.out.println(this + " requestChildFocus()");
        }
        // 如果descendantFocusability为FOCUS_BLOCK_DESCENDANTS则直接返回
        if (getDescendantFocusability() == FOCUS_BLOCK_DESCENDANTS) {
            return;
        }
    
        // Unfocus us, if necessary
        super.unFocus(focused);
    
        // 1\. 清空旧的焦点
        if (mFocused != child) {
            if (mFocused != null) {
                mFocused.unFocus(focused);
            }
    
            mFocused = child;
        }
        // 2\. 继续往父View方向传递
        if (mParent != null) {
            mParent.requestChildFocus(this, focused);
        }
    }
    

    ViewGroup#requestChildFocus主要做了两件事:

    • 清空旧的焦点状态
    • 继续调用父View的requestChildFocus,目的是继续更新父View的记录状态

    ViewGroup#requestFocus

    ViewGroup会对requestFocus进行重写

    // ViewGroup.java
    @Override
    public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
        if (DBG) {
            System.out.println(this + " ViewGroup.requestFocus direction="
                    + direction);
        }
        int descendantFocusability = getDescendantFocusability();
    
        boolean result;
        switch (descendantFocusability) {
            // 1\. 不允许子View获取焦点
            case FOCUS_BLOCK_DESCENDANTS:
                result = super.requestFocus(direction, previouslyFocusedRect);
                break;
            // 2\. 自己优先于子View获取焦点    
            case FOCUS_BEFORE_DESCENDANTS: {
                final boolean took = super.requestFocus(direction, previouslyFocusedRect);
                result = took ? took : onRequestFocusInDescendants(direction,
                        previouslyFocusedRect);
                break;
            }
            // 3\. 子View优先于自己获取焦点
            case FOCUS_AFTER_DESCENDANTS: {
                final boolean took = onRequestFocusInDescendants(direction, previouslyFocusedRect);
                result = took ? took : super.requestFocus(direction, previouslyFocusedRect);
                break;
            }
            default:
                throw new IllegalStateException("descendant focusability must be "
            + "one of FOCUS_BEFORE_DESCENDANTS, FOCUS_AFTER_DESCENDANTS, FOCUS_BLOCK_DESCENDANTS "
            + "but is " + descendantFocusability);
        }
        if (result && !isLayoutValid() && ((mPrivateFlags & PFLAG_WANTS_FOCUS) == 0)) {
            mPrivateFlags |= PFLAG_WANTS_FOCUS;
        }
        return result;
    }
    
    // 4\. 寻找可以获取焦点的子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获取焦点,见注释4处

    这里主要是扩展了获取焦点策略的能力,也是根据定义

    • FOCUS_BLOCK_DESCENDANTS不允许子View获取焦点,所以这里直接令自己去获取焦点,见注释1处
    • FOCUS_BEFORE_DESCENDANTS:自己优先于子View获取焦点,所以这里会先自己调用super.requestFocus,再通过结果调用onRequestFocusInDescendants,见注释2处
    • FOCUS_AFTER_DESCENDANTS子View优先于自己获取焦点,所以会先通过onRequestFocusInDescendants返回的结果,再视情况令自己获得焦点,见注释3处

    开发中,可以重写onRequestFocusInDescendants以此来控制您想希望获取焦点的子View获取焦点

    默认获取焦点

    回到刚开始的流程,如果目前还没有找到拥有焦点的View,会怎样呢?

    // ViewRootImpl.ViewPostImeInputStage.java#performFocusNavigation
    View focused = mView.findFocus();
    if (focused != null) {
        。。。
    } else {
        if (mView.restoreDefaultFocus()) {
            return true;
        }
    }
    
    // View.java
    public boolean restoreDefaultFocus() {
        return requestFocus(View.FOCUS_DOWN);
    }
    

    如果找不到当前拥有焦点的View,则会调用View#restoreDefaultFocus,实际上就是requestFocus,一般其实是ViewGroup#requestFocus,因为mView是DecorView。那这样就有机会通过onRequestFocusInDescendants令子View获取到焦点。

    最后

    本文主要对焦点流程进行了梳理,包括焦点的产生与搜索过程,事件分发的过程。

    相关文章

      网友评论

          本文标题:Android焦点流程梳理

          本文链接:https://www.haomeiwen.com/subject/egltsdtx.html