OSDN Git Service

Add support for automatic focusability determination
authorEvan Rosky <erosky@google.com>
Sat, 17 Dec 2016 01:27:55 +0000 (17:27 -0800)
committerEvan Rosky <erosky@google.com>
Thu, 19 Jan 2017 23:39:05 +0000 (15:39 -0800)
New android:focusable attribute value "auto" and corresponding constant
FOCUSABLE_AUTO specifies that the framework can determine a view's
focusability. For now, the only "auto" logic is that focusable = clickable.

This has the effect of making clickable views focusable by default
unless the developer has explicitly set them focusable=false.

Bug: 31963283
Change-Id: Icce768fa701966970ea6b3469b33a4ef97ebc3ea
Test: Added CTS test, related CTS passes, made a test-app which
      enumerates all clickable/focusable combinations to verify

api/current.txt
api/system-current.txt
api/test-current.txt
core/java/android/view/View.java
core/java/android/widget/RatingBar.java
core/java/android/widget/SearchView.java
core/java/android/widget/TextView.java
core/res/res/values/attrs.xml

index c4ed24e..16250dd 100644 (file)
@@ -43376,6 +43376,7 @@ package android.view {
     method public float getElevation();
     method public boolean getFilterTouchesWhenObscured();
     method public boolean getFitsSystemWindows();
+    method public int getFocusable();
     method public java.util.ArrayList<android.view.View> getFocusables(int);
     method public void getFocusedRect(android.graphics.Rect);
     method public android.graphics.drawable.Drawable getForeground();
@@ -43680,6 +43681,7 @@ package android.view {
     method public void setFilterTouchesWhenObscured(boolean);
     method public void setFitsSystemWindows(boolean);
     method public void setFocusable(boolean);
+    method public void setFocusable(int);
     method public void setFocusableInTouchMode(boolean);
     method public void setForeground(android.graphics.drawable.Drawable);
     method public void setForegroundGravity(int);
@@ -43818,8 +43820,10 @@ package android.view {
     field protected static final int[] ENABLED_WINDOW_FOCUSED_STATE_SET;
     field public static final int FIND_VIEWS_WITH_CONTENT_DESCRIPTION = 2; // 0x2
     field public static final int FIND_VIEWS_WITH_TEXT = 1; // 0x1
+    field public static final int FOCUSABLE = 1; // 0x1
     field public static final int FOCUSABLES_ALL = 0; // 0x0
     field public static final int FOCUSABLES_TOUCH_MODE = 1; // 0x1
+    field public static final int FOCUSABLE_AUTO = 16; // 0x10
     field protected static final int[] FOCUSED_SELECTED_STATE_SET;
     field protected static final int[] FOCUSED_SELECTED_WINDOW_FOCUSED_STATE_SET;
     field protected static final int[] FOCUSED_STATE_SET;
@@ -43851,6 +43855,7 @@ package android.view {
     field public static final int MEASURED_SIZE_MASK = 16777215; // 0xffffff
     field public static final int MEASURED_STATE_MASK = -16777216; // 0xff000000
     field public static final int MEASURED_STATE_TOO_SMALL = 16777216; // 0x1000000
+    field public static final int NOT_FOCUSABLE = 0; // 0x0
     field public static final int NO_ID = -1; // 0xffffffff
     field public static final int OVER_SCROLL_ALWAYS = 0; // 0x0
     field public static final int OVER_SCROLL_IF_CONTENT_SCROLLS = 1; // 0x1
index 01adae8..2c89a2e 100644 (file)
@@ -46600,6 +46600,7 @@ package android.view {
     method public float getElevation();
     method public boolean getFilterTouchesWhenObscured();
     method public boolean getFitsSystemWindows();
+    method public int getFocusable();
     method public java.util.ArrayList<android.view.View> getFocusables(int);
     method public void getFocusedRect(android.graphics.Rect);
     method public android.graphics.drawable.Drawable getForeground();
@@ -46904,6 +46905,7 @@ package android.view {
     method public void setFilterTouchesWhenObscured(boolean);
     method public void setFitsSystemWindows(boolean);
     method public void setFocusable(boolean);
+    method public void setFocusable(int);
     method public void setFocusableInTouchMode(boolean);
     method public void setForeground(android.graphics.drawable.Drawable);
     method public void setForegroundGravity(int);
@@ -47042,8 +47044,10 @@ package android.view {
     field protected static final int[] ENABLED_WINDOW_FOCUSED_STATE_SET;
     field public static final int FIND_VIEWS_WITH_CONTENT_DESCRIPTION = 2; // 0x2
     field public static final int FIND_VIEWS_WITH_TEXT = 1; // 0x1
+    field public static final int FOCUSABLE = 1; // 0x1
     field public static final int FOCUSABLES_ALL = 0; // 0x0
     field public static final int FOCUSABLES_TOUCH_MODE = 1; // 0x1
+    field public static final int FOCUSABLE_AUTO = 16; // 0x10
     field protected static final int[] FOCUSED_SELECTED_STATE_SET;
     field protected static final int[] FOCUSED_SELECTED_WINDOW_FOCUSED_STATE_SET;
     field protected static final int[] FOCUSED_STATE_SET;
@@ -47075,6 +47079,7 @@ package android.view {
     field public static final int MEASURED_SIZE_MASK = 16777215; // 0xffffff
     field public static final int MEASURED_STATE_MASK = -16777216; // 0xff000000
     field public static final int MEASURED_STATE_TOO_SMALL = 16777216; // 0x1000000
+    field public static final int NOT_FOCUSABLE = 0; // 0x0
     field public static final int NO_ID = -1; // 0xffffffff
     field public static final int OVER_SCROLL_ALWAYS = 0; // 0x0
     field public static final int OVER_SCROLL_IF_CONTENT_SCROLLS = 1; // 0x1
index ad84b7b..c141e74 100644 (file)
@@ -43665,6 +43665,7 @@ package android.view {
     method public float getElevation();
     method public boolean getFilterTouchesWhenObscured();
     method public boolean getFitsSystemWindows();
+    method public int getFocusable();
     method public java.util.ArrayList<android.view.View> getFocusables(int);
     method public void getFocusedRect(android.graphics.Rect);
     method public android.graphics.drawable.Drawable getForeground();
@@ -43970,6 +43971,7 @@ package android.view {
     method public void setFilterTouchesWhenObscured(boolean);
     method public void setFitsSystemWindows(boolean);
     method public void setFocusable(boolean);
+    method public void setFocusable(int);
     method public void setFocusableInTouchMode(boolean);
     method public void setForeground(android.graphics.drawable.Drawable);
     method public void setForegroundGravity(int);
@@ -44108,8 +44110,10 @@ package android.view {
     field protected static final int[] ENABLED_WINDOW_FOCUSED_STATE_SET;
     field public static final int FIND_VIEWS_WITH_CONTENT_DESCRIPTION = 2; // 0x2
     field public static final int FIND_VIEWS_WITH_TEXT = 1; // 0x1
+    field public static final int FOCUSABLE = 1; // 0x1
     field public static final int FOCUSABLES_ALL = 0; // 0x0
     field public static final int FOCUSABLES_TOUCH_MODE = 1; // 0x1
+    field public static final int FOCUSABLE_AUTO = 16; // 0x10
     field protected static final int[] FOCUSED_SELECTED_STATE_SET;
     field protected static final int[] FOCUSED_SELECTED_WINDOW_FOCUSED_STATE_SET;
     field protected static final int[] FOCUSED_STATE_SET;
@@ -44141,6 +44145,7 @@ package android.view {
     field public static final int MEASURED_SIZE_MASK = 16777215; // 0xffffff
     field public static final int MEASURED_STATE_MASK = -16777216; // 0xff000000
     field public static final int MEASURED_STATE_TOO_SMALL = 16777216; // 0x1000000
+    field public static final int NOT_FOCUSABLE = 0; // 0x0
     field public static final int NO_ID = -1; // 0xffffffff
     field public static final int OVER_SCROLL_ALWAYS = 0; // 0x0
     field public static final int OVER_SCROLL_IF_CONTENT_SCROLLS = 1; // 0x1
index aedd0df..1286185 100644 (file)
@@ -857,22 +857,39 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
      */
     static boolean sCascadedDragDrop;
 
+    /** @hide */
+    @IntDef({NOT_FOCUSABLE, FOCUSABLE, FOCUSABLE_AUTO})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Focusable {}
+
     /**
-     * This view does not want keystrokes. Use with TAKES_FOCUS_MASK when
-     * calling setFlags.
+     * This view does not want keystrokes.
+     * <p>
+     * Use with {@link #setFocusable(int)} and <a href="#attr_android:focusable">{@code
+     * android:focusable}.
      */
-    private static final int NOT_FOCUSABLE = 0x00000000;
+    public static final int NOT_FOCUSABLE = 0x00000000;
 
     /**
-     * This view wants keystrokes. Use with TAKES_FOCUS_MASK when calling
-     * setFlags.
+     * This view wants keystrokes.
+     * <p>
+     * Use with {@link #setFocusable(int)} and <a href="#attr_android:focusable">{@code
+     * android:focusable}.
      */
-    private static final int FOCUSABLE = 0x00000001;
+    public static final int FOCUSABLE = 0x00000001;
+
+    /**
+     * This view determines focusability automatically. This is the default.
+     * <p>
+     * Use with {@link #setFocusable(int)} and <a href="#attr_android:focusable">{@code
+     * android:focusable}.
+     */
+    public static final int FOCUSABLE_AUTO = 0x00000010;
 
     /**
      * Mask for use with setFlags indicating bits used for focus.
      */
-    private static final int FOCUSABLE_MASK = 0x00000001;
+    private static final int FOCUSABLE_MASK = 0x00000011;
 
     /**
      * This view will adjust its padding to fit sytem windows (e.g. status bar)
@@ -4160,7 +4177,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
     public View(Context context) {
         mContext = context;
         mResources = context != null ? context.getResources() : null;
-        mViewFlags = SOUND_EFFECTS_ENABLED | HAPTIC_FEEDBACK_ENABLED;
+        mViewFlags = SOUND_EFFECTS_ENABLED | HAPTIC_FEEDBACK_ENABLED | FOCUSABLE_AUTO;
         // Set some flags defaults
         mPrivateFlags2 =
                 (LAYOUT_DIRECTION_DEFAULT << PFLAG2_LAYOUT_DIRECTION_MASK_SHIFT) |
@@ -4346,6 +4363,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
 
         final int targetSdkVersion = context.getApplicationInfo().targetSdkVersion;
 
+        // Set default values.
+        viewFlagValues |= FOCUSABLE_AUTO;
+        viewFlagMasks |= FOCUSABLE_AUTO;
+
         final int N = a.getIndexCount();
         for (int i = 0; i < N; i++) {
             int attr = a.getIndex(i);
@@ -4458,8 +4479,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
                     }
                     break;
                 case com.android.internal.R.styleable.View_focusable:
-                    if (a.getBoolean(attr, false)) {
-                        viewFlagValues |= FOCUSABLE;
+                    viewFlagValues = (viewFlagValues & ~FOCUSABLE_MASK) | getFocusableAttribute(a);
+                    if ((viewFlagValues & FOCUSABLE_AUTO) == 0) {
                         viewFlagMasks |= FOCUSABLE_MASK;
                     }
                     break;
@@ -5033,7 +5054,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
             case GONE: out.append('G'); break;
             default: out.append('.'); break;
         }
-        out.append((mViewFlags&FOCUSABLE_MASK) == FOCUSABLE ? 'F' : '.');
+        out.append((mViewFlags & FOCUSABLE) == FOCUSABLE ? 'F' : '.');
         out.append((mViewFlags&ENABLED_MASK) == ENABLED ? 'E' : '.');
         out.append((mViewFlags&DRAW_MASK) == WILL_NOT_DRAW ? '.' : 'D');
         out.append((mViewFlags&SCROLLBARS_HORIZONTAL) != 0 ? 'H' : '.');
@@ -8503,20 +8524,39 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
 
     /**
      * Set whether this view can receive the focus.
-     *
+     * <p>
      * Setting this to false will also ensure that this view is not focusable
      * in touch mode.
      *
      * @param focusable If true, this view can receive the focus.
      *
      * @see #setFocusableInTouchMode(boolean)
+     * @see #setFocusable(int)
      * @attr ref android.R.styleable#View_focusable
      */
     public void setFocusable(boolean focusable) {
-        if (!focusable) {
+        setFocusable(focusable ? FOCUSABLE : NOT_FOCUSABLE);
+    }
+
+    /**
+     * Sets whether this view can receive focus.
+     * <p>
+     * Setting this to {@link #FOCUSABLE_AUTO} tells the framework to determine focusability
+     * automatically based on the view's interactivity. This is the default.
+     * <p>
+     * Setting this to NOT_FOCUSABLE will ensure that this view is also not focusable
+     * in touch mode.
+     *
+     * @param focusable One of {@link #NOT_FOCUSABLE}, {@link #FOCUSABLE},
+     *                  or {@link #FOCUSABLE_AUTO}.
+     * @see #setFocusableInTouchMode(boolean)
+     * @attr ref android.R.styleable#View_focusable
+     */
+    public void setFocusable(@Focusable int focusable) {
+        if ((focusable & (FOCUSABLE_AUTO | FOCUSABLE)) == 0) {
             setFlags(0, FOCUSABLE_IN_TOUCH_MODE);
         }
-        setFlags(focusable ? FOCUSABLE : NOT_FOCUSABLE, FOCUSABLE_MASK);
+        setFlags(focusable, FOCUSABLE_MASK);
     }
 
     /**
@@ -9106,14 +9146,29 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
 
 
     /**
-     * Returns whether this View is able to take focus.
+     * Returns whether this View is currently able to take focus.
      *
      * @return True if this view can take focus, or false otherwise.
-     * @attr ref android.R.styleable#View_focusable
      */
     @ViewDebug.ExportedProperty(category = "focus")
     public final boolean isFocusable() {
-        return FOCUSABLE == (mViewFlags & FOCUSABLE_MASK);
+        return FOCUSABLE == (mViewFlags & FOCUSABLE);
+    }
+
+    /**
+     * Returns the focusable setting for this view.
+     *
+     * @return One of {@link #NOT_FOCUSABLE}, {@link #FOCUSABLE}, or {@link #FOCUSABLE_AUTO}.
+     * @attr ref android.R.styleable#View_focusable
+     */
+    @ViewDebug.ExportedProperty(mapping = {
+            @ViewDebug.IntToString(from = NOT_FOCUSABLE, to = "NOT_FOCUSABLE"),
+            @ViewDebug.IntToString(from = FOCUSABLE, to = "FOCUSABLE"),
+            @ViewDebug.IntToString(from = FOCUSABLE_AUTO, to = "FOCUSABLE_AUTO")
+            })
+    @Focusable
+    public int getFocusable() {
+        return (mViewFlags & FOCUSABLE_AUTO) > 0 ? FOCUSABLE_AUTO : mViewFlags & FOCUSABLE;
     }
 
     /**
@@ -9653,8 +9708,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
 
     private boolean requestFocusNoSearch(int direction, Rect previouslyFocusedRect) {
         // need to be focusable
-        if ((mViewFlags & FOCUSABLE_MASK) != FOCUSABLE ||
-                (mViewFlags & VISIBILITY_MASK) != VISIBLE) {
+        if ((mViewFlags & FOCUSABLE) != FOCUSABLE
+                || (mViewFlags & VISIBILITY_MASK) != VISIBLE) {
             return false;
         }
 
@@ -12008,14 +12063,27 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
         }
         int privateFlags = mPrivateFlags;
 
+        // If focusable is auto, update the FOCUSABLE bit.
+        if (((mViewFlags & FOCUSABLE_AUTO) != 0)
+                && (changed & (FOCUSABLE_MASK | CLICKABLE | FOCUSABLE_IN_TOUCH_MODE)) != 0) {
+            int newFocus = NOT_FOCUSABLE;
+            if ((mViewFlags & (CLICKABLE | FOCUSABLE_IN_TOUCH_MODE)) != 0) {
+                newFocus = FOCUSABLE;
+            } else {
+                mViewFlags = (mViewFlags & ~FOCUSABLE_IN_TOUCH_MODE);
+            }
+            mViewFlags = (mViewFlags & ~FOCUSABLE) | newFocus;
+            int focusChanged = (old & FOCUSABLE) ^ (newFocus & FOCUSABLE);
+            changed = (changed & ~FOCUSABLE) | focusChanged;
+        }
+
         /* Check if the FOCUSABLE bit has changed */
-        if (((changed & FOCUSABLE_MASK) != 0) &&
-                ((privateFlags & PFLAG_HAS_BOUNDS) !=0)) {
-            if (((old & FOCUSABLE_MASK) == FOCUSABLE)
+        if (((changed & FOCUSABLE) != 0) && ((privateFlags & PFLAG_HAS_BOUNDS) != 0)) {
+            if (((old & FOCUSABLE) == FOCUSABLE)
                     && ((privateFlags & PFLAG_FOCUSED) != 0)) {
                 /* Give up focus if we are no longer focusable */
                 clearFocus();
-            } else if (((old & FOCUSABLE_MASK) == NOT_FOCUSABLE)
+            } else if (((old & FOCUSABLE) == NOT_FOCUSABLE)
                     && ((privateFlags & PFLAG_FOCUSED) == 0)) {
                 /*
                  * Tell the view system that we are now available to take focus
@@ -12158,7 +12226,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
         }
 
         if (accessibilityEnabled) {
-            if ((changed & FOCUSABLE_MASK) != 0 || (changed & VISIBILITY_MASK) != 0
+            if ((changed & FOCUSABLE) != 0 || (changed & VISIBILITY_MASK) != 0
                     || (changed & CLICKABLE) != 0 || (changed & LONG_CLICKABLE) != 0
                     || (changed & CONTEXT_CLICKABLE) != 0) {
                 if (oldIncludeForAccessibility != includeForAccessibility()) {
@@ -18083,7 +18151,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
     private static String printFlags(int flags) {
         String output = "";
         int numFlags = 0;
-        if ((flags & FOCUSABLE_MASK) == FOCUSABLE) {
+        if ((flags & FOCUSABLE) == FOCUSABLE) {
             output += "TAKES_FOCUS";
             numFlags++;
         }
@@ -24704,6 +24772,19 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
                 ViewConfiguration.getLongPressTooltipHideTimeout());
     }
 
+    private int getFocusableAttribute(TypedArray attributes) {
+        TypedValue val = new TypedValue();
+        if (attributes.getValue(com.android.internal.R.styleable.View_focusable, val)) {
+            if (val.type == TypedValue.TYPE_INT_BOOLEAN) {
+                return (val.data == 0 ? NOT_FOCUSABLE : FOCUSABLE);
+            } else {
+                return val.data;
+            }
+        } else {
+            return FOCUSABLE_AUTO;
+        }
+    }
+
     /**
      * @return The content view of the tooltip popup currently being shown, or null if the tooltip
      * is not showing.
index 3b7fe86..70b70bc 100644 (file)
@@ -150,7 +150,11 @@ public class RatingBar extends AbsSeekBar {
      */
     public void setIsIndicator(boolean isIndicator) {
         mIsUserSeekable = !isIndicator;
-        setFocusable(!isIndicator);
+        if (isIndicator) {
+            setFocusable(FOCUSABLE_AUTO);
+        } else {
+            setFocusable(FOCUSABLE);
+        }
     }
 
     /**
index 9139361..3822138 100644 (file)
@@ -357,9 +357,9 @@ public class SearchView extends LinearLayout implements CollapsibleActionView {
             setInputType(inputType);
         }
 
-        boolean focusable = true;
-        focusable = a.getBoolean(R.styleable.SearchView_focusable, focusable);
-        setFocusable(focusable);
+        if (getFocusable() == FOCUSABLE_AUTO) {
+            setFocusable(FOCUSABLE);
+        }
 
         a.recycle();
 
index 1ddf53d..4cc9034 100644 (file)
@@ -1492,26 +1492,22 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
         if (hint != null) setHint(hint);
 
         /*
-         * Views are not normally focusable unless specified to be.
+         * Views are not normally clickable unless specified to be.
          * However, TextViews that have input or movement methods *are*
-         * focusable by default.
+         * clickable by default. By setting clickable here, we implicitly set focusable as well
+         * if not overridden by the developer.
          */
         a = context.obtainStyledAttributes(
                 attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes);
-
-        boolean focusable = mMovement != null || getKeyListener() != null;
-        boolean clickable = focusable || isClickable();
-        boolean longClickable = focusable || isLongClickable();
+        boolean canInputOrMove = (mMovement != null || getKeyListener() != null);
+        boolean clickable = canInputOrMove || isClickable();
+        boolean longClickable = canInputOrMove || isLongClickable();
 
         n = a.getIndexCount();
         for (int i = 0; i < n; i++) {
             int attr = a.getIndex(i);
 
             switch (attr) {
-                case com.android.internal.R.styleable.View_focusable:
-                    focusable = a.getBoolean(attr, focusable);
-                    break;
-
                 case com.android.internal.R.styleable.View_clickable:
                     clickable = a.getBoolean(attr, clickable);
                     break;
@@ -1523,7 +1519,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
         }
         a.recycle();
 
-        setFocusable(focusable);
         setClickable(clickable);
         setLongClickable(longClickable);
 
@@ -1968,11 +1963,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
 
     private void fixFocusableAndClickableSettings() {
         if (mMovement != null || (mEditor != null && mEditor.mKeyListener != null)) {
-            setFocusable(true);
+            setFocusable(FOCUSABLE);
             setClickable(true);
             setLongClickable(true);
         } else {
-            setFocusable(false);
+            setFocusable(FOCUSABLE_AUTO);
             setClickable(false);
             setLongClickable(false);
         }
@@ -5936,7 +5931,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
 
         mEditor.mTextIsSelectable = selectable;
         setFocusableInTouchMode(selectable);
-        setFocusable(selectable);
+        setFocusable(FOCUSABLE_AUTO);
         setClickable(selectable);
         setLongClickable(selectable);
 
index c3cc97f..635ed56 100644 (file)
         <!-- Sets the padding, in pixels, of the end edge; see {@link android.R.attr#padding}. -->
         <attr name="paddingEnd" format="dimension" />
 
-        <!-- Boolean that controls whether a view can take focus.  By default the user can not
-             move focus to a view; by setting this attribute to true the view is
-             allowed to take focus.  This value does not impact the behavior of
+        <!-- Controls whether a view can take focus.  By default, this is "auto" which lets the
+             framework determine whether a user can move focus to a view.  By setting this attribute
+             to true the view is allowed to take focus. By setting it to "false" the view will not
+             take focus. This value does not impact the behavior of
              directly calling {@link android.view.View#requestFocus}, which will
              always request focus regardless of this view.  It only impacts where
              focus navigation will try to move focus. -->
-        <attr name="focusable" format="boolean" />
+        <attr name="focusable" format="boolean|enum">
+            <enum name="auto" value="0x00000010" />
+        </attr>
 
         <!-- Boolean that controls whether a view can take focus while in touch mode.
              If this is true for a view, that view can gain focus when clicked on, and can keep
         <attr name="queryBackground" format="reference" />
         <!-- Background for the section containing the action (e.g. voice search) -->
         <attr name="submitBackground" format="reference" />
-        <attr name="focusable" />
     </declare-styleable>
 
     <declare-styleable name="Switch">