Add support for automatic focusability determination

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
This commit is contained in:
Evan Rosky
2016-12-16 17:27:55 -08:00
parent 2cddef3329
commit 4c8c963ece
8 changed files with 147 additions and 50 deletions

View 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

View 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

View 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

View File

@@ -857,22 +857,39 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
*/
static boolean sCascadedDragDrop;
/**
* This view does not want keystrokes. Use with TAKES_FOCUS_MASK when
* calling setFlags.
*/
private static final int NOT_FOCUSABLE = 0x00000000;
/** @hide */
@IntDef({NOT_FOCUSABLE, FOCUSABLE, FOCUSABLE_AUTO})
@Retention(RetentionPolicy.SOURCE)
public @interface Focusable {}
/**
* This view wants 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 FOCUSABLE = 0x00000001;
public static final int NOT_FOCUSABLE = 0x00000000;
/**
* This view wants keystrokes.
* <p>
* Use with {@link #setFocusable(int)} and <a href="#attr_android:focusable">{@code
* android:focusable}.
*/
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.

View 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);
}
}
/**

View 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();

View 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);

View File

@@ -2272,13 +2272,16 @@
<!-- 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
@@ -7889,7 +7892,6 @@
<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">