From 8062949ece210df7c7a61478448d05c8ad286d58 Mon Sep 17 00:00:00 2001 From: Jiaquan He Date: Mon, 6 Feb 2017 15:59:26 -0800 Subject: [PATCH] Draw a default focus highlight if needed. This commit draws a default highlight for a focused View if it's detected to have no state_focused defined and its useDefaultFocusHighlight attribute is true. When we detect a default highlight is needed, we show it on top of the view to. Once we detect that it's no longer needed, we remove it. Test: Check that views without a focused_state in its state spec have a default highlight when they get focused. Bug: 35096940 Change-Id: Ifbe4bb9e1297d98845314e24d8b758f14e5987a9 --- core/java/android/view/View.java | 149 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 147 insertions(+), 2 deletions(-) diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 5c061eeaf75b..546d8f7d893b 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -3920,6 +3920,14 @@ public class View implements Drawable.Callback, KeyEvent.Callback, private int mBackgroundResource; private boolean mBackgroundSizeChanged; + /** The default focus highlight. + * @see #mDefaultFocusHighlightEnabled + * @see Drawable#hasFocusStateSpecified() + */ + private Drawable mDefaultFocusHighlight; + private Drawable mDefaultFocusHighlightCache; + private boolean mDefaultFocusHighlightSizeChanged; + private String mTransitionName; static class TintInfo { @@ -6811,6 +6819,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED); } + // Here we check whether we still need the default focus highlight, and switch it on/off. + switchDefaultFocusHighlight(); + InputMethodManager imm = InputMethodManager.peekInstance(); if (!gainFocus) { if (isPressed()) { @@ -11790,6 +11801,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if (dr != null && isVisible != dr.isVisible()) { dr.setVisible(isVisible, false); } + final Drawable hl = mDefaultFocusHighlight; + if (hl != null && isVisible != hl.isVisible()) { + hl.setVisible(isVisible, false); + } final Drawable fg = mForegroundInfo != null ? mForegroundInfo.mDrawable : null; if (fg != null && isVisible != fg.isVisible()) { fg.setVisible(isVisible, false); @@ -13003,6 +13018,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if ((changed & DRAW_MASK) != 0) { if ((mViewFlags & WILL_NOT_DRAW) != 0) { if (mBackground != null + || mDefaultFocusHighlight != null || (mForegroundInfo != null && mForegroundInfo.mDrawable != null)) { mPrivateFlags &= ~PFLAG_SKIP_DRAW; } else { @@ -13074,6 +13090,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } mBackgroundSizeChanged = true; + mDefaultFocusHighlightSizeChanged = true; if (mForegroundInfo != null) { mForegroundInfo.mBoundsChanged = true; } @@ -13965,6 +13982,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, invalidate(true); } mBackgroundSizeChanged = true; + mDefaultFocusHighlightSizeChanged = true; if (mForegroundInfo != null) { mForegroundInfo.mBoundsChanged = true; } @@ -14033,6 +14051,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, invalidate(true); } mBackgroundSizeChanged = true; + mDefaultFocusHighlightSizeChanged = true; if (mForegroundInfo != null) { mForegroundInfo.mBoundsChanged = true; } @@ -14095,6 +14114,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, invalidate(true); } mBackgroundSizeChanged = true; + mDefaultFocusHighlightSizeChanged = true; if (mForegroundInfo != null) { mForegroundInfo.mBoundsChanged = true; } @@ -14154,6 +14174,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, invalidate(true); } mBackgroundSizeChanged = true; + mDefaultFocusHighlightSizeChanged = true; if (mForegroundInfo != null) { mForegroundInfo.mBoundsChanged = true; } @@ -18758,6 +18779,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, // Step 6, draw decorations (foreground, scrollbars) onDrawForeground(canvas); + // Step 7, draw the default focus highlight + drawDefaultFocusHighlight(canvas); + if (debugDraw()) { debugDrawFocus(canvas); } @@ -19316,6 +19340,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, mPrivateFlags |= drawn; mBackgroundSizeChanged = true; + mDefaultFocusHighlightSizeChanged = true; if (mForegroundInfo != null) { mForegroundInfo.mBoundsChanged = true; } @@ -19467,6 +19492,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if (mForegroundInfo != null && mForegroundInfo.mDrawable != null) { mForegroundInfo.mDrawable.setLayoutDirection(layoutDirection); } + if (mDefaultFocusHighlight != null) { + mDefaultFocusHighlight.setLayoutDirection(layoutDirection); + } mPrivateFlags2 |= PFLAG2_DRAWABLE_RESOLVED; onResolveDrawables(layoutDirection); } @@ -19525,7 +19553,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, // Avoid verifying the scroll bar drawable so that we don't end up in // an invalidation loop. This effectively prevents the scroll bar // drawable from triggering invalidations and scheduling runnables. - return who == mBackground || (mForegroundInfo != null && mForegroundInfo.mDrawable == who); + return who == mBackground || (mForegroundInfo != null && mForegroundInfo.mDrawable == who) + || (mDefaultFocusHighlight == who); } /** @@ -19549,6 +19578,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, changed |= bg.setState(state); } + final Drawable hl = mDefaultFocusHighlight; + if (hl != null && hl.isStateful()) { + changed |= hl.setState(state); + } + final Drawable fg = mForegroundInfo != null ? mForegroundInfo.mDrawable : null; if (fg != null && fg.isStateful()) { changed |= fg.setState(state); @@ -19588,6 +19622,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if (mBackground != null) { mBackground.setHotspot(x, y); } + if (mDefaultFocusHighlight != null) { + mDefaultFocusHighlight.setHotspot(x, y); + } if (mForegroundInfo != null && mForegroundInfo.mDrawable != null) { mForegroundInfo.mDrawable.setHotspot(x, y); } @@ -19624,6 +19661,104 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** + * Create a default focus highlight if it doesn't exist. + * @return a default focus highlight. + */ + private Drawable getDefaultFocusHighlightDrawable() { + if (mDefaultFocusHighlightCache == null) { + if (mContext != null) { + final int[] attrs = new int[] { android.R.attr.selectableItemBackground }; + final TypedArray ta = mContext.obtainStyledAttributes(attrs); + mDefaultFocusHighlightCache = ta.getDrawable(0); + ta.recycle(); + } + } + return mDefaultFocusHighlightCache; + } + + /** + * Set the current default focus highlight. + * @param highlight the highlight drawable, or {@code null} if it's no longer needed. + */ + private void setDefaultFocusHighlight(Drawable highlight) { + mDefaultFocusHighlight = highlight; + mDefaultFocusHighlightSizeChanged = true; + if (highlight != null) { + if ((mPrivateFlags & PFLAG_SKIP_DRAW) != 0) { + mPrivateFlags &= ~PFLAG_SKIP_DRAW; + } + highlight.setLayoutDirection(getLayoutDirection()); + if (highlight.isStateful()) { + highlight.setState(getDrawableState()); + } + if (isAttachedToWindow()) { + highlight.setVisible(getWindowVisibility() == VISIBLE && isShown(), false); + } + // Set callback last, since the view may still be initializing. + highlight.setCallback(this); + } else if ((mViewFlags & WILL_NOT_DRAW) != 0 && mBackground == null + && (mForegroundInfo == null || mForegroundInfo.mDrawable == null)) { + mPrivateFlags |= PFLAG_SKIP_DRAW; + } + requestLayout(); + invalidate(); + } + + /** + * Check whether we need to draw a default focus highlight when this view gets focused, + * which requires: + * + * @return {@code true} if a default focus highlight is needed. + */ + private boolean isDefaultFocusHighlightNeeded(Drawable background) { + final boolean hasFocusStateSpecified = background == null || !background.isStateful() + || !background.hasFocusStateSpecified(); + return !isInTouchMode() && getDefaultFocusHighlightEnabled() && hasFocusStateSpecified; + } + + /** + * When this view is focused, switches on/off the default focused highlight. + *

+ * This always happens when this view is focused, and only at this moment the default focus + * highlight can be visible. + */ + private void switchDefaultFocusHighlight() { + if (isFocused()) { + final boolean needed = isDefaultFocusHighlightNeeded(mBackground); + final boolean active = mDefaultFocusHighlight != null; + if (needed && !active) { + setDefaultFocusHighlight(getDefaultFocusHighlightDrawable()); + } else if (!needed && active) { + // The highlight is no longer needed, so tear it down. + setDefaultFocusHighlight(null); + } + } + } + + /** + * Draw the default focus highlight onto the canvas. + * @param canvas the canvas where we're drawing the highlight. + */ + private void drawDefaultFocusHighlight(Canvas canvas) { + if (mDefaultFocusHighlight != null) { + if (mDefaultFocusHighlightSizeChanged) { + mDefaultFocusHighlightSizeChanged = false; + final int l = mScrollX; + final int r = l + mRight - mLeft; + final int t = mScrollY; + final int b = t + mBottom - mTop; + mDefaultFocusHighlight.setBounds(l, t, r, b); + } + mDefaultFocusHighlight.draw(canvas); + } + } + + /** * Return an array of resource IDs of the drawable states representing the * current state of the view. * @@ -19763,6 +19898,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if (mStateListAnimator != null) { mStateListAnimator.jumpToCurrentState(); } + if (mDefaultFocusHighlight != null) { + mDefaultFocusHighlight.jumpToCurrentState(); + } if (mForegroundInfo != null && mForegroundInfo.mDrawable != null) { mForegroundInfo.mDrawable.jumpToCurrentState(); } @@ -19907,6 +20045,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, /* Remove the background */ mBackground = null; if ((mViewFlags & WILL_NOT_DRAW) != 0 + && (mDefaultFocusHighlight == null) && (mForegroundInfo == null || mForegroundInfo.mDrawable == null)) { mPrivateFlags |= PFLAG_SKIP_DRAW; } @@ -20098,7 +20237,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } // Set callback last, since the view may still be initializing. foreground.setCallback(this); - } else if ((mViewFlags & WILL_NOT_DRAW) != 0 && mBackground == null) { + } else if ((mViewFlags & WILL_NOT_DRAW) != 0 && mBackground == null + && (mDefaultFocusHighlight == null)) { mPrivateFlags |= PFLAG_SKIP_DRAW; } requestLayout(); @@ -21913,6 +22053,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, // Similarly, we remove the foreground drawable's non-transparent parts. applyDrawableToTransparentRegion(mForegroundInfo.mDrawable, region); } + if (mDefaultFocusHighlight != null + && mDefaultFocusHighlight.getOpacity() != PixelFormat.TRANSPARENT) { + // Similarly, we remove the default focus highlight's non-transparent parts. + applyDrawableToTransparentRegion(mDefaultFocusHighlight, region); + } } } return true; -- 2.11.0