diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index e36a2989afbcd..19ce43febf690 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -4163,6 +4163,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ private static boolean sUseDefaultFocusHighlight; + /** + * True if zero-sized views can be focused. + */ + private static boolean sCanFocusZeroSized; + private String mTransitionName; static class TintInfo { @@ -4745,6 +4750,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, sUseDefaultFocusHighlight = context.getResources().getBoolean( com.android.internal.R.bool.config_useDefaultFocusHighlight); + sCanFocusZeroSized = targetSdkVersion < Build.VERSION_CODES.P; + sCompatibilityDone = true; } } @@ -6952,6 +6959,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, void clearFocusInternal(View focused, boolean propagate, boolean refocus) { if ((mPrivateFlags & PFLAG_FOCUSED) != 0) { mPrivateFlags &= ~PFLAG_FOCUSED; + clearParentsWantFocus(); if (propagate && mParent != null) { mParent.clearChildFocus(this); @@ -10933,8 +10941,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * descendants. * * A view will not actually take focus if it is not focusable ({@link #isFocusable} returns - * false), or if it is focusable and it is not focusable in touch mode - * ({@link #isFocusableInTouchMode}) while the device is in touch mode. + * false), or if it can't be focused due to other conditions (not focusable in touch mode + * ({@link #isFocusableInTouchMode}) while the device is in touch mode, not visible, not + * enabled, or has no size). * * See also {@link #focusSearch(int)}, which is what you call to say that you * have focus, and you want your parent to look for the next one. @@ -10972,17 +10981,17 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ @TestApi public boolean restoreFocusNotInCluster() { - return requestFocus(View.FOCUS_DOWN); + return requestFocus(); } /** * Gives focus to the default-focus view in the view hierarchy that has this view as a root. - * If the default-focus view cannot be found, falls back to calling {@link #requestFocus(int)}. + * If the default-focus view cannot be found, falls back to calling {@link #requestFocus()}. * * @return Whether this view or one of its descendants actually took focus */ public boolean restoreDefaultFocus() { - return requestFocus(View.FOCUS_DOWN); + return requestFocus(); } /** @@ -11041,9 +11050,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, private boolean requestFocusNoSearch(int direction, Rect previouslyFocusedRect) { // need to be focusable - if ((mViewFlags & FOCUSABLE) != FOCUSABLE - || (mViewFlags & VISIBILITY_MASK) != VISIBLE - || (mViewFlags & ENABLED_MASK) != ENABLED) { + if (!canTakeFocus()) { return false; } @@ -11058,10 +11065,21 @@ public class View implements Drawable.Callback, KeyEvent.Callback, return false; } + if (!isLaidOut()) { + mPrivateFlags |= PFLAG_WANTS_FOCUS; + } + handleFocusGainInternal(direction, previouslyFocusedRect); return true; } + void clearParentsWantFocus() { + if (mParent instanceof View) { + ((View) mParent).mPrivateFlags &= ~PFLAG_WANTS_FOCUS; + ((View) mParent).clearParentsWantFocus(); + } + } + /** * Call this to try to give focus to a specific view or to one of its descendants. This is a * special variant of {@link #requestFocus() } that will allow views that are not focusable in @@ -13433,6 +13451,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback, mAttachInfo.mUnbufferedDispatchRequested = true; } + private boolean canTakeFocus() { + return ((mViewFlags & VISIBILITY_MASK) == VISIBLE) + && ((mViewFlags & FOCUSABLE) == FOCUSABLE) + && ((mViewFlags & ENABLED_MASK) == ENABLED) + && (sCanFocusZeroSized || !isLaidOut() || (mBottom > mTop) && (mRight > mLeft)); + } + /** * Set flags controlling behavior of this view. * @@ -13452,6 +13477,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, return; } int privateFlags = mPrivateFlags; + boolean shouldNotifyFocusableAvailable = false; // If focusable is auto, update the FOCUSABLE bit. int focusableChangedByAuto = 0; @@ -13490,7 +13516,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, || focusableChangedByAuto == 0 || viewRootImpl == null || viewRootImpl.mThread == Thread.currentThread()) { - mParent.focusableViewAvailable(this); + shouldNotifyFocusableAvailable = true; } } } @@ -13513,10 +13539,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, // about in case nothing has focus. even if this specific view // isn't focusable, it may contain something that is, so let // the root view try to give this focus if nothing else does. - if ((mParent != null) && ((mViewFlags & ENABLED_MASK) == ENABLED) - && (mBottom > mTop) && (mRight > mLeft)) { - mParent.focusableViewAvailable(this); - } + shouldNotifyFocusableAvailable = true; } } @@ -13525,17 +13548,18 @@ public class View implements Drawable.Callback, KeyEvent.Callback, // a view becoming enabled should notify the parent as long as the view is also // visible and the parent wasn't already notified by becoming visible during this // setFlags invocation. - if ((mViewFlags & VISIBILITY_MASK) == VISIBLE - && ((changed & VISIBILITY_MASK) == 0)) { - if ((mParent != null) && (mViewFlags & ENABLED_MASK) == ENABLED) { - mParent.focusableViewAvailable(this); - } - } + shouldNotifyFocusableAvailable = true; } else { if (hasFocus()) clearFocus(); } } + if (shouldNotifyFocusableAvailable) { + if (mParent != null && canTakeFocus()) { + mParent.focusableViewAvailable(this); + } + } + /* Check if the GONE bit has changed */ if ((changed & GONE) != 0) { needGlobalAttributesUpdate(false); @@ -20034,15 +20058,58 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } } + final boolean wasLaidOut = isLaidOut(); + mPrivateFlags &= ~PFLAG_FORCE_LAYOUT; mPrivateFlags3 |= PFLAG3_IS_LAID_OUT; + if (!wasLaidOut && isFocused()) { + mPrivateFlags &= ~PFLAG_WANTS_FOCUS; + if (canTakeFocus()) { + // We have a robust focus, so parents should no longer be wanting focus. + clearParentsWantFocus(); + } else if (!getViewRootImpl().isInLayout()) { + // This is a weird case. Most-likely the user, rather than ViewRootImpl, called + // layout. In this case, there's no guarantee that parent layouts will be evaluated + // and thus the safest action is to clear focus here. + clearFocusInternal(null, /* propagate */ true, /* refocus */ false); + clearParentsWantFocus(); + } else if (!hasParentWantsFocus()) { + // original requestFocus was likely on this view directly, so just clear focus + clearFocusInternal(null, /* propagate */ true, /* refocus */ false); + } + // otherwise, we let parents handle re-assigning focus during their layout passes. + } else if ((mPrivateFlags & PFLAG_WANTS_FOCUS) != 0) { + mPrivateFlags &= ~PFLAG_WANTS_FOCUS; + View focused = findFocus(); + if (focused != null) { + // Try to restore focus as close as possible to our starting focus. + if (!restoreDefaultFocus() && !hasParentWantsFocus()) { + // Give up and clear focus once we've reached the top-most parent which wants + // focus. + focused.clearFocusInternal(null, /* propagate */ true, /* refocus */ false); + } + } + } + if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) { mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT; notifyEnterOrExitForAutoFillIfNeeded(true); } } + private boolean hasParentWantsFocus() { + ViewParent parent = mParent; + while (parent instanceof ViewGroup) { + ViewGroup pv = (ViewGroup) parent; + if ((pv.mPrivateFlags & PFLAG_WANTS_FOCUS) != 0) { + return true; + } + parent = pv.mParent; + } + return false; + } + /** * Called from layout when this view should * assign a size and position to each of its children. @@ -20149,6 +20216,23 @@ public class View implements Drawable.Callback, KeyEvent.Callback, mOverlay.getOverlayView().setRight(newWidth); mOverlay.getOverlayView().setBottom(newHeight); } + // If this isn't laid out yet, focus assignment will be handled during the "deferment/ + // backtracking" of requestFocus during layout, so don't touch focus here. + if (!sCanFocusZeroSized && isLaidOut()) { + if (newWidth <= 0 || newHeight <= 0) { + if (hasFocus()) { + clearFocus(); + if (mParent instanceof ViewGroup) { + ((ViewGroup) mParent).clearFocusedInCluster(); + } + } + clearAccessibilityFocus(); + } else if (oldWidth <= 0 || oldHeight <= 0) { + if (mParent != null && canTakeFocus()) { + mParent.focusableViewAvailable(this); + } + } + } rebuildOutline(); } diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index 929beaea42b85..d0fcc5dfb8144 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -3215,22 +3215,31 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } int descendantFocusability = getDescendantFocusability(); + boolean result; switch (descendantFocusability) { case FOCUS_BLOCK_DESCENDANTS: - return super.requestFocus(direction, previouslyFocusedRect); + result = super.requestFocus(direction, previouslyFocusedRect); + break; case FOCUS_BEFORE_DESCENDANTS: { final boolean took = super.requestFocus(direction, previouslyFocusedRect); - return took ? took : onRequestFocusInDescendants(direction, previouslyFocusedRect); + result = took ? took : onRequestFocusInDescendants(direction, + previouslyFocusedRect); + break; } case FOCUS_AFTER_DESCENDANTS: { final boolean took = onRequestFocusInDescendants(direction, previouslyFocusedRect); - return took ? took : super.requestFocus(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 && !isLaidOut() && ((mPrivateFlags & PFLAG_WANTS_FOCUS) == 0)) { + mPrivateFlags |= PFLAG_WANTS_FOCUS; + } + return result; } /**