diff options
author | Alan Viverette <alanv@google.com> | 2013-04-18 13:59:48 -0700 |
---|---|---|
committer | Alan Viverette <alanv@google.com> | 2013-04-18 13:59:48 -0700 |
commit | 090a46d6ee500d3674401fd3de48cd0f12ce7186 (patch) | |
tree | 364c70f7333242d902bc5ff77f32c89ef68846c8 /src/com | |
parent | e4efb8a14871c227368bc79fd043e761fc2b2f1f (diff) | |
download | datetimepicker-090a46d6ee500d3674401fd3de48cd0f12ce7186.tar.gz |
Add a virtual node provider to the date picker.
Adds a virtual node provider using TouchExplorationHelper, refactors
SimpleMonthView to handle its own touch events, and modifies
DayPickerView to attempt to save and restore accessibility focus during
a layout operation.
Change-Id: Iefe2ffe7185ec2ed8557fb94ea783e32030ed655
Diffstat (limited to 'src/com')
4 files changed, 787 insertions, 26 deletions
diff --git a/src/com/android/datetimepicker/date/DayPickerView.java b/src/com/android/datetimepicker/date/DayPickerView.java index 1b0e871..fcd1d8c 100644 --- a/src/com/android/datetimepicker/date/DayPickerView.java +++ b/src/com/android/datetimepicker/date/DayPickerView.java @@ -347,4 +347,59 @@ public class DayPickerView extends ListView implements OnScrollListener, OnDateC public void onDateChanged() { goTo(mController.getSelectedDay(), false, true, true); } + + /** + * Attempts to return the date that has accessibility focus. + * + * @return The date that has accessibility focus, or {@code null} if no date + * has focus. + */ + private CalendarDay findAccessibilityFocus() { + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + if (child instanceof SimpleMonthView) { + final CalendarDay focus = ((SimpleMonthView) child).getAccessibilityFocus(); + if (focus != null) { + // Clear focus to avoid ListView bug in Jelly Bean MR1. + ((SimpleMonthView) child).clearAccessibilityFocus(); + return focus; + } + } + } + + return null; + } + + /** + * Attempts to restore accessibility focus to a given date. No-op if + * {@code day} is {@code null}. + * + * @param day The date that should receive accessibility focus + * @return {@code true} if focus was restored + */ + private boolean restoreAccessibilityFocus(CalendarDay day) { + if (day == null) { + return false; + } + + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + if (child instanceof SimpleMonthView) { + if (((SimpleMonthView) child).restoreAccessibilityFocus(day)) { + return true; + } + } + } + + return false; + } + + @Override + protected void layoutChildren() { + final CalendarDay focusedDay = findAccessibilityFocus(); + super.layoutChildren(); + restoreAccessibilityFocus(focusedDay); + } } diff --git a/src/com/android/datetimepicker/date/SimpleMonthAdapter.java b/src/com/android/datetimepicker/date/SimpleMonthAdapter.java index 6d3ef7a..84be210 100644 --- a/src/com/android/datetimepicker/date/SimpleMonthAdapter.java +++ b/src/com/android/datetimepicker/date/SimpleMonthAdapter.java @@ -18,28 +18,26 @@ package com.android.datetimepicker.date; import android.content.Context; import android.util.Log; -import android.view.GestureDetector; -import android.view.MotionEvent; import android.view.View; -import android.view.View.OnTouchListener; import android.view.ViewGroup; import android.widget.AbsListView.LayoutParams; import android.widget.BaseAdapter; +import com.android.datetimepicker.date.SimpleMonthView.OnDayClickListener; + import java.util.Calendar; import java.util.HashMap; /** * An adapter for a list of {@link SimpleMonthView} items. */ -public class SimpleMonthAdapter extends BaseAdapter implements OnTouchListener { +public class SimpleMonthAdapter extends BaseAdapter implements OnDayClickListener { private static final String TAG = "SimpleMonthAdapter"; private final Context mContext; private final DatePickerController mController; - private GestureDetector mGestureDetector; private CalendarDay mSelectedDay; protected static int WEEK_7_OVERHANG_HEIGHT = 7; @@ -121,7 +119,6 @@ public class SimpleMonthAdapter extends BaseAdapter implements OnTouchListener { * Set up the gesture detector and selected time */ protected void init() { - mGestureDetector = new GestureDetector(mContext, new CalendarGestureListener()); mSelectedDay = new CalendarDay(System.currentTimeMillis()); } @@ -156,7 +153,7 @@ public class SimpleMonthAdapter extends BaseAdapter implements OnTouchListener { LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); v.setLayoutParams(params); v.setClickable(true); - v.setOnTouchListener(this); + v.setOnDayClickListener(this); } if (drawingParams == null) { drawingParams = new HashMap<String, Integer>(); @@ -190,15 +187,10 @@ public class SimpleMonthAdapter extends BaseAdapter implements OnTouchListener { } @Override - public boolean onTouch(View v, MotionEvent event) { - if (mGestureDetector.onTouchEvent(event)) { - CalendarDay day = ((SimpleMonthView) v).getDayFromLocation(event.getX(), event.getY()); - if (day != null) { - onDayTapped(day); - } - return true; + public void onDayClick(SimpleMonthView view, CalendarDay day) { + if (day != null) { + onDayTapped(day); } - return false; } /** @@ -211,15 +203,4 @@ public class SimpleMonthAdapter extends BaseAdapter implements OnTouchListener { mController.onDayOfMonthSelected(day.year, day.month, day.day); setSelectedDay(day); } - - /** - * This is here so we can identify single tap events and set the selected - * day correctly - */ - protected class CalendarGestureListener extends GestureDetector.SimpleOnGestureListener { - @Override - public boolean onSingleTapUp(MotionEvent e) { - return true; - } - } } diff --git a/src/com/android/datetimepicker/date/SimpleMonthView.java b/src/com/android/datetimepicker/date/SimpleMonthView.java index c3a4346..32cffef 100644 --- a/src/com/android/datetimepicker/date/SimpleMonthView.java +++ b/src/com/android/datetimepicker/date/SimpleMonthView.java @@ -22,17 +22,27 @@ import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Paint.Align; import android.graphics.Paint.Style; +import android.graphics.Rect; import android.graphics.Typeface; +import android.os.Bundle; +import android.support.v4.view.ViewCompat; +import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; import android.text.format.Time; +import android.util.SparseArray; +import android.view.MotionEvent; import android.view.View; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; import com.android.datetimepicker.R; import com.android.datetimepicker.Utils; import com.android.datetimepicker.date.SimpleMonthAdapter.CalendarDay; +import com.googlecode.eyesfree.utils.TouchExplorationHelper; import java.security.InvalidParameterException; import java.util.Calendar; import java.util.HashMap; +import java.util.List; import java.util.Locale; /** @@ -152,9 +162,15 @@ public class SimpleMonthView extends View { private final Calendar mCalendar; private final Calendar mDayLabelCalendar; + private final MonthViewNodeProvider mNodeProvider; private int mNumRows = DEFAULT_NUM_ROWS; + // Optional listener for handling day click actions + private OnDayClickListener mOnDayClickListener; + // Whether to prevent setting the accessibility delegate + private boolean mLockAccessibilityDelegate; + protected int mDayTextColor; protected int mTodayNumberColor; protected int mMonthTitleColor; @@ -185,10 +201,52 @@ public class SimpleMonthView extends View { mRowHeight = (res.getDimensionPixelOffset(R.dimen.date_picker_view_animator_height) - MONTH_HEADER_SIZE) / MAX_NUM_ROWS; + + // Set up accessibility components. + mNodeProvider = new MonthViewNodeProvider(context, this); + ViewCompat.setAccessibilityDelegate(this, mNodeProvider.getAccessibilityDelegate()); + ViewCompat.setImportantForAccessibility(this, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); + mLockAccessibilityDelegate = true; + // Sets up any standard paints that will be used initView(); } + @Override + public void setAccessibilityDelegate(AccessibilityDelegate delegate) { + // Workaround for a JB MR1 issue where accessibility delegates on + // top-level ListView items are overwritten. + if (!mLockAccessibilityDelegate) { + super.setAccessibilityDelegate(delegate); + } + } + + public void setOnDayClickListener(OnDayClickListener listener) { + mOnDayClickListener = listener; + } + + @Override + public boolean onHoverEvent(MotionEvent event) { + // First right-of-refusal goes the touch exploration helper. + if (mNodeProvider.onHover(this, event)) { + return true; + } + return super.onHoverEvent(event); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_UP: + final CalendarDay day = getDayFromLocation(event.getX(), event.getY()); + if (day != null) { + onDayClick(day); + } + break; + } + return true; + } + /** * Sets up the text and style properties for painting. Override this if you * want to use a different paint. @@ -301,6 +359,9 @@ public class SimpleMonthView extends View { } } mNumRows = calculateNumRows(); + + // Invalidate cached accessibility information. + mNodeProvider.invalidateParent(); } public void reuse() { @@ -330,6 +391,9 @@ public class SimpleMonthView extends View { @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { mWidth = w; + + // Invalidate cached accessibility information. + mNodeProvider.invalidateParent(); } private void drawMonthTitle(Canvas canvas) { @@ -421,4 +485,190 @@ public class SimpleMonthView extends View { return new CalendarDay(mYear, mMonth, day); } + /** + * Called when the user clicks on a day. Handles callbacks to the + * {@link OnDayClickListener} if one is set. + * + * @param day A time object representing the day that was clicked + */ + private void onDayClick(CalendarDay day) { + if (mOnDayClickListener != null) { + mOnDayClickListener.onDayClick(this, day); + } + + // This is a no-op if accessibility is turned off. + mNodeProvider.sendEventForItem(day, AccessibilityEvent.TYPE_VIEW_CLICKED); + } + + /** + * @return The date that has accessibility focus, or {@code null} if no date + * has focus + */ + public CalendarDay getAccessibilityFocus() { + return mNodeProvider.getFocusedItem(); + } + + /** + * Clears accessibility focus within the view. No-op if the view does not + * contain accessibility focus. + */ + public void clearAccessibilityFocus() { + mNodeProvider.clearFocusedItem(); + } + + /** + * Attempts to restore accessibility focus to the specified date. + * + * @param day The date which should receive focus + * @return {@code false} if the date is not valid for this month view, or + * {@code true} if the date received focus + */ + public boolean restoreAccessibilityFocus(CalendarDay day) { + if ((day.year != mYear) || (day.month != mMonth) || (day.day > mNumCells)) { + return false; + } + + mNodeProvider.setFocusedItem(day); + return true; + } + + /** + * Provides a virtual view hierarchy for interfacing with an accessibility + * service. + */ + private class MonthViewNodeProvider extends TouchExplorationHelper<CalendarDay> { + private final SparseArray<CalendarDay> mCachedItems = new SparseArray<CalendarDay>(); + private final Rect mTempRect = new Rect(); + + public MonthViewNodeProvider(Context context, View parent) { + super(context, parent); + } + + @Override + public void invalidateItem(CalendarDay item) { + super.invalidateItem(item); + mCachedItems.delete(getIdForItem(item)); + } + + @Override + public void invalidateParent() { + super.invalidateParent(); + mCachedItems.clear(); + } + + @Override + protected boolean performActionForItem(CalendarDay item, int action, Bundle arguments) { + switch (action) { + case AccessibilityNodeInfo.ACTION_CLICK: + onDayClick(item); + return true; + } + + return false; + } + + @Override + protected void populateEventForItem(CalendarDay item, AccessibilityEvent event) { + event.setContentDescription(getItemDescription(item)); + } + + @Override + protected void populateNodeForItem(CalendarDay item, AccessibilityNodeInfoCompat node) { + getItemBounds(item, mTempRect); + + node.setContentDescription(getItemDescription(item)); + node.setBoundsInParent(mTempRect); + node.addAction(AccessibilityNodeInfo.ACTION_CLICK); + + if (item.day == mSelectedDay) { + node.setSelected(true); + } + } + + @Override + protected void getVisibleItems(List<CalendarDay> items) { + // TODO: Optimize, only return items visible within parent bounds. + for (int day = 1; day <= mNumCells; day++) { + items.add(getItemForId(day)); + } + } + + @Override + protected CalendarDay getItemAt(float x, float y) { + return getDayFromLocation(x, y); + } + + @Override + protected int getIdForItem(CalendarDay item) { + return item.day; + } + + @Override + protected CalendarDay getItemForId(int id) { + if ((id < 1) || (id > mNumCells)) { + return null; + } + + final CalendarDay item; + if (mCachedItems.indexOfKey(id) >= 0) { + item = mCachedItems.get(id); + } else { + item = new CalendarDay(mYear, mMonth, id); + mCachedItems.put(id, item); + } + + return item; + } + + /** + * Calculates the bounding rectangle of a given time object. + * + * @param item The time object to calculate bounds for + * @param rect The rectangle in which to store the bounds + */ + private void getItemBounds(CalendarDay item, Rect rect) { + final int offsetX = mPadding; + final int offsetY = MONTH_HEADER_SIZE; + final int cellHeight = mRowHeight; + final int cellWidth = ((mWidth - (2 * mPadding)) / mNumDays); + final int index = ((item.day - 1) + findDayOffset()); + final int row = (index / mNumDays); + final int column = (index % mNumDays); + final int x = (offsetX + (column * cellWidth)); + final int y = (offsetY + (row * cellHeight)); + + rect.set(x, y, (x + cellWidth), (y + cellHeight)); + } + + /** + * Generates a description for a given time object. Since this + * description will be spoken, the components are ordered by descending + * specificity as DAY MONTH YEAR. + * + * @param item The time object to generate a description for + * @return A description of the time object + */ + private CharSequence getItemDescription(CalendarDay item) { + final StringBuffer sbuf = new StringBuffer(); + sbuf.append(String.format("%d", item.day)); + sbuf.append(" "); + sbuf.append(mCalendar.getDisplayName(Calendar.MONTH, Calendar.LONG, + Locale.getDefault())); + sbuf.append(" "); + sbuf.append(String.format("%d", mYear)); + + if (item.day == mSelectedDay) { + return getContext().getString(R.string.item_is_selected, sbuf); + } + + return sbuf; + } + } + + /** + * Handles callbacks when the user clicks on a time object. + */ + public interface OnDayClickListener { + public void onDayClick(SimpleMonthView view, CalendarDay day); + } } diff --git a/src/com/googlecode/eyesfree/utils/TouchExplorationHelper.java b/src/com/googlecode/eyesfree/utils/TouchExplorationHelper.java new file mode 100644 index 0000000..b9df653 --- /dev/null +++ b/src/com/googlecode/eyesfree/utils/TouchExplorationHelper.java @@ -0,0 +1,475 @@ +/* + * Copyright (C) 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.googlecode.eyesfree.utils; + +import android.content.Context; +import android.graphics.Rect; +import android.os.Bundle; +import android.support.v4.view.AccessibilityDelegateCompat; +import android.support.v4.view.ViewCompat; +import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; +import android.support.v4.view.accessibility.AccessibilityNodeProviderCompat; +import android.support.v4.view.accessibility.AccessibilityRecordCompat; +import android.text.TextUtils; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; + +import java.util.LinkedList; +import java.util.List; + +public abstract class TouchExplorationHelper<T> extends AccessibilityNodeProviderCompat + implements View.OnHoverListener { + /** Virtual node identifier value for invalid nodes. */ + public static final int INVALID_ID = Integer.MIN_VALUE; + + private final Rect mTempScreenRect = new Rect(); + private final Rect mTempParentRect = new Rect(); + private final Rect mTempVisibleRect = new Rect(); + private final int[] mTempGlobalRect = new int[2]; + + private final AccessibilityManager mManager; + + private View mParentView; + private int mFocusedItemId = INVALID_ID; + private T mCurrentItem = null; + + /** + * Constructs a new touch exploration helper. + * + * @param context The parent context. + */ + public TouchExplorationHelper(Context context, View parentView) { + mManager = (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE); + mParentView = parentView; + } + + /** + * @return The current accessibility focused item, or {@code null} if no + * item is focused. + */ + public T getFocusedItem() { + return getItemForId(mFocusedItemId); + } + + /** + * Clears the current accessibility focused item. + */ + public void clearFocusedItem() { + final int itemId = mFocusedItemId; + if (itemId == INVALID_ID) { + return; + } + + performAction(itemId, AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS, null); + } + + /** + * Requests accessibility focus be placed on the specified item. + * + * @param item The item to place focus on. + */ + public void setFocusedItem(T item) { + final int itemId = getIdForItem(item); + if (itemId == INVALID_ID) { + return; + } + + performAction(itemId, AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS, null); + } + + /** + * Invalidates cached information about the parent view. + * <p> + * You <b>must</b> call this method after adding or removing items from the + * parent view. + * </p> + */ + public void invalidateParent() { + mParentView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); + } + + /** + * Invalidates cached information for a particular item. + * <p> + * You <b>must</b> call this method when any of the properties set in + * {@link #populateNodeForItem(Object, AccessibilityNodeInfoCompat)} have + * changed. + * </p> + * + * @param item + */ + public void invalidateItem(T item) { + sendEventForItem(item, AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); + } + + /** + * Populates an event of the specified type with information about an item + * and attempts to send it up through the view hierarchy. + * + * @param item The item for which to send an event. + * @param eventType The type of event to send. + * @return {@code true} if the event was sent successfully. + */ + public boolean sendEventForItem(T item, int eventType) { + if (!mManager.isEnabled()) { + return false; + } + + final AccessibilityEvent event = getEventForItem(item, eventType); + final ViewGroup group = (ViewGroup) mParentView.getParent(); + + return group.requestSendAccessibilityEvent(mParentView, event); + } + + @Override + public AccessibilityNodeInfoCompat createAccessibilityNodeInfo(int virtualViewId) { + if (virtualViewId == View.NO_ID) { + return getNodeForParent(); + } + + final T item = getItemForId(virtualViewId); + if (item == null) { + return null; + } + + final AccessibilityNodeInfoCompat node = AccessibilityNodeInfoCompat.obtain(); + populateNodeForItemInternal(item, node); + return node; + } + + @Override + public boolean performAction(int virtualViewId, int action, Bundle arguments) { + if (virtualViewId == View.NO_ID) { + return ViewCompat.performAccessibilityAction(mParentView, action, arguments); + } + + final T item = getItemForId(virtualViewId); + if (item == null) { + return false; + } + + boolean handled = false; + + switch (action) { + case AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS: + if (mFocusedItemId != virtualViewId) { + mFocusedItemId = virtualViewId; + sendEventForItem(item, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED); + handled = true; + } + break; + case AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS: + if (mFocusedItemId == virtualViewId) { + mFocusedItemId = INVALID_ID; + sendEventForItem(item, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); + handled = true; + } + break; + } + + handled |= performActionForItem(item, action, arguments); + + return handled; + } + + @Override + public boolean onHover(View view, MotionEvent event) { + if (!mManager.isTouchExplorationEnabled()) { + return false; + } + + switch (event.getAction()) { + case MotionEvent.ACTION_HOVER_ENTER: + case MotionEvent.ACTION_HOVER_MOVE: + final T item = getItemAt(event.getX(), event.getY()); + setCurrentItem(item); + return true; + case MotionEvent.ACTION_HOVER_EXIT: + setCurrentItem(null); + return true; + } + + return false; + } + + private void setCurrentItem(T item) { + if (mCurrentItem == item) { + return; + } + + if (mCurrentItem != null) { + sendEventForItem(mCurrentItem, AccessibilityEvent.TYPE_VIEW_HOVER_EXIT); + } + + mCurrentItem = item; + + if (mCurrentItem != null) { + sendEventForItem(mCurrentItem, AccessibilityEvent.TYPE_VIEW_HOVER_ENTER); + } + } + + private AccessibilityEvent getEventForItem(T item, int eventType) { + final AccessibilityEvent event = AccessibilityEvent.obtain(eventType); + final AccessibilityRecordCompat record = new AccessibilityRecordCompat(event); + final int virtualDescendantId = getIdForItem(item); + + // Ensure the client has good defaults. + event.setEnabled(true); + + // Allow the client to populate the event. + populateEventForItem(item, event); + + if (event.getText().isEmpty() && TextUtils.isEmpty(event.getContentDescription())) { + throw new RuntimeException( + "You must add text or a content description in populateEventForItem()"); + } + + // Don't allow the client to override these properties. + event.setClassName(item.getClass().getName()); + event.setPackageName(mParentView.getContext().getPackageName()); + record.setSource(mParentView, virtualDescendantId); + + return event; + } + + private AccessibilityNodeInfoCompat getNodeForParent() { + final AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain(mParentView); + ViewCompat.onInitializeAccessibilityNodeInfo(mParentView, info); + + final LinkedList<T> items = new LinkedList<T>(); + getVisibleItems(items); + + for (T item : items) { + final int virtualDescendantId = getIdForItem(item); + info.addChild(mParentView, virtualDescendantId); + } + + return info; + } + + private AccessibilityNodeInfoCompat populateNodeForItemInternal( + T item, AccessibilityNodeInfoCompat node) { + final int virtualDescendantId = getIdForItem(item); + + // Ensure the client has good defaults. + node.setEnabled(true); + + // Allow the client to populate the node. + populateNodeForItem(item, node); + + if (TextUtils.isEmpty(node.getText()) && TextUtils.isEmpty(node.getContentDescription())) { + throw new RuntimeException( + "You must add text or a content description in populateNodeForItem()"); + } + + // Don't allow the client to override these properties. + node.setPackageName(mParentView.getContext().getPackageName()); + node.setClassName(item.getClass().getName()); + node.setParent(mParentView); + node.setSource(mParentView, virtualDescendantId); + + if (mFocusedItemId == virtualDescendantId) { + node.addAction(AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS); + } else { + node.addAction(AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS); + } + + node.getBoundsInParent(mTempParentRect); + if (mTempParentRect.isEmpty()) { + throw new RuntimeException("You must set parent bounds in populateNodeForItem()"); + } + + // Set the visibility based on the parent bound. + if (intersectVisibleToUser(mTempParentRect)) { + node.setVisibleToUser(true); + node.setBoundsInParent(mTempParentRect); + } + + // Calculate screen-relative bound. + mParentView.getLocationOnScreen(mTempGlobalRect); + final int offsetX = mTempGlobalRect[0]; + final int offsetY = mTempGlobalRect[1]; + mTempScreenRect.set(mTempParentRect); + mTempScreenRect.offset(offsetX, offsetY); + node.setBoundsInScreen(mTempScreenRect); + + return node; + } + + /** + * Computes whether the specified {@link Rect} intersects with the visible + * portion of its parent {@link View}. Modifies {@code localRect} to + * contain only the visible portion. + * + * @param localRect A rectangle in local (parent) coordinates. + * @return Whether the specified {@link Rect} is visible on the screen. + */ + private boolean intersectVisibleToUser(Rect localRect) { + // Missing or empty bounds mean this view is not visible. + if ((localRect == null) || localRect.isEmpty()) { + return false; + } + + // Attached to invisible window means this view is not visible. + if (mParentView.getWindowVisibility() != View.VISIBLE) { + return false; + } + + // An invisible predecessor or one with alpha zero means + // that this view is not visible to the user. + Object current = this; + while (current instanceof View) { + final View view = (View) current; + // We have attach info so this view is attached and there is no + // need to check whether we reach to ViewRootImpl on the way up. + if ((view.getAlpha() <= 0) || (view.getVisibility() != View.VISIBLE)) { + return false; + } + current = view.getParent(); + } + + // If no portion of the parent is visible, this view is not visible. + if (!mParentView.getLocalVisibleRect(mTempVisibleRect)) { + return false; + } + + // Check if the view intersects the visible portion of the parent. + return localRect.intersect(mTempVisibleRect); + } + + public AccessibilityDelegateCompat getAccessibilityDelegate() { + return mDelegate; + } + + private final AccessibilityDelegateCompat mDelegate = new AccessibilityDelegateCompat() { + @Override + public void onInitializeAccessibilityEvent(View view, AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(view, event); + event.setClassName(view.getClass().getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(View view, AccessibilityNodeInfoCompat info) { + super.onInitializeAccessibilityNodeInfo(view, info); + info.setClassName(view.getClass().getName()); + } + + @Override + public AccessibilityNodeProviderCompat getAccessibilityNodeProvider(View host) { + return TouchExplorationHelper.this; + } + }; + + /** + * Performs an accessibility action on the specified item. See + * {@link AccessibilityNodeInfoCompat#performAction(int, Bundle)}. + * <p> + * The helper class automatically handles focus management resulting from + * {@link AccessibilityNodeInfoCompat#ACTION_ACCESSIBILITY_FOCUS} and + * {@link AccessibilityNodeInfoCompat#ACTION_CLEAR_ACCESSIBILITY_FOCUS}, so + * typically a developer only needs to handle actions added manually in the + * {{@link #populateNodeForItem(Object, AccessibilityNodeInfoCompat)} + * method. + * </p> + * + * @param item The item on which to perform the action. + * @param action The accessibility action to perform. + * @param arguments Arguments for the action, or optionally {@code null}. + * @return {@code true} if the action was performed successfully. + */ + protected abstract boolean performActionForItem(T item, int action, Bundle arguments); + + /** + * Populates an event with information about the specified item. + * <p> + * At a minimum, a developer must populate the event text by doing one of + * the following: + * <ul> + * <li>appending text to {@link AccessibilityEvent#getText()}</li> + * <li>populating a description with + * {@link AccessibilityEvent#setContentDescription(CharSequence)}</li> + * </ul> + * </p> + * + * @param item The item for which to populate the event. + * @param event The event to populate. + */ + protected abstract void populateEventForItem(T item, AccessibilityEvent event); + + /** + * Populates a node with information about the specified item. + * <p> + * At a minimum, a developer must: + * <ul> + * <li>populate the event text using + * {@link AccessibilityNodeInfoCompat#setText(CharSequence)} or + * {@link AccessibilityNodeInfoCompat#setContentDescription(CharSequence)} + * </li> + * <li>set the item's parent-relative bounds using + * {@link AccessibilityNodeInfoCompat#setBoundsInParent(Rect)} + * </ul> + * + * @param item The item for which to populate the node. + * @param node The node to populate. + */ + protected abstract void populateNodeForItem(T item, AccessibilityNodeInfoCompat node); + + /** + * Populates a list with the parent view's visible items. + * <p> + * The result of this method is cached until the developer calls + * {@link #invalidateParent()}. + * </p> + * + * @param items The list to populate with visible items. + */ + protected abstract void getVisibleItems(List<T> items); + + /** + * Returns the item under the specified parent-relative coordinates. + * + * @param x The parent-relative x coordinate. + * @param y The parent-relative y coordinate. + * @return The item under coordinates (x,y). + */ + protected abstract T getItemAt(float x, float y); + + /** + * Returns the unique identifier for an item. If the specified item does not + * exist, returns {@link #INVALID_ID}. + * <p> + * This result of this method must be consistent with + * {@link #getItemForId(int)}. + * </p> + * + * @param item The item whose identifier to return. + * @return A unique identifier, or {@link #INVALID_ID}. + */ + protected abstract int getIdForItem(T item); + + /** + * Returns the item for a unique identifier. If the specified item does not + * exist, returns {@code null}. + * + * @param id The identifier for the item to return. + * @return An item, or {@code null}. + */ + protected abstract T getItemForId(int id); +} |