Bug 854457 - Implement a11y actions/state and focus handling in TwoWayView (r=mfinkle)
This commit is contained in:
@@ -35,14 +35,17 @@ import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.TransitionDrawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.os.SystemClock;
|
||||
import android.support.v4.util.LongSparseArray;
|
||||
import android.support.v4.util.SparseArrayCompat;
|
||||
import android.support.v4.view.AccessibilityDelegateCompat;
|
||||
import android.support.v4.view.MotionEventCompat;
|
||||
import android.support.v4.view.VelocityTrackerCompat;
|
||||
import android.support.v4.view.ViewCompat;
|
||||
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
|
||||
import android.support.v4.widget.EdgeEffectCompat;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
@@ -56,6 +59,8 @@ import android.view.ViewConfiguration;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewParent;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
import android.view.accessibility.AccessibilityNodeInfo;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.Checkable;
|
||||
import android.widget.ListAdapter;
|
||||
@@ -140,6 +145,8 @@ public class TwoWayView extends AdapterView<ListAdapter> implements
|
||||
private final RecycleBin mRecycler;
|
||||
private AdapterDataSetObserver mDataSetObserver;
|
||||
|
||||
private boolean mItemsCanFocus;
|
||||
|
||||
final boolean[] mIsScrap = new boolean[1];
|
||||
|
||||
private boolean mDataChanged;
|
||||
@@ -160,6 +167,8 @@ public class TwoWayView extends AdapterView<ListAdapter> implements
|
||||
private float mTouchRemainderPos;
|
||||
private int mActivePointerId;
|
||||
|
||||
private final Rect mTempRect;
|
||||
|
||||
private Rect mTouchFrame;
|
||||
private int mMotionPosition;
|
||||
private CheckForTap mPendingCheckForTap;
|
||||
@@ -178,6 +187,9 @@ public class TwoWayView extends AdapterView<ListAdapter> implements
|
||||
private int mOverScroll;
|
||||
private final int mOverscrollDistance;
|
||||
|
||||
private boolean mDesiredFocusableState;
|
||||
private boolean mDesiredFocusableInTouchModeState;
|
||||
|
||||
private SelectionNotifier mSelectionNotifier;
|
||||
|
||||
private boolean mNeedSync;
|
||||
@@ -215,6 +227,11 @@ public class TwoWayView extends AdapterView<ListAdapter> implements
|
||||
|
||||
private View mEmptyView;
|
||||
|
||||
private ListItemAccessibilityDelegate mAccessibilityDelegate;
|
||||
|
||||
private int mLastAccessibilityScrollEventFromIndex;
|
||||
private int mLastAccessibilityScrollEventToIndex;
|
||||
|
||||
public interface OnScrollListener {
|
||||
|
||||
/**
|
||||
@@ -316,6 +333,10 @@ public class TwoWayView extends AdapterView<ListAdapter> implements
|
||||
|
||||
mIsVertical = true;
|
||||
|
||||
mItemsCanFocus = false;
|
||||
|
||||
mTempRect = new Rect();
|
||||
|
||||
mSelectorPosition = INVALID_POSITION;
|
||||
|
||||
mSelectorRect = new Rect();
|
||||
@@ -411,6 +432,27 @@ public class TwoWayView extends AdapterView<ListAdapter> implements
|
||||
return mItemMargin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that the views created by the ListAdapter can contain focusable
|
||||
* items.
|
||||
*
|
||||
* @param itemsCanFocus true if items can get focus, false otherwise
|
||||
*/
|
||||
public void setItemsCanFocus(boolean itemsCanFocus) {
|
||||
mItemsCanFocus = itemsCanFocus;
|
||||
if (!itemsCanFocus) {
|
||||
setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Whether the views created by the ListAdapter can contain focusable
|
||||
* items.
|
||||
*/
|
||||
public boolean getItemsCanFocus() {
|
||||
return mItemsCanFocus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the listener that will receive notifications every time the list scrolls.
|
||||
*
|
||||
@@ -778,10 +820,7 @@ public class TwoWayView extends AdapterView<ListAdapter> implements
|
||||
checkSelectionChanged();
|
||||
}
|
||||
|
||||
if (mEmptyView != null) {
|
||||
updateEmptyStatus();
|
||||
}
|
||||
|
||||
checkFocus();
|
||||
requestLayout();
|
||||
}
|
||||
|
||||
@@ -795,6 +834,11 @@ public class TwoWayView extends AdapterView<ListAdapter> implements
|
||||
return mFirstPosition + getChildCount() - 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return mItemCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPositionForView(View view) {
|
||||
View child = view;
|
||||
@@ -820,6 +864,83 @@ public class TwoWayView extends AdapterView<ListAdapter> implements
|
||||
return INVALID_POSITION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getFocusedRect(Rect r) {
|
||||
View view = getSelectedView();
|
||||
|
||||
if (view != null && view.getParent() == this) {
|
||||
// The focused rectangle of the selected view offset into the
|
||||
// coordinate space of this view.
|
||||
view.getFocusedRect(r);
|
||||
offsetDescendantRectToMyCoords(view, r);
|
||||
} else {
|
||||
super.getFocusedRect(r);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
|
||||
super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
|
||||
|
||||
if (gainFocus && mSelectedPosition < 0 && !isInTouchMode()) {
|
||||
if (!mIsAttached && mAdapter != null) {
|
||||
// Data may have changed while we were detached and it's valid
|
||||
// to change focus while detached. Refresh so we don't die.
|
||||
mDataChanged = true;
|
||||
mOldItemCount = mItemCount;
|
||||
mItemCount = mAdapter.getCount();
|
||||
}
|
||||
|
||||
resurrectSelection();
|
||||
}
|
||||
|
||||
final ListAdapter adapter = mAdapter;
|
||||
int closetChildIndex = INVALID_POSITION;
|
||||
int closestChildStart = 0;
|
||||
|
||||
if (adapter != null && gainFocus && previouslyFocusedRect != null) {
|
||||
previouslyFocusedRect.offset(getScrollX(), getScrollY());
|
||||
|
||||
// Don't cache the result of getChildCount or mFirstPosition here,
|
||||
// it could change in layoutChildren.
|
||||
if (adapter.getCount() < getChildCount() + mFirstPosition) {
|
||||
mLayoutMode = LAYOUT_NORMAL;
|
||||
layoutChildren();
|
||||
}
|
||||
|
||||
// Figure out which item should be selected based on previously
|
||||
// focused rect.
|
||||
Rect otherRect = mTempRect;
|
||||
int minDistance = Integer.MAX_VALUE;
|
||||
final int childCount = getChildCount();
|
||||
final int firstPosition = mFirstPosition;
|
||||
|
||||
for (int i = 0; i < childCount; i++) {
|
||||
// Only consider selectable views
|
||||
if (!adapter.isEnabled(firstPosition + i)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
View other = getChildAt(i);
|
||||
other.getDrawingRect(otherRect);
|
||||
offsetDescendantRectToMyCoords(other, otherRect);
|
||||
int distance = getDistance(previouslyFocusedRect, otherRect, direction);
|
||||
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
closetChildIndex = i;
|
||||
closestChildStart = (mIsVertical ? other.getTop() : other.getLeft());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (closetChildIndex >= 0) {
|
||||
setSelectionFromOffset(closetChildIndex + mFirstPosition, closestChildStart);
|
||||
} else {
|
||||
requestLayout();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
@@ -1465,6 +1586,7 @@ public class TwoWayView extends AdapterView<ListAdapter> implements
|
||||
case TOUCH_MODE_OVERSCROLL:
|
||||
mTouchMode = TOUCH_MODE_REST;
|
||||
reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
|
||||
break;
|
||||
}
|
||||
|
||||
cancelCheckForTap();
|
||||
@@ -1514,6 +1636,95 @@ public class TwoWayView extends AdapterView<ListAdapter> implements
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendAccessibilityEvent(int eventType) {
|
||||
// Since this class calls onScrollChanged even if the mFirstPosition and the
|
||||
// child count have not changed we will avoid sending duplicate accessibility
|
||||
// events.
|
||||
if (eventType == AccessibilityEvent.TYPE_VIEW_SCROLLED) {
|
||||
final int firstVisiblePosition = getFirstVisiblePosition();
|
||||
final int lastVisiblePosition = getLastVisiblePosition();
|
||||
|
||||
if (mLastAccessibilityScrollEventFromIndex == firstVisiblePosition
|
||||
&& mLastAccessibilityScrollEventToIndex == lastVisiblePosition) {
|
||||
return;
|
||||
} else {
|
||||
mLastAccessibilityScrollEventFromIndex = firstVisiblePosition;
|
||||
mLastAccessibilityScrollEventToIndex = lastVisiblePosition;
|
||||
}
|
||||
}
|
||||
|
||||
super.sendAccessibilityEvent(eventType);
|
||||
}
|
||||
|
||||
@Override
|
||||
@TargetApi(14)
|
||||
public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
|
||||
super.onInitializeAccessibilityEvent(event);
|
||||
event.setClassName(TwoWayView.class.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
@TargetApi(14)
|
||||
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
|
||||
super.onInitializeAccessibilityNodeInfo(info);
|
||||
info.setClassName(TwoWayView.class.getName());
|
||||
|
||||
AccessibilityNodeInfoCompat infoCompat = new AccessibilityNodeInfoCompat(info);
|
||||
|
||||
if (isEnabled()) {
|
||||
if (getFirstVisiblePosition() > 0) {
|
||||
infoCompat.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD);
|
||||
}
|
||||
|
||||
if (getLastVisiblePosition() < getCount() - 1) {
|
||||
infoCompat.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@TargetApi(16)
|
||||
public boolean performAccessibilityAction(int action, Bundle arguments) {
|
||||
if (super.performAccessibilityAction(action, arguments)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
|
||||
if (isEnabled() && getLastVisiblePosition() < getCount() - 1) {
|
||||
final int viewportSize;
|
||||
if (mIsVertical) {
|
||||
viewportSize = getHeight() - getPaddingTop() - getPaddingBottom();
|
||||
} else {
|
||||
viewportSize = getWidth() - getPaddingLeft() - getPaddingRight();
|
||||
}
|
||||
|
||||
// TODO: Use some form of smooth scroll instead
|
||||
trackMotionScroll(viewportSize);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD:
|
||||
if (isEnabled() && mFirstPosition > 0) {
|
||||
final int viewportSize;
|
||||
if (mIsVertical) {
|
||||
viewportSize = getHeight() - getPaddingTop() - getPaddingBottom();
|
||||
} else {
|
||||
viewportSize = getWidth() - getPaddingLeft() - getPaddingRight();
|
||||
}
|
||||
|
||||
// TODO: Use some form of smooth scroll instead
|
||||
trackMotionScroll(-viewportSize);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void initOrResetVelocityTracker() {
|
||||
if (mVelocityTracker == null) {
|
||||
mVelocityTracker = VelocityTracker.obtain();
|
||||
@@ -1711,7 +1922,70 @@ public class TwoWayView extends AdapterView<ListAdapter> implements
|
||||
}
|
||||
}
|
||||
|
||||
int findMotionRowOrColumn(int motionPos) {
|
||||
/**
|
||||
* What is the distance between the source and destination rectangles given the direction of
|
||||
* focus navigation between them? The direction basically helps figure out more quickly what is
|
||||
* self evident by the relationship between the rects...
|
||||
*
|
||||
* @param source the source rectangle
|
||||
* @param dest the destination rectangle
|
||||
* @param direction the direction
|
||||
* @return the distance between the rectangles
|
||||
*/
|
||||
private static int getDistance(Rect source, Rect dest, int direction) {
|
||||
int sX, sY; // source x, y
|
||||
int dX, dY; // dest x, y
|
||||
|
||||
switch (direction) {
|
||||
case View.FOCUS_RIGHT:
|
||||
sX = source.right;
|
||||
sY = source.top + source.height() / 2;
|
||||
dX = dest.left;
|
||||
dY = dest.top + dest.height() / 2;
|
||||
break;
|
||||
|
||||
case View.FOCUS_DOWN:
|
||||
sX = source.left + source.width() / 2;
|
||||
sY = source.bottom;
|
||||
dX = dest.left + dest.width() / 2;
|
||||
dY = dest.top;
|
||||
break;
|
||||
|
||||
case View.FOCUS_LEFT:
|
||||
sX = source.left;
|
||||
sY = source.top + source.height() / 2;
|
||||
dX = dest.right;
|
||||
dY = dest.top + dest.height() / 2;
|
||||
break;
|
||||
|
||||
case View.FOCUS_UP:
|
||||
sX = source.left + source.width() / 2;
|
||||
sY = source.top;
|
||||
dX = dest.left + dest.width() / 2;
|
||||
dY = dest.bottom;
|
||||
break;
|
||||
|
||||
case View.FOCUS_FORWARD:
|
||||
case View.FOCUS_BACKWARD:
|
||||
sX = source.right + source.width() / 2;
|
||||
sY = source.top + source.height() / 2;
|
||||
dX = dest.left + dest.width() / 2;
|
||||
dY = dest.top + dest.height() / 2;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new IllegalArgumentException("direction must be one of "
|
||||
+ "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT, "
|
||||
+ "FOCUS_FORWARD, FOCUS_BACKWARD}.");
|
||||
}
|
||||
|
||||
int deltaX = dX - sX;
|
||||
int deltaY = dY - sY;
|
||||
|
||||
return deltaY * deltaY + deltaX * deltaX;
|
||||
}
|
||||
|
||||
private int findMotionRowOrColumn(int motionPos) {
|
||||
int childCount = getChildCount();
|
||||
if (childCount == 0) {
|
||||
return INVALID_POSITION;
|
||||
@@ -2193,6 +2467,7 @@ public class TwoWayView extends AdapterView<ListAdapter> implements
|
||||
post(mSelectionNotifier);
|
||||
} else {
|
||||
fireOnSelected();
|
||||
performAccessibilityActionsOnSelected();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2212,6 +2487,14 @@ public class TwoWayView extends AdapterView<ListAdapter> implements
|
||||
}
|
||||
}
|
||||
|
||||
private void performAccessibilityActionsOnSelected() {
|
||||
final int position = getSelectedItemPosition();
|
||||
if (position >= 0) {
|
||||
// We fire selection events here not in View
|
||||
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
|
||||
}
|
||||
}
|
||||
|
||||
private int lookForSelectablePosition(int position) {
|
||||
return lookForSelectablePosition(position, true);
|
||||
}
|
||||
@@ -2503,6 +2786,8 @@ public class TwoWayView extends AdapterView<ListAdapter> implements
|
||||
int index = 0;
|
||||
int delta = 0;
|
||||
|
||||
View focusLayoutRestoreView = null;
|
||||
|
||||
View selected = null;
|
||||
View oldSelected = null;
|
||||
View newSelected = null;
|
||||
@@ -2562,6 +2847,9 @@ public class TwoWayView extends AdapterView<ListAdapter> implements
|
||||
|
||||
setSelectedPositionInt(mNextSelectedPosition);
|
||||
|
||||
// Reset the focus restoration
|
||||
View focusLayoutRestoreDirectChild = null;
|
||||
|
||||
// Pull all children into the RecycleBin.
|
||||
// These views will be reused if possible
|
||||
final int firstPosition = mFirstPosition;
|
||||
@@ -2575,6 +2863,32 @@ public class TwoWayView extends AdapterView<ListAdapter> implements
|
||||
recycleBin.fillActiveViews(childCount, firstPosition);
|
||||
}
|
||||
|
||||
// Take focus back to us temporarily to avoid the eventual
|
||||
// call to clear focus when removing the focused child below
|
||||
// from messing things up when ViewAncestor assigns focus back
|
||||
// to someone else.
|
||||
final View focusedChild = getFocusedChild();
|
||||
if (focusedChild != null) {
|
||||
// We can remember the focused view to restore after relayout if the
|
||||
// data hasn't changed, or if the focused position is a header or footer.
|
||||
if (!dataChanged) {
|
||||
focusLayoutRestoreDirectChild = focusedChild;
|
||||
|
||||
// Remember the specific view that had focus
|
||||
focusLayoutRestoreView = findFocus();
|
||||
if (focusLayoutRestoreView != null) {
|
||||
// Tell it we are going to mess with it
|
||||
focusLayoutRestoreView.onStartTemporaryDetach();
|
||||
}
|
||||
}
|
||||
|
||||
requestFocus();
|
||||
}
|
||||
|
||||
// FIXME: We need a way to save current accessibility focus here
|
||||
// so that it can be restored after we re-attach the children on each
|
||||
// layout round.
|
||||
|
||||
detachAllViewsFromParent();
|
||||
|
||||
switch (mLayoutMode) {
|
||||
@@ -2644,7 +2958,29 @@ public class TwoWayView extends AdapterView<ListAdapter> implements
|
||||
recycleBin.scrapActiveViews();
|
||||
|
||||
if (selected != null) {
|
||||
positionSelector(INVALID_POSITION, selected);
|
||||
if (mItemsCanFocus && hasFocus() && !selected.hasFocus()) {
|
||||
final boolean focusWasTaken = (selected == focusLayoutRestoreDirectChild &&
|
||||
focusLayoutRestoreView != null &&
|
||||
focusLayoutRestoreView.requestFocus()) || selected.requestFocus();
|
||||
|
||||
if (!focusWasTaken) {
|
||||
// Selected item didn't take focus, fine, but still want
|
||||
// to make sure something else outside of the selected view
|
||||
// has focus
|
||||
final View focused = getFocusedChild();
|
||||
if (focused != null) {
|
||||
focused.clearFocus();
|
||||
}
|
||||
|
||||
positionSelector(INVALID_POSITION, selected);
|
||||
} else {
|
||||
selected.setSelected(false);
|
||||
mSelectorRect.setEmpty();
|
||||
}
|
||||
} else {
|
||||
positionSelector(INVALID_POSITION, selected);
|
||||
}
|
||||
|
||||
mSelectedStart = (mIsVertical ? selected.getTop() : selected.getLeft());
|
||||
} else {
|
||||
if (mTouchMode > TOUCH_MODE_DOWN && mTouchMode < TOUCH_MODE_DRAGGING) {
|
||||
@@ -2657,6 +2993,19 @@ public class TwoWayView extends AdapterView<ListAdapter> implements
|
||||
mSelectedStart = 0;
|
||||
mSelectorRect.setEmpty();
|
||||
}
|
||||
|
||||
// Even if there is not selected position, we may need to restore
|
||||
// focus (i.e. something focusable in touch mode)
|
||||
if (hasFocus() && focusLayoutRestoreView != null) {
|
||||
focusLayoutRestoreView.requestFocus();
|
||||
}
|
||||
}
|
||||
|
||||
// Tell focus view we are done mucking with it, if it is still in
|
||||
// our view hierarchy.
|
||||
if (focusLayoutRestoreView != null
|
||||
&& focusLayoutRestoreView.getWindowToken() != null) {
|
||||
focusLayoutRestoreView.onFinishTemporaryDetach();
|
||||
}
|
||||
|
||||
mLayoutMode = LAYOUT_NORMAL;
|
||||
@@ -2998,7 +3347,6 @@ public class TwoWayView extends AdapterView<ListAdapter> implements
|
||||
mSelectorPosition = INVALID_POSITION;
|
||||
|
||||
checkSelectionChanged();
|
||||
|
||||
}
|
||||
|
||||
private int reconcileSelectedPosition() {
|
||||
@@ -3517,9 +3865,9 @@ public class TwoWayView extends AdapterView<ListAdapter> implements
|
||||
return selectedView;
|
||||
}
|
||||
|
||||
private View fillSpecific(int position, int top) {
|
||||
private View fillSpecific(int position, int offset) {
|
||||
final boolean tempIsSelected = (position == mSelectedPosition);
|
||||
View temp = makeAndAddView(position, top, true, tempIsSelected);
|
||||
View temp = makeAndAddView(position, offset, true, tempIsSelected);
|
||||
|
||||
// Possibly changed again in fillBefore if we add rows above this one.
|
||||
mFirstPosition = position;
|
||||
@@ -3667,7 +4015,7 @@ public class TwoWayView extends AdapterView<ListAdapter> implements
|
||||
private void correctTooHigh(int childCount) {
|
||||
// First see if the last item is visible. If it is not, it is OK for the
|
||||
// top of the list to be pushed up.
|
||||
int lastPosition = mFirstPosition + childCount - 1;
|
||||
final int lastPosition = mFirstPosition + childCount - 1;
|
||||
if (lastPosition != mItemCount - 1 || childCount == 0) {
|
||||
return;
|
||||
}
|
||||
@@ -3901,11 +4249,11 @@ public class TwoWayView extends AdapterView<ListAdapter> implements
|
||||
return INVALID_POSITION;
|
||||
}
|
||||
|
||||
View obtainView(int position, boolean[] isScrap) {
|
||||
@TargetApi(16)
|
||||
private View obtainView(int position, boolean[] isScrap) {
|
||||
isScrap[0] = false;
|
||||
View scrapView;
|
||||
|
||||
scrapView = mRecycler.getTransientStateView(position);
|
||||
View scrapView = mRecycler.getTransientStateView(position);
|
||||
if (scrapView != null) {
|
||||
return scrapView;
|
||||
}
|
||||
@@ -3925,6 +4273,10 @@ public class TwoWayView extends AdapterView<ListAdapter> implements
|
||||
child = mAdapter.getView(position, null, this);
|
||||
}
|
||||
|
||||
if (ViewCompat.getImportantForAccessibility(child) == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
|
||||
ViewCompat.setImportantForAccessibility(child, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
|
||||
}
|
||||
|
||||
if (mHasStableIds) {
|
||||
LayoutParams lp = (LayoutParams) child.getLayoutParams();
|
||||
|
||||
@@ -3939,6 +4291,12 @@ public class TwoWayView extends AdapterView<ListAdapter> implements
|
||||
child.setLayoutParams(lp);
|
||||
}
|
||||
|
||||
if (mAccessibilityDelegate == null) {
|
||||
mAccessibilityDelegate = new ListItemAccessibilityDelegate();
|
||||
}
|
||||
|
||||
ViewCompat.setAccessibilityDelegate(child, mAccessibilityDelegate);
|
||||
|
||||
return child;
|
||||
}
|
||||
|
||||
@@ -4475,6 +4833,7 @@ public class TwoWayView extends AdapterView<ListAdapter> implements
|
||||
return null;
|
||||
}
|
||||
|
||||
@TargetApi(14)
|
||||
void addScrapView(View scrap, int position) {
|
||||
LayoutParams lp = (LayoutParams) scrap.getLayoutParams();
|
||||
if (lp == null) {
|
||||
@@ -4505,11 +4864,18 @@ public class TwoWayView extends AdapterView<ListAdapter> implements
|
||||
mScrapViews[viewType].add(scrap);
|
||||
}
|
||||
|
||||
// FIXME: Unfortunately, ViewCompat.setAccessibilityDelegate() doesn't accept
|
||||
// null delegates.
|
||||
if (Build.VERSION.SDK_INT >= 14) {
|
||||
scrap.setAccessibilityDelegate(null);
|
||||
}
|
||||
|
||||
if (mRecyclerListener != null) {
|
||||
mRecyclerListener.onMovedToScrapHeap(scrap);
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(14)
|
||||
void scrapActiveViews() {
|
||||
final View[] activeViews = mActiveViews;
|
||||
final boolean multipleScraps = (mViewTypeCount > 1);
|
||||
@@ -4547,6 +4913,12 @@ public class TwoWayView extends AdapterView<ListAdapter> implements
|
||||
lp.scrappedFromPosition = mFirstActivePosition + i;
|
||||
scrapViews.add(victim);
|
||||
|
||||
// FIXME: Unfortunately, ViewCompat.setAccessibilityDelegate() doesn't accept
|
||||
// null delegates.
|
||||
if (Build.VERSION.SDK_INT >= 14) {
|
||||
victim.setAccessibilityDelegate(null);
|
||||
}
|
||||
|
||||
if (mRecyclerListener != null) {
|
||||
mRecyclerListener.onMovedToScrapHeap(victim);
|
||||
}
|
||||
@@ -4625,8 +4997,50 @@ public class TwoWayView extends AdapterView<ListAdapter> implements
|
||||
updateEmptyStatus();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFocusable(boolean focusable) {
|
||||
final ListAdapter adapter = getAdapter();
|
||||
final boolean empty = (adapter == null || adapter.getCount() == 0);
|
||||
|
||||
mDesiredFocusableState = focusable;
|
||||
if (!focusable) {
|
||||
mDesiredFocusableInTouchModeState = false;
|
||||
}
|
||||
|
||||
super.setFocusable(focusable && !empty);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFocusableInTouchMode(boolean focusable) {
|
||||
final ListAdapter adapter = getAdapter();
|
||||
final boolean empty = (adapter == null || adapter.getCount() == 0);
|
||||
|
||||
mDesiredFocusableInTouchModeState = focusable;
|
||||
if (focusable) {
|
||||
mDesiredFocusableState = true;
|
||||
}
|
||||
|
||||
super.setFocusableInTouchMode(focusable && !empty);
|
||||
}
|
||||
|
||||
private void checkFocus() {
|
||||
final ListAdapter adapter = getAdapter();
|
||||
final boolean focusable = (adapter != null && adapter.getCount() > 0);
|
||||
|
||||
// The order in which we set focusable in touch mode/focusable may matter
|
||||
// for the client, see View.setFocusableInTouchMode() comments for more
|
||||
// details
|
||||
super.setFocusableInTouchMode(focusable && mDesiredFocusableInTouchModeState);
|
||||
super.setFocusable(focusable && mDesiredFocusableState);
|
||||
|
||||
if (mEmptyView != null) {
|
||||
updateEmptyStatus();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateEmptyStatus() {
|
||||
final boolean isEmpty = (mAdapter == null || mAdapter.isEmpty());
|
||||
|
||||
if (isEmpty) {
|
||||
if (mEmptyView != null) {
|
||||
mEmptyView.setVisibility(View.VISIBLE);
|
||||
@@ -4641,7 +5055,7 @@ public class TwoWayView extends AdapterView<ListAdapter> implements
|
||||
// Force one here to make sure that the state of the list matches
|
||||
// the state of the adapter.
|
||||
if (mDataChanged) {
|
||||
this.onLayout(false, getLeft(), getTop(), getRight(), getBottom());
|
||||
onLayout(false, getLeft(), getTop(), getRight(), getBottom());
|
||||
}
|
||||
} else {
|
||||
if (mEmptyView != null) {
|
||||
@@ -4671,10 +5085,7 @@ public class TwoWayView extends AdapterView<ListAdapter> implements
|
||||
rememberSyncState();
|
||||
}
|
||||
|
||||
if (mEmptyView != null) {
|
||||
updateEmptyStatus();
|
||||
}
|
||||
|
||||
checkFocus();
|
||||
requestLayout();
|
||||
}
|
||||
|
||||
@@ -4700,10 +5111,7 @@ public class TwoWayView extends AdapterView<ListAdapter> implements
|
||||
|
||||
mNeedSync = false;
|
||||
|
||||
if (mEmptyView != null) {
|
||||
updateEmptyStatus();
|
||||
}
|
||||
|
||||
checkFocus();
|
||||
requestLayout();
|
||||
}
|
||||
}
|
||||
@@ -4811,6 +5219,7 @@ public class TwoWayView extends AdapterView<ListAdapter> implements
|
||||
}
|
||||
} else {
|
||||
fireOnSelected();
|
||||
performAccessibilityActionsOnSelected();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4926,4 +5335,93 @@ public class TwoWayView extends AdapterView<ListAdapter> implements
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ListItemAccessibilityDelegate extends AccessibilityDelegateCompat {
|
||||
@Override
|
||||
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
|
||||
super.onInitializeAccessibilityNodeInfo(host, info);
|
||||
|
||||
final int position = getPositionForView(host);
|
||||
final ListAdapter adapter = getAdapter();
|
||||
|
||||
// Cannot perform actions on invalid items
|
||||
if (position == INVALID_POSITION || adapter == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Cannot perform actions on disabled items
|
||||
if (!isEnabled() || !adapter.isEnabled(position)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (position == getSelectedItemPosition()) {
|
||||
info.setSelected(true);
|
||||
info.addAction(AccessibilityNodeInfoCompat.ACTION_CLEAR_SELECTION);
|
||||
} else {
|
||||
info.addAction(AccessibilityNodeInfoCompat.ACTION_SELECT);
|
||||
}
|
||||
|
||||
if (isClickable()) {
|
||||
info.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
|
||||
info.setClickable(true);
|
||||
}
|
||||
|
||||
if (isLongClickable()) {
|
||||
info.addAction(AccessibilityNodeInfoCompat.ACTION_LONG_CLICK);
|
||||
info.setLongClickable(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean performAccessibilityAction(View host, int action, Bundle arguments) {
|
||||
if (super.performAccessibilityAction(host, action, arguments)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final int position = getPositionForView(host);
|
||||
final ListAdapter adapter = getAdapter();
|
||||
|
||||
// Cannot perform actions on invalid items
|
||||
if (position == INVALID_POSITION || adapter == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cannot perform actions on disabled items
|
||||
if (!isEnabled() || !adapter.isEnabled(position)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final long id = getItemIdAtPosition(position);
|
||||
|
||||
switch (action) {
|
||||
case AccessibilityNodeInfoCompat.ACTION_CLEAR_SELECTION:
|
||||
if (getSelectedItemPosition() == position) {
|
||||
setSelection(INVALID_POSITION);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
case AccessibilityNodeInfoCompat.ACTION_SELECT:
|
||||
if (getSelectedItemPosition() != position) {
|
||||
setSelection(position);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
case AccessibilityNodeInfoCompat.ACTION_CLICK:
|
||||
if (isClickable()) {
|
||||
return performItemClick(host, position, id);
|
||||
}
|
||||
return false;
|
||||
|
||||
case AccessibilityNodeInfoCompat.ACTION_LONG_CLICK:
|
||||
if (isLongClickable()) {
|
||||
return performLongPress(host, position, id);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user