Mobile Development 14 min read

How Android TV Manages Focus Navigation: A Deep Dive into KeyEvent and FocusSearch

This article explains how Android TV processes focus changes using KeyEvent dispatch, the dispatchKeyEvent flow, and the focusSearch algorithm, covering XML attributes, Java methods, and the internal View hierarchy that determines which view receives focus.

Tencent Music Tech Team
Tencent Music Tech Team
Tencent Music Tech Team
How Android TV Manages Focus Navigation: A Deep Dive into KeyEvent and FocusSearch

Overview

On Android TV a view must have focus to receive click events. Focus is moved with the remote’s directional keys and confirmed with the OK button.

Making a View Focusable

Set android:focusable="true" and, if the view must receive focus in touch mode, also android:focusableInTouchMode="true" (or call setFocusable(true) / setFocusableInTouchMode(true) in code). Example XML:

<Button
    android:id="@+id/btn_1"
    android:focusable="true"
    android:focusableInTouchMode="true" />

Programmatically request focus with view.requestFocus(). The navigation direction can be overridden with the XML attributes android:nextFocusDown, android:nextFocusLeft, android:nextFocusRight, android:nextFocusUp or the corresponding setNextFocus* methods.

KeyEvent Dispatch Flow (API 23)

When a remote button is pressed a KeyEvent is created and first processed by ViewRootImpl in the inner class ViewPostImeInputStage.processKeyEvent(). The method performs three main actions:

Call mView.dispatchKeyEvent(event) to give the view hierarchy a chance to consume the event.

If the event is a KeyEvent.ACTION_DOWN and represents a directional key, compute the direction and invoke focusSearch(direction) on the currently focused view.

If no view currently has focus, call focusSearch(null, direction) from the root.

private int processKeyEvent(QueuedInputEvent q) {
    KeyEvent event = (KeyEvent) q.mEvent;
    if (mView.dispatchKeyEvent(event)) {
        return FINISH_HANDLED;
    }
    if (event.getAction() == KeyEvent.ACTION_DOWN) {
        int direction = /* map keyCode to direction */;
        if (direction != 0) {
            View focused = mView.findFocus();
            if (focused != null) {
                View next = focused.focusSearch(direction);
                if (next != null && next != focused && next.requestFocus(direction, mTempRect)) {
                    return FINISH_HANDLED;
                }
                if (mView.dispatchUnhandledMove(focused, direction)) {
                    return FINISH_HANDLED;
                }
            } else {
                View next = focusSearch(null, direction);
                if (next != null && next.requestFocus(direction)) {
                    return FINISH_HANDLED;
                }
            }
        }
    }
    return FORWARD;
}

dispatchKeyEvent in ViewGroup

The ViewGroup.dispatchKeyEvent() implementation first tries to handle the event itself; if it cannot, it forwards the event to the child that currently holds focus.

@Override
public boolean dispatchKeyEvent(KeyEvent event) {
    if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS))
            == (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) {
        if (super.dispatchKeyEvent(event)) {
            return true;
        }
    } else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS) == PFLAG_HAS_BOUNDS) {
        if (mFocused.dispatchKeyEvent(event)) {
            return true;
        }
    }
    return false;
}

dispatchKeyEvent in View

The view first checks an OnKeyListener, then delegates to KeyEvent.dispatch(), which eventually calls the view’s onKeyDown/onKeyUp callbacks.

public boolean dispatchKeyEvent(KeyEvent event) {
    ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnKeyListener != null
            && (mViewFlags & ENABLED_MASK) == ENABLED
            && li.mOnKeyListener.onKey(this, event.getKeyCode(), event)) {
        return true;
    }
    return event.dispatch(this,
            mAttachInfo != null ? mAttachInfo.mKeyDispatchState : null,
            this);
}

Focus Search Algorithm

The method View.focusSearch(int direction) delegates to its parent. The root ViewGroup calls FocusFinder.getInstance().findNextFocus().

public View focusSearch(@FocusRealDirection int direction) {
    return (mParent != null) ? mParent.focusSearch(this, direction) : null;
}
public View focusSearch(View focused, int direction) {
    if (isRootNamespace()) {
        return FocusFinder.getInstance().findNextFocus(this, focused, direction);
    } else if (mParent != null) {
        return mParent.focusSearch(focused, direction);
    }
    return null;
}

FocusFinder Logic

FocusFinder performs two passes:

Check for a developer‑specified next focus view via findNextUserSpecifiedFocus(). This reads the XML attributes ( nextFocus*) or the values set with setNextFocus*.

If no explicit target is found, collect all focusable views in the hierarchy and choose the one that is geometrically closest in the requested direction.

private View findNextUserSpecifiedFocus(ViewGroup root, View focused, int direction) {
    View user = focused.findUserSetNextFocus(root, direction);
    if (user != null && user.isFocusable()
            && (!user.isInTouchMode() || user.isFocusableInTouchMode())) {
        return user;
    }
    return null;
}

Resolving nextFocus IDs

The view searches for the ID specified by the nextFocus* attribute using findViewInsideOutShouldExist(). The search starts at the root, walks down the tree, and if the ID is not found it climbs up the hierarchy. When multiple views share the same ID (e.g., items inside a RecyclerView), the algorithm returns the nearest match, which can cause “focus jumps” that appear to ignore the developer’s specification.

private View findViewInsideOutShouldExist(View root, int id) {
    if (mMatchIdPredicate == null) {
        mMatchIdPredicate = new MatchIdPredicate();
    }
    mMatchIdPredicate.mId = id;
    return root.findViewByPredicateInsideOut(this, mMatchIdPredicate);
}

Practical Guidelines

Ensure every interactive view has android:focusable="true" (and android:focusableInTouchMode="true" if needed).

Guide directional navigation with android:nextFocus* attributes or the corresponding setNextFocus* methods.

Avoid duplicate IDs across different view hierarchies; duplicate IDs make the “user‑specified” search return the nearest instance, which may be unexpected.

Remember that onKeyDown will only be called if the event is not consumed earlier in the dispatch chain ( ViewRootImpl → ViewGroup → View).

Focus highlight example
Focus highlight example
Focus movement animation
Focus movement animation
View hierarchy diagram
View hierarchy diagram
KeyEvent dispatch flow
KeyEvent dispatch flow
Android TVKeyEventView HierarchyFocus NavigationFocusSearch
Tencent Music Tech Team
Written by

Tencent Music Tech Team

Public account of Tencent Music's development team, focusing on technology sharing and communication.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.