Based on the Android11

An overview of the

The focus mechanism of the Android View is not that important for touchscreen devices, but for TV devices, the focus mechanism of the View is essential, so it is a must have skill for TV developers.

This period of time the fire at home and abroad net red plum seven out of a lot of “life series”, what pumpkin life, grape life, garlic life…..

This article takes a page out of the same book and discusses the life of Android View Focus.

The story begins with long long ago…… No, the story begins when you turn on your Android TV, hold the remote control in hand and press your sacred button.

Press a down button

Before discussing this, let’s take a look at the input devices that Android supports. In addition to our usual touch screens, Android supports keyboards, mice, gamepads, remote controls, and more. In Linux, there is a saying that everything is a file, and external devices are also files on Linux. They are usually created in the /dev/directory, and input devices are created in the /dev/input directory:

chengfangpeng@ubuntu:/dev/input$ ls
by-id    event0  event10  event12  event14  event16  event2  event4  event6  event8  mice
by-path  event1  event11  event13  event15  event17  event3  event5  event7  event9  mouse0
Copy the code

Above is the /dev/input directory on a Linux desktop.

Android is based on Linux, so device management is done by the Linux kernel. Here is the /dev/input directory for an Android Tv device

marconi:/dev/input # ls
event0 event1 event2 event3 event4 mice mouse0
Copy the code

When the device is available, the Linux kernel creates a node file named event0-n or another in the /dev/input directory, and deletes the node when the device is unavailable. When we operate an infrared remote control and press the down button, the Tv device receives an infrared signal and triggers a hardware interrupt. The Linux kernel receives the hardware interrupt accordingly, and then processes the interrupt into raw input event data and writes it to the corresponding device node. User space can read this event through the read() function.

The Android input system is constantly listening to these input nodes. When it finds that a node has input, it reads out the node’s events and distributes them to the corresponding recipients.

Read the /dev/input event

If we want to see what these events look like in the /dev/input node, can we use the getevent tool

chengfangpeng@ubuntu:~$adb root chengfangpeng@ubuntu:~$ADB shell getevent -lt /dev/input_event0 [3378.901913] EV_KEY KEY_DOWN DOWN [3378.901913] EV_SYN SYN_REPORT 00000000 [3379.104075] EV_KEY KEY_DOWN UP [3379.104075] EV_SYN SYN_REPORT 00000000Copy the code

I can listen for input events under /dev/input_event0. When I press the down button on the remote control, I can listen for input events. The event type is KEY_DOWN. The values are DOWN and UP. There’s a SY_REPORT. What does it do? EV_SYN is a special event type that is used to split multiple input data generated at the same time into multiple packets. We’ve seen the data read from /dev/input/event0, but what does the real Android do? This involves the Android input system.

Android Input System

The content of input system is more, here do not do a detailed introduction, the logic of this piece will be specially introduced later, first dig a pit (do not know when to fill), borrow a << in-depth understanding of Android volume 3>> a picture, very clear.

! [image-20201015154906543](/home/chengfangpeng/Nutstore Files/Nutstore/res/image-20201015154906543.png)

We input the event, like the monkey in the sky, to our current Window, in addition to the input system, and added WMS logic, continue to digging (later fill…) By default, the event we send has found the current Window, or the current Activity. If you’re not familiar with the concepts of Activity, Window, PhoneWIndow, WindowMnager, WindowManagerService, ViewRootImpl, or DecorView, Keep digging (there will be an Activity life article later). However, some of the above concepts cannot be avoided when introducing the focus mechanism of a View. If you have any questions, please find the corresponding article or source code.

ViewRootImpl event distribution

As mentioned in the above section, the event sent by the remote control is passed to the Window, so where is the event passed to the Window? The following is the stack information of the crash triggered by clicking the down button, which can clearly obtain the path of the event passing, and also verifies the above conclusion.

2020-10-15 16:52:59.634 1378-1378/com.xray.focus.sample E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.xray.focus.sample, PID: 1378
    java.lang.RuntimeException: dispatchKeyEvent crash
        at com.xray.focus.sample.widget.TraceKevEventView.dispatchKeyEvent(TraceKevEventView.java:25)
        at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1912)
        at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1912)
        at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1912)
        at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:1912)
        at com.android.internal.policy.DecorView.superDispatchKeyEvent(DecorView.java:436)
        at com.android.internal.policy.PhoneWindow.superDispatchKeyEvent(PhoneWindow.java:1830)
        at android.app.Activity.dispatchKeyEvent(Activity.java:3827)
        at com.android.internal.policy.DecorView.dispatchKeyEvent(DecorView.java:350)
        at android.view.ViewRootImpl$ViewPostImeInputStage.processKeyEvent(ViewRootImpl.java:5337)
        at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:5205)
        at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4726)
        at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4779)
        at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4745)
        at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:4885)
        at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4753)
        at android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:4942)
        at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4726)
        at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4779)
        at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4745)
        at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4753)
        at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4726)
        at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4779)
        at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4745)
        at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:4918)
        at android.view.ViewRootImpl$ImeInputStage.onFinishedInputEvent(ViewRootImpl.java:5079)
        at android.view.inputmethod.InputMethodManager$PendingEvent.run(InputMethodManager.java:2844)
        at android.view.inputmethod.InputMethodManager.invokeFinishedInputEventCallback(InputMethodManager.java:2427)
        at android.view.inputmethod.InputMethodManager.finishedInputEvent(InputMethodManager.java:2418)
        at android.view.inputmethod.InputMethodManager$ImeInputEventSender.onInputEventFinished(InputMethodManager.java:2821)
        at android.view.InputEventSender.dispatchInputEventFinished(InputEventSender.java:143)
        at android.os.MessageQueue.nativePollOnce(Native Method)
        at android.os.MessageQueue.next(MessageQueue.java:327)
        at android.os.Looper.loop(Looper.java:169)
        at android.app.ActivityThread.main(ActivityThread.java:7021)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:486)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:872)
Copy the code

Since ViewRootImpl

ViewRootImpl is the top layer of the View tree. In an Activity, the DecorView is the top View, but its mParent is ViewRootImpl, so it’s technically the top layer of the View tree. Remote control events are distributed to the onProcess method of the ViewPostImeInputStage.

# ViewRootImpl. Java/** * Delivers post-ime input events to the view hierarchy. */
    final class ViewPostImeInputStage extends InputStage {
        public ViewPostImeInputStage(InputStage next) {
            super(next);
        }

        @Override
        protected int onProcess(QueuedInputEvent q) {
            if (q.mEvent instanceof KeyEvent) {
                // Perform event distribution
                return processKeyEvent(q);
            } 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 {
                    returnprocessGenericMotionEvent(q); }}}... }Copy the code

Execute event distribution

 private int processKeyEvent(QueuedInputEvent q) {
            finalKeyEvent event = (KeyEvent)q.mEvent; .// Handle automatic focus changes.
            if (event.getAction() == KeyEvent.ACTION_DOWN) {
                if(groupNavigationDirection ! =0) {
                    if (performKeyboardGroupNavigation(groupNavigationDirection)) {
                        returnFINISH_HANDLED; }}else {
                    if (performFocusNavigation(event)) {// Handle the focus shift
                        returnFINISH_HANDLED; }}}return FORWARD;
        }
Copy the code

Handle focus movement

 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;
            }
            if(direction ! =0) {
                // This is the DecorView, which is the lowest View in the Viwe tree.
                // Then findFoucs is used to find the View in the tree that has the focus.
                View focused = mView.findFocus();
                if(focused ! =null) {// Focus is not empty, then look for the next View to focus on
                    View v = focused.focusSearch(direction);
                    if(v ! =null&& v ! = focused) {// The next View to get focus is found
                        // 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);
                        }
                        if (v.requestFocus(direction, mTempRect)) {// Let the View that is going to get the focus actually get the focus, or maybe not
                            playSoundEffect(SoundEffectConstants
                                    .getContantForFocusDirection(direction));// Execute the sound after getting the focus
                            return true; }}// Failed to find the focus to give the view one last chance to consume the button event
                    if (mView.dispatchUnhandledMove(focused, direction)) {
                        return true; }}else {
                    // If the current interface does not have focus here, set a default focus
                    if (mView.restoreDefaultFocus()) {
                        return true; }}}return false;
        }
Copy the code

Find the View that currently has focus from the View tree

The findFocus method is implemented differently in View and ViewGroup. This lookup is very similar to how findViewById is implemented. If you look at the implementation of the View, what you’re doing is you’re determining whether PFLAG_FOCUSED is set in mPrivateFlags, and you’re inadvertently saying what the focus is. The focus of the View is PFLAG_FOCUSED.

# view.java public View findFocus() {return (mPrivateFlags & PFLAG_FOCUSED)! = 0? this : null; }Copy the code

Implementation in ViewGroup:

#ViewGroup.java
public View findFocus(a) {
        if (DBG) {
            System.out.println("Find focus in " + this + ": flags="
                    + isFocused() + ", child=" + mFocused);
        }

        if (isFocused()) {
            return this;
        }

        if(mFocused ! =null) {// The mFocused member variable indicates that it has a focus or that it contains a focused View
            return mFocused.findFocus();// Keep searching
        }
        return null;
    }
Copy the code

Find the next View that will get focus

Going back to the performFocusNavigation method of the ViewRootImpl above, we find the View that currently has focus. Next, look for the next View that will be focused using a focused approach. This is done using a method called focusSearch, which is implemented in both View and ViewGroup.

Let’s look at the implementation in the View first. The logic is simple, if you have a parent, you keep looking up.

#View.java
/**
     * Find the nearest view in the specified direction that can take focus.
     * This does not actually give focus to that view.
     *
     * @param direction One of FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, and FOCUS_RIGHT
     *
     * @return The nearest focusable in the specified direction, or null if none
     *         can be found.
     */
    public View focusSearch(@FocusRealDirection int direction) {
        if (mParent != null) {
            return mParent.focusSearch(this, direction);
        } else {
            return null;
        }
    }
Copy the code

Our layouts usually have more than one layer, and mParent is a ViewGroup, so let’s look at the implementation of focusSearch in a ViewGroup.

#ViewGroup.java
/**
     * Find the nearest view in the specified direction that wants to take
     * focus.
     *
     * @param focused The view that currently has focus
     * @param direction One of FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, and
     *        FOCUS_RIGHT, or 0 for not applicable.
     */
    @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;
    }
Copy the code

Check whether the current ViewGroup is at the top of the View tree. If not, continue to look up. If yes, perform the following logic:

return FocusFinder.getInstance().findNextFocus(this, focused, direction);
Copy the code

FocusFinder is a utility class for finding FoucsViews. Now take a look at the findNextFocus method

#FocusFinder.java private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) { View next = null; ViewGroup effectiveRoot = getEffectiveRoot(root, focused); if (focused ! = null) {// If the current focus is not null, look in the view specified by the user, Focused set nextFoucs * * * Id attribute next = findNextUserSpecifiedFocus (effectiveRoot, focused, direction); } if (next ! = null) { return next; } ArrayList<View> focusables = mTempList; try { focusables.clear(); effectiveRoot.addFocusables(focusables, direction); // Put an alternative view that can get focus into the focusables set. Focusables.isempty ()) {// If the set of candidate views is not empty, EffectiveRoot, focusedRect, direction, focusables); } } finally { focusables.clear(); } return next; }Copy the code

First, if focused is not empty, specify the nextFocus***Id from the user to get the next focused View. Usually we can specify the ID of the View that gets the focus in XML, as follows:

  	   android:nextFocusLeft=""
       android:nextFocusRight=""
       android:nextFocusDown=""
       android:nextFocusUp=""
Copy the code

FindNextUserSpecifiedFocus is find next from the set of these ids for focal point of View.

#FocusFinder.java 
private View findNextUserSpecifiedFocus(ViewGroup root, View focused, int direction) {
        // check for user specified next focus
        View userSetNextFocus = focused.findUserSetNextFocus(root, direction);// Look for the next View specified by the user to get focus from the current View
        View cycleCheck = userSetNextFocus;
        boolean cycleStep = true; // we want the first toggle to yield false
        while(userSetNextFocus ! =null) {
            if(userSetNextFocus.isFocusable() && userSetNextFocus.getVisibility() == View.VISIBLE && (! userSetNextFocus.isInTouchMode() || userSetNextFocus.isFocusableInTouchMode())) {return userSetNextFocus;
            }
            userSetNextFocus = userSetNextFocus.findUserSetNextFocus(root, direction);
            if(cycleStep = ! cycleStep) { cycleCheck = cycleCheck.findUserSetNextFocus(root, direction);if (cycleCheck == userSetNextFocus) {
                    // found a cycle, user-specified focus forms a loop and none of the views
                    // are currently focusable.
                    break; }}}return null;
    }
Copy the code
#View.java
/**
     * If a user manually specified the next view id for a particular direction,
     * use the root to look up the view.
     * @param root The root view of the hierarchy containing this view.
     * @param direction One of FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT, FOCUS_FORWARD,
     * or FOCUS_BACKWARD.
     * @return The user specified next view, or null if there is none.
     */
    View findUserSetNextFocus(View root, @FocusDirection int direction) {
        switch (direction) {
            case FOCUS_LEFT:
                if (mNextFocusLeftId == View.NO_ID) return null;
                return findViewInsideOutShouldExist(root, mNextFocusLeftId);
            case FOCUS_RIGHT:
                if (mNextFocusRightId == View.NO_ID) return null;
                return findViewInsideOutShouldExist(root, mNextFocusRightId);
            case FOCUS_UP:
                if (mNextFocusUpId == View.NO_ID) return null;
                return findViewInsideOutShouldExist(root, mNextFocusUpId);
            case FOCUS_DOWN:
                if (mNextFocusDownId == View.NO_ID) return null;
                return findViewInsideOutShouldExist(root, mNextFocusDownId);
            case FOCUS_FORWARD:
                if (mNextFocusForwardId == View.NO_ID) return null;
                return findViewInsideOutShouldExist(root, mNextFocusForwardId);
            case FOCUS_BACKWARD: {
                if (mID == View.NO_ID) return null;
                final View rootView = root;
                final View startView = this;
                // Since we have forward links but no backward links, we need to find the view that
                // forward links to this view. We can't just find the view with the specified ID
                // because view IDs need not be unique throughout the tree.
                returnroot.findViewByPredicateInsideOut(startView, t -> findViewInsideOutShouldExist(rootView, t, t.mNextFocusForwardId) == startView); }}return null;
    }
Copy the code

If the user does not specify the next View to get focus, the next step is to follow Android’s internal focusing algorithm. First, the candidate View that can get focus is placed into a View list focusables. The addFocusables method is used to do this. It’s going to go through the whole View tree from top to bottom as well.

    effectiveRoot.addFocusables(focusables, direction);
Copy the code

So addFocusables can also be divided into View and ViewGroup. Look at the logic of the View first. If the View qualifies for focus, then add it to the views. First look at the implementation in the View:

# View.java
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);// Add yourself to views if you qualify for focus
    }
Copy the code

In ViewGroup the logic is a little more complicated.

#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 booleanfocusSelf = (isFocusableInTouchMode() || ! blockFocusForTouchscreen);if (descendantFocusability == FOCUS_BLOCK_DESCENDANTS) {// When descendantFocusability is FOCUS_BLOCK_DESCENDANTS,
            // Disable your child View from getting focus
            if (focusSelf) {
                super.addFocusables(views, direction, focusableMode);// Verify that you can be added to the views, and if so, add it
            }
            return;
        }

        if (blockFocusForTouchscreen) {
            focusableMode |= FOCUSABLES_TOUCH_MODE;
        }

        if ((descendantFocusability == FOCUS_BEFORE_DESCENDANTS) && focusSelf) {/ / when descendantFocusability
            // Obtain focus for FOCUS_BEFORE_DESCENDANTS priority subview
            super.addFocusables(views, direction, focusableMode);// Verify that you can be added to the views, and if so, add it
        }

        int count = 0;
        final View[] children = new View[mChildrenCount];
        for (int i = 0; i < mChildrenCount; ++i) {
            View child = mChildren[i];
            if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
                children[count++] = child;
            }
        }
        FocusFinder.sort(children, 0, count, this, isLayoutRtl());
        for (int i = 0; i < count; ++i) {// Add your own subviews to the views
            children[i].addFocusables(views, direction, focusableMode);
        }

        // When set to FOCUS_AFTER_DESCENDANTS, we only add ourselves if
        // there aren't any focusable descendants. this is
        // to avoid the focus search finding layouts when a more precise search
        // among the focusable children would be more interesting.
        if ((descendantFocusability == FOCUS_AFTER_DESCENDANTS) && focusSelf
                && focusableCount == views.size()) {
            super.addFocusables(views, direction, focusableMode); }}Copy the code

So there are three strategies for ViewGroup to get focus,

  • FOCUS_BEFORE_DESCENDANTS: get focus first before subviews.
  • FOCUS_AFTER_DESCENDANTS: get focus when none of the child views get focus
  • FOCUS_BLOCK_DESCENDANTS: prohibits subviews from obtaining focus

In ViewGroup, the default descendantFocusability is FOCUS_BEFORE_DESCENDANTS:

 private void initViewGroup(a) {... setDescendantFocusability(FOCUS_BEFORE_DESCENDANTS); . }Copy the code

The above is the process of addFocusables. This method is very useful. By copying it, the size of focusables can be simplified, so as to improve the efficiency of focusing. The onAddFocusables method of GridLayoutManager class in Tv development Leanback framework is the addFocusables method of RecyclerView.

The next list of candidates for focus has been found, but which one should we choose? Continuing with our focusing logic, findNextFocus also has a namesake reuse method that will find the best candidate from the list, or it may not.

# FoucsFinder.java
private View findNextFocus(ViewGroup root, View focused, Rect focusedRect,
            int direction, ArrayList<View> focusables) {
        if(focused ! =null) {
            if (focusedRect == null) {
                focusedRect = mFocusedRect;
            }
            // fill in interesting rect from focused
            focused.getFocusedRect(focusedRect);// Get the area where the focus is located
            root.offsetDescendantRectToMyCoords(focused, focusedRect);// Unify coordinates
        } 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);FOCUS_RIGHT and FOCUS_DOWN are FOCUS_RIGHT if FOCUS_DOWN is empty
                        FocusedRect is a point
                        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);/ / same as above
                        break;
                    case View.FOCUS_BACKWARD:
                        if (root.isLayoutRtl()) {
                            setFocusTopLeft(root, focusedRect);
                        } else {
                            setFocusBottomRight(root, focusedRect);
                        break; }}}}// The logic above is to calculate the area occupied by focused
        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:
                // Look for the next algorithm to get the focus view
                return findNextFocusInAbsoluteDirection(focusables, root, focused,
                        focusedRect, direction);
            default:
                throw new IllegalArgumentException("Unknown direction: "+ direction); }}Copy the code

The logic is to calculate the area occupied by focusedRect, a Rect, and then compare that focusedRect to the Rect of all the candidates to find the ‘closest’ one. To FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT as an example, the algorithm realization in findNextFocusInAbsoluteDirection approach, is getting closer and closer to the truth, isn’t it frozen chicken!!!!!!

# FoucsFinder.java
View findNextFocusInAbsoluteDirection(ArrayList<View> focusables, ViewGroup root, View focused,
            Rect focusedRect, int direction) {
        // initialize the best candidate to something impossible
        // (so the first plausible view will become the best choice)
        mBestCandidateRect.set(focusedRect);//mBestCandidateRect is the area where the best candidate View is located and initialized as 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;

        int numFocusables = focusables.size();
        for (int i = 0; i < numFocusables; i++) {// Iterate through the entire focusables, looking for the next most consistent View to get focus
            View focusable = focusables.get(i);

            // only interested in other non-root views
            if (focusable == focused || focusable == root) continue;

            // get focus bounds of other view in same coordinate system
            focusable.getFocusedRect(mOtherRect);// Save the focusable location information in mOtherRect
            root.offsetDescendantRectToMyCoords(focusable, mOtherRect);// Unify coordinates

            if (isBetterCandidate(direction, focusedRect, mOtherRect, mBestCandidateRect)) {
                // Compare mOtherRect and mBestCandidateRect, if mOtherRect is closer to focusedRect than mBestCandidateRect
                // Assign mOtherRect to mBestCandidateRectmBestCandidateRect.set(mOtherRect); closest = focusable; }}// When the loop is finished, closest means the next best View to get the focus
        return closest;
    }
Copy the code

To illustrate the logic above: Given a variable mBestCandidateRect, which stores the current “closest” optimal solution, compare it with mBestCandidateRect by traversing the focusables. If there is a better solution than mBestCandidateRect, Then replace the mBestCandidateRect until the traversal is complete, and the last mBestCandidateRect is the optimal solution we find. The process is similar to selection sorting. So there’s one last question, which one of these two rects wins? Which is closer?

Compare the area of two views which is the best candidate

# FoucsFinder. Java/**
     * Is rect1 a better candidate than rect2 for a focus search in a particular
     * direction from a source rect?  This is the core routine that determines
     * the order of focus searching.
     * @param direction the direction (up, down, left, right)
     * @param source The source we are searching from
     * @param rect1 The candidate rectangle
     * @param rect2 The current best candidate.
     * @return Whether the candidate is the new best.
     */
    boolean isBetterCandidate(int direction, Rect source, Rect rect1, Rect rect2) {

        // to be a better candidate, need to at least be a candidate in the first
        // place :)
        if(! isCandidate(source, rect1, direction)) {// The candidate view should be in the specified direction
            return false;
        }

        // we know that rect1 is a candidate.. if rect2 is not a candidate,
        // rect1 is better
        if(! isCandidate(source, rect2, direction)) {return true;
        }

        // if rect1 is better by beam, it wins
        if (beamBeats(direction, source, rect1, rect2)) {// Determine which of the two candidates is in the beam and which is the winner. We'll talk about it later, graphically
            return true;
        }

        // if rect2 is better, then rect1 cant' be :)
        if (beamBeats(direction, source, rect2, rect1)) {
            return false;
        }

        // otherwise, do fudge-tastic comparison of the major and minor axis
        return (getWeightedDistanceFor(// The weight distance is related to the direction
                        majorAxisDistance(direction, source, rect1),
                        minorAxisDistance(direction, source, rect1))
                < getWeightedDistanceFor(
                        majorAxisDistance(direction, source, rect2),
                        minorAxisDistance(direction, source, rect2)));
    }
Copy the code

Main code is posted, but if you don’t have a lot of patience, figuring out the few paragraphs above algorithm code or more difficult (posted) so some branch code is not, I am now directly conclusions are given, and then you can see the code to verify these conclusions, of course I will give a demo, convenient for you to verify these conclusions, whether very close. The above code concludes with three algorithmic rules. A picture is worth a thousand words. I will explain these three algorithmic rules with three images:

  • Rule Number one: isCandidate

    First, compare the two rects passed in. Which one is in the candidate region? Then what is the candidate region? The candidate area should be located in the direction of focus, that is, in the direction of direction. For example, if your direction is FOCUS_LEFT, the candidate area must be to the left of the Focused area, as shown below: A is in the candidate area, but B is not.

  • Rule number two :beamBeats

    Take FOCUS_LEFT as an example. As shown in the figure below, if A and BEAM overlap but B does not, A wins in the competition between AB.

  • Rule number three :getWeightedDistanceFor

    What if in beamBeats, neither A nor B overlaps with BEAM, or both overlap with BEAM? This leads to the third rule, compare the weight distance between AB and Focused. What is the weight distance? Again, take the direction as FOCUS_LEFT. Why do we emphasize this direction? Because it will affect the calculation of the weight distance

    A: 13 * dx2^2 + dy2^2; B: 13 * dx1^2 + dy2^2. FOCUS_DOWN increases the weight of dy.

 

Now that focusSearch logic has run its course, there are two outcomes. Either we find the next View to get focus, or we don’t. If not, let the View handle how the Event is consumed, or not consumed. And if you find it? Back to ViewRootImpl performFocusNavigation method, is used to find the View to requestFocus next. And what it does is it notifys your sister-in-law that I’ve got the focus, which is the whole View tree, and when I get the focus, I update my status, because how else would the user know that you’ve got the focus, right. Ok, so to get to the code, let’s look at the requestFocus method of the View, which of course is copied by the ViewGroup.

public boolean requestFocus(int direction, Rect previouslyFocusedRect) { return requestFocusNoSearch(direction, previouslyFocusedRect); } private boolean requestFocusNoSearch(int direction, Rect previouslyFocusedRect) { // need to be focusable if (! CanTakeFocus ()) {// Determine if the current View is qualified to get the focus return false; } // need to be focusable in touch mode if in touch mode if (isInTouchMode() && (FOCUSABLE_IN_TOUCH_MODE ! = (mViewFlags & FOCUSABLE_IN_TOUCH_MODE)) {// If focusableInTouchMode is in touch mode, return false; } / / need to not have any parents blocking us if (hasAncestorThatBlocksDescendantFocus ()) {/ / parent layout should we ban access to focus the return false; } if (! isLayoutValid()) { mPrivateFlags |= PFLAG_WANTS_FOCUS; } else { clearParentsWantFocus(); }} handleFocusGainInternal(direction, previouslyFocusedRect); return true; }Copy the code

Get focus

 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) {
                // Tell the parent to get the current View to get the focus and clear the old focus
                // When passed to the topmost ViewRootImpl, it triggers a View tree redraw
                mParent.requestChildFocus(this.this);
                updateFocusedInCluster(oldFocus, direction);
            }

            if(mAttachInfo ! =null) {
                mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, this);
            }
			// The listener that notifies the listener of the focus change
            onFocusChanged(true, direction, previouslyFocusedRect);
            refreshDrawableState();// Update drawble status, highlight, etc}}Copy the code

It tells the parent layout to get the current View to get the focus, and to clear the old focus

Override 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) {// clear the old focus state if (mFocused! = null) { mFocused.unFocus(focused); } mFocused = child; } if (mParent ! = null) {/ / continue to perform the current operation mParent requestChildFocus (this, focused); }}Copy the code

The requestChildFocus method reports to the top of the View tree, the ViewRootImpl, and triggers a redraw of the View tree. This should be used when optimizing UI efficiency.

# ViewRootImpl. Java@Override
    public void requestChildFocus(View child, View focused) {
        if (DEBUG_INPUT_RESIZE) {
            Log.v(mTag, "Request child focus: focus now " + focused);
        }
        checkThread();// Check the thread
        scheduleTraversals();// Trigger the View to redraw
    }
Copy the code

To summarize what a View’s requestFoucs does:

  • First determine whether the current View is eligible for focus
  • If it qualifies for focus, the View adds PFLAG_FOCUSED, and layers the parent layout to notify it, ensuring that only one View gets focus. A call to the View’s top ViewRootImpl triggers a redraw of the entire View tree.
  • Call onFoucsChange to notify the listener.
  • Update the Drawable status.

The ViewGroup requestFoucs process is a little more complicated than the View process.

 public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
        if (DBG) {
            System.out.println(this + " ViewGroup.requestFocus direction="
                    + direction);
        }
        int descendantFocusability = getDescendantFocusability();

        boolean result;
        switch (descendantFocusability) {
            case FOCUS_BLOCK_DESCENDANTS:// Disable the child View from getting focus
                result = super.requestFocus(direction, previouslyFocusedRect);
                break;
            case FOCUS_BEFORE_DESCENDANTS: {// The priority child View gets the focus
                final boolean took = super.requestFocus(direction, previouslyFocusedRect);
                result = took ? took : onRequestFocusInDescendants(direction,
                        previouslyFocusedRect);
                break;
            }
            case FOCUS_AFTER_DESCENDANTS: {// The child View gets focus first
                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;
    }
Copy the code

OnRequestFocusInDescendants is responsible for the child to get focus View, developers can copy the method, a custom View for the focus strategy.

  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 (inti = index; i ! = end; i += increment) { View child = children[i];if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
                if (child.requestFocus(direction, previouslyFocusedRect)) {
                    return true; }}}return false;
    }
Copy the code

conclusion

Ok, so to recap, you start by pressing a key with the remote control, until the device receives the key signal, which is then converted into an Event via the Android input system, and then distributed to the ViewRootImpl of the current window, Then look for the next View that gets focus with the View’s focusSearch, and make it really get focus with the requestFocus. This article mainly discusses the focus, so the Input system of Android, WMS system and so on are relatively simple to skip, but since it is the focus of life, so the context and context are mentioned, but these modules involved are very important and complex, hope to be a separate article later to introduce. Finished!

The resources

Android key focus distribution processing mechanism

In-depth understanding of Android Volume 3