Understanding Android Focus Navigation: The findNextFocus Mechanism
When Android cannot use a specified nextFocusId, it gathers all focusable views via ViewGroup.addFocusables, converts their rectangles to the root’s coordinate space, and through FocusFinder.findNextFocus selects the nearest view in the requested direction, then requests focus, with key‑event handling prioritized before this automatic search.
Due to the character limit of the original WeChat article, the content is split into two parts. This is the continuation.
2.2 findNextFocus
If a developer does not specify a nextFocusId , Android uses findNextFocus to locate the nearest view in the requested direction.
Typical usage:
focusables.clear();
// 2.2.1 Find all views that are focusable
root.addFocusables(focusables, direction);
if (!focusables.isEmpty()) {
// 2.2.2 From the focusable list, find the nearest one
next = findNextFocus(root, focused, focusedRect, direction, focusables);
}2.2.1 View.addFocusables – Starting from the root, it collects all isFocusable views.
public void addFocusables(ArrayList<View> views, @FocusDirection int direction) {
addFocusables(views, direction, FOCUSABLES_TOUCH_MODE);
}
public void addFocusables(ArrayList<View> views, @FocusDirection int direction,
@FocusableMode int focusableMode) {
...
views.add(this);
}If the root is a simple View , it adds itself, but this is rare; most roots are ViewGroup s.
// ViewGroup.java
public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
final int focusableCount = views.size();
final int descendantFocusability = getDescendantFocusability();
if (descendantFocusability != FOCUS_BLOCK_DESCENDANTS) {
final int count = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < count; i++) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
child.addFocusables(views, direction, focusableMode);
}
}
}
if ((descendantFocusability != FOCUS_AFTER_DESCENDANTS || (focusableCount == views.size()))
&& (isFocusableInTouchMode() || !shouldBlockFocusForTouchscreen())) {
super.addFocusables(views, direction, focusableMode);
}
}For a ViewGroup , it traverses its children and adds those that are focusable. The variable descendantFocusability can take three values:
FOCUS_BEFORE_DESCENDANTS – the group gets focus before its children.
FOCUS_AFTER_DESCENDANTS – the group gets focus after its children.
FOCUS_BLOCK_DESCENDANTS – the group blocks all its descendants from receiving focus, even if they are focusable.
2.2.2 FocusFinder.findNextFocus
private View findNextFocus(ViewGroup root, View focused, Rect focusedRect,
int direction, ArrayList<View> focusables) {
if (focused != null) {
if (focusedRect == null) {
focusedRect = mFocusedRect;
}
// 2.2.2.1 Obtain the focused rect after accounting for scroll
focused.getFocusedRect(focusedRect);
// 2.2.2.2 Convert the focused view's coordinates to the root's coordinate system
root.offsetDescendantRectToMyCoords(focused, focusedRect);
} else {
...
}
switch (direction) {
case View.FOCUS_UP:
case View.FOCUS_DOWN:
case View.FOCUS_LEFT:
case View.FOCUS_RIGHT:
// 2.2.2.3 Find the next focusable view in the given direction
return findNextFocusInAbsoluteDirection(focusables, root, focused,
focusedRect, direction);
default:
throw new IllegalArgumentException("Unknown direction: " + direction);
}
}2.2.2.1 focused.getFocusedRect(focusedRect)
public void getFocusedRect(Rect r) {
getDrawingRect(r);
}
public void getDrawingRect(Rect outRect) {
outRect.left = mScrollX;
outRect.top = mScrollY;
outRect.right = mScrollX + (mRight - mLeft);
outRect.bottom = mScrollY + (mBottom - mTop);
}This obtains the focused rectangle after scroll offsets, relative to the focused view itself.
2.2.2.2 root.offsetDescendantRectToMyCoords(focused, focusedRect)
public final void offsetDescendantRectToMyCoords(View descendant, Rect rect) {
offsetRectBetweenParentAndChild(descendant, rect, true, false);
}
void offsetRectBetweenParentAndChild(View descendant, Rect rect,
boolean offsetFromChildToParent, boolean clipToBounds) {
if (descendant == this) {
return;
}
ViewParent theParent = descendant.mParent;
while ((theParent != null) && (theParent instanceof View) && (theParent != this)) {
if (offsetFromChildToParent) {
rect.offset(descendant.mLeft - descendant.mScrollX,
descendant.mTop - descendant.mScrollY);
} else {
rect.offset(descendant.mScrollX - descendant.mLeft,
descendant.mScrollY - descendant.mTop);
}
descendant = (View) theParent;
theParent = descendant.mParent;
}
if (theParent == this) {
if (offsetFromChildToParent) {
rect.offset(descendant.mLeft - descendant.mScrollX,
descendant.mTop - descendant.mScrollY);
} else {
rect.offset(descendant.mScrollX - descendant.mLeft,
descendant.mScrollY - descendant.mTop);
}
} else {
throw new IllegalArgumentException("parameter must be a descendant of this view");
}
}Through successive coordinate transformations, the focused view’s rectangle is finally expressed in the root’s coordinate space, enabling uniform calculations.
2.2.2.3 findNextFocusInAbsoluteDirection
View findNextFocusInAbsoluteDirection(ArrayList<View> focusables, ViewGroup root, View focused,
Rect focusedRect, int direction) {
mBestCandidateRect.set(focusedRect);
switch(direction) {
case View.FOCUS_LEFT:
// Create a default candidate rectangle just to the right of the focused rect
mBestCandidateRect.offset(focusedRect.width() + 1, 0);
break;
...
}
View closest = null;
int numFocusables = focusables.size();
for (int i = 0; i < numFocusables; i++) {
View focusable = focusables.get(i);
if (focusable == focused || focusable == root) continue;
focusable.getFocusedRect(mOtherRect);
root.offsetDescendantRectToMyCoords(focusable, mOtherRect);
if (isBetterCandidate(direction, focusedRect, mOtherRect, mBestCandidateRect)) {
mBestCandidateRect.set(mOtherRect);
closest = focusable;
}
}
return closest;
}After normalizing coordinates, the algorithm iterates over all focusable views, compares their rectangles with the current best candidate, and selects the view that is closest in the requested direction. The helper method isBetterCandidate encapsulates the geometric comparison.
Once the next focus view is identified, requestFocus is called to transfer focus.
Summary
The system’s process for locating the next focus view is:
First, if a nextFocusId is specified, the framework searches from the current focus node to find the view with that ID, stopping at the nearest match in the view hierarchy.
If no ID is provided, it gathers all isFocusable views, converts them to a common coordinate system, and computes which view lies closest in the requested direction.
Combined with the KeyEvent flow, the priority order for handling focus is:
dispatchKeyEvent
mOnKeyListener.onKey callback
onKeyDown / onKeyUp
focusSearch
Specified nextFocusId
Automatic search among all focusable views
Any earlier step that successfully determines a focus target halts further processing.
Many view classes override parts of this chain. For example, ScrollView handles key events in its dispatchKeyEvent method to perform internal focus movement or scrolling.
// ScrollView.java
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
// Let the focused view and/or our descendants get the key first
return super.dispatchKeyEvent(event) || executeKeyEvent(event);
}
public boolean executeKeyEvent(KeyEvent event) {
mTempRect.setEmpty();
if (!canScroll()) {
if (isFocused() && event.getKeyCode() != KeyEvent.KEYCODE_BACK) {
View currentFocused = findFocus();
if (currentFocused == this) currentFocused = null;
View nextFocused = FocusFinder.getInstance().findNextFocus(this,
currentFocused, View.FOCUS_DOWN);
return nextFocused != null && nextFocused != this &&
nextFocused.requestFocus(View.FOCUS_DOWN);
}
return false;
}
boolean handled = false;
if (event.getAction() == KeyEvent.ACTION_DOWN) {
switch (event.getKeyCode()) {
case KeyEvent.KEYCODE_DPAD_UP:
handled = event.isAltPressed() ? fullScroll(View.FOCUS_UP) : arrowScroll(View.FOCUS_UP);
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
handled = event.isAltPressed() ? fullScroll(View.FOCUS_DOWN) : arrowScroll(View.FOCUS_DOWN);
break;
case KeyEvent.KEYCODE_SPACE:
pageScroll(event.isShiftPressed() ? View.FOCUS_UP : View.FOCUS_DOWN);
break;
}
}
return handled;
}Because dispatchKeyEvent processes the event first, key events related to scrolling are not seen by onKeyDown , which explains why developers sometimes cannot capture those key events there.
This article analyzes the source code to explain the underlying principles of focus movement in Android. Readers are encouraged to explore further and discuss.
Tencent Music Tech Team
Public account of Tencent Music's development team, focusing on technology sharing and communication.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.