/*
* Copyright (C) 2006 The Android Open Source Project
*
* 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.appunite.list;
import android.R;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.database.DataSetObserver;
import android.os.Build;
import android.os.Parcelable;
import android.os.SystemClock;
import android.support.v7.app.ActionBarActivity;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.util.TypedValue;
import android.view.*;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.Adapter;
/**
* An AdapterView is a view whose children are determined by an {@link Adapter}.
*
* <p>
* See {@link ListView}, {@link android.widget.GridView}, {@link android.widget.Spinner} and
* {@link android.widget.Gallery} for commonly used subclasses of AdapterView.
*
* <div class="special reference">
* <h3>Developer Guides</h3>
* <p>For more information about using AdapterView, read the
* <a href="{@docRoot}guide/topics/ui/binding.html">Binding to Data with AdapterView</a>
* developer guide.</p></div>
*/
public abstract class AdapterView<T extends Adapter> extends ViewGroup {
/**
* The item view type returned by {@link Adapter#getItemViewType(int)} when
* the adapter does not want the item's view recycled.
*/
public static final int ITEM_VIEW_TYPE_IGNORE = -1;
/**
* The item view type returned by {@link Adapter#getItemViewType(int)} when
* the item is a header or footer.
*/
public static final int ITEM_VIEW_TYPE_HEADER_OR_FOOTER = -2;
/**
* The position of the first child displayed
*/
@ViewDebug.ExportedProperty(category = "scrolling")
int mFirstPosition = 0;
/**
* The offset in pixels from the top of the AdapterView to the top
* of the view to select during the next layout.
*/
int mSpecificTop;
/**
* Position from which to start looking for mSyncRowId
*/
int mSyncPosition;
/**
* Row id to look for when data has changed
*/
long mSyncRowId = INVALID_ROW_ID;
/**
* Height of the view when mSyncPosition and mSyncRowId where set
*/
long mSyncHeight;
/**
* True if we need to sync to mSyncRowId
*/
boolean mNeedSync = false;
/**
* Indicates whether to sync based on the selection or position. Possible
* values are {@link #SYNC_SELECTED_POSITION} or
* {@link #SYNC_FIRST_POSITION}.
*/
int mSyncMode;
/**
* Our width after the last layout
*/
private int mLayoutHeight;
/**
* Sync based on the selected child
*/
static final int SYNC_SELECTED_POSITION = 0;
/**
* Sync based on the first child displayed
*/
static final int SYNC_FIRST_POSITION = 1;
/**
* Maximum amount of time to spend in {@link #findSyncPosition()}
*/
static final int SYNC_MAX_DURATION_MILLIS = 100;
/**
* Indicates that this view is currently being laid out.
*/
boolean mInLayout = false;
/**
* The listener that receives notifications when an item is selected.
*/
OnItemSelectedListener mOnItemSelectedListener;
/**
* The listener that receives notifications when an item is clicked.
*/
OnItemClickListener mOnItemClickListener;
/**
* The listener that receives notifications when an item is long clicked.
*/
OnItemLongClickListener mOnItemLongClickListener;
/**
* True if the data has changed since the last layout
*/
boolean mDataChanged;
/**
* The position within the adapter's data set of the item to select
* during the next layout.
*/
@ViewDebug.ExportedProperty(category = "list")
int mNextSelectedPosition = INVALID_POSITION;
/**
* The item id of the item to select during the next layout.
*/
long mNextSelectedRowId = INVALID_ROW_ID;
/**
* The position within the adapter's data set of the currently selected item.
*/
@ViewDebug.ExportedProperty(category = "list")
int mSelectedPosition = INVALID_POSITION;
/**
* The item id of the currently selected item.
*/
long mSelectedRowId = INVALID_ROW_ID;
/**
* View to show if there are no items to show.
*/
private View mEmptyView;
/**
* The number of items in the current adapter.
*/
@ViewDebug.ExportedProperty(category = "list")
int mItemCount;
/**
* The number of items in the adapter before a data changed event occurred.
*/
int mOldItemCount;
/**
* Represents an invalid position. All valid positions are in the range 0 to 1 less than the
* number of items in the current adapter.
*/
public static final int INVALID_POSITION = -1;
/**
* Represents an empty or invalid row id
*/
public static final long INVALID_ROW_ID = Long.MIN_VALUE;
/**
* The last selected position we used when notifying
*/
int mOldSelectedPosition = INVALID_POSITION;
/**
* The id of the last selected position we used when notifying
*/
long mOldSelectedRowId = INVALID_ROW_ID;
/**
* Indicates what focusable state is requested when calling setFocusable().
* In addition to this, this view has other criteria for actually
* determining the focusable state (such as whether its empty or the text
* filter is shown).
*
* @see #setFocusable(boolean)
* @see #checkFocus()
*/
private boolean mDesiredFocusableState;
private boolean mDesiredFocusableInTouchModeState;
private SelectionNotifier mSelectionNotifier;
/**
* When set to true, calls to requestLayout() will not propagate up the parent hierarchy.
* This is used to layout the children during a layout pass.
*/
boolean mBlockLayoutRequests = false;
/**
* Track if we are currently attached to a window.
*/
boolean mIsAttached;
// Unhide method from ViewGroup (j.m.)
protected boolean mClipToPadding = false;
// Unhide method from ViewGroup (j.m.)
protected boolean mDisallowIntercept = false;
private float mVerticalScrollFactor = 0.0f;
private float mHorizontalScrollFactor = 0.0f;
public AdapterView(Context context) {
super(context);
}
public AdapterView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public AdapterView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
setImportantAccessibility();
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
private void setImportantAccessibility() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
// If not explicitly specified this view is important for accessibility.
if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
}
}
}
/**
* Interface definition for a callback to be invoked when an item in this
* AdapterView has been clicked.
*/
public interface OnItemClickListener {
/**
* Callback method to be invoked when an item in this AdapterView has
* been clicked.
* <p>
* Implementers can call getItemAtPosition(position) if they need
* to access the data associated with the selected item.
*
* @param parent The AdapterView where the click happened.
* @param view The view within the AdapterView that was clicked (this
* will be a view provided by the adapter)
* @param position The position of the view in the adapter.
* @param id The row id of the item that was clicked.
*/
void onItemClick(AdapterView<?> parent, View view, int position, long id);
}
/**
* Register a callback to be invoked when an item in this AdapterView has
* been clicked.
*
* @param listener The callback that will be invoked.
*/
public void setOnItemClickListener(OnItemClickListener listener) {
mOnItemClickListener = listener;
}
/**
* @return The callback to be invoked with an item in this AdapterView has
* been clicked, or null id no callback has been set.
*/
public final OnItemClickListener getOnItemClickListener() {
return mOnItemClickListener;
}
/**
* Call the OnItemClickListener, if it is defined.
*
* @param view The view within the AdapterView that was clicked.
* @param position The position of the view in the adapter.
* @param id The row id of the item that was clicked.
* @return True if there was an assigned OnItemClickListener that was
* called, false otherwise is returned.
*/
public boolean performItemClick(View view, int position, long id) {
if (mOnItemClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
if (view != null) {
view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
}
mOnItemClickListener.onItemClick(this, view, position, id);
return true;
}
return false;
}
/**
* Interface definition for a callback to be invoked when an item in this
* view has been clicked and held.
*/
public interface OnItemLongClickListener {
/**
* Callback method to be invoked when an item in this view has been
* clicked and held.
*
* Implementers can call getItemAtPosition(position) if they need to access
* the data associated with the selected item.
*
* @param parent The AbsListView where the click happened
* @param view The view within the AbsListView that was clicked
* @param position The position of the view in the list
* @param id The row id of the item that was clicked
*
* @return true if the callback consumed the long click, false otherwise
*/
boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id);
}
/**
* Register a callback to be invoked when an item in this AdapterView has
* been clicked and held
*
* @param listener The callback that will run
*/
public void setOnItemLongClickListener(OnItemLongClickListener listener) {
if (!isLongClickable()) {
setLongClickable(true);
}
mOnItemLongClickListener = listener;
}
/**
* @return The callback to be invoked with an item in this AdapterView has
* been clicked and held, or null id no callback as been set.
*/
public final OnItemLongClickListener getOnItemLongClickListener() {
return mOnItemLongClickListener;
}
/**
* Interface definition for a callback to be invoked when
* an item in this view has been selected.
*/
public interface OnItemSelectedListener {
/**
* <p>Callback method to be invoked when an item in this view has been
* selected. This callback is invoked only when the newly selected
* position is different from the previously selected position or if
* there was no selected item.</p>
*
* Impelmenters can call getItemAtPosition(position) if they need to access the
* data associated with the selected item.
*
* @param parent The AdapterView where the selection happened
* @param view The view within the AdapterView that was clicked
* @param position The position of the view in the adapter
* @param id The row id of the item that is selected
*/
void onItemSelected(AdapterView<?> parent, View view, int position, long id);
/**
* Callback method to be invoked when the selection disappears from this
* view. The selection can disappear for instance when touch is activated
* or when the adapter becomes empty.
*
* @param parent The AdapterView that now contains no selected item.
*/
void onNothingSelected(AdapterView<?> parent);
}
/**
* Register a callback to be invoked when an item in this AdapterView has
* been selected.
*
* @param listener The callback that will run
*/
public void setOnItemSelectedListener(OnItemSelectedListener listener) {
mOnItemSelectedListener = listener;
}
public final OnItemSelectedListener getOnItemSelectedListener() {
return mOnItemSelectedListener;
}
/**
* Extra menu information provided to the
* {@link android.view.View.OnCreateContextMenuListener#onCreateContextMenu(ContextMenu, View, ContextMenuInfo) }
* callback when a context menu is brought up for this AdapterView.
*
*/
public static class AdapterContextMenuInfo implements ContextMenu.ContextMenuInfo {
public AdapterContextMenuInfo(View targetView, int position, long id) {
this.targetView = targetView;
this.position = position;
this.id = id;
}
/**
* The child view for which the context menu is being displayed. This
* will be one of the children of this AdapterView.
*/
public View targetView;
/**
* The position in the adapter for which the context menu is being
* displayed.
*/
public int position;
/**
* The row id of the item for which the context menu is being displayed.
*/
public long id;
}
/**
* Returns the adapter currently associated with this widget.
*
* @return The adapter used to provide this view's content.
*/
public abstract T getAdapter();
/**
* Sets the adapter that provides the data and the views to represent the data
* in this widget.
*
* @param adapter The adapter to use to create this view's content.
*/
public abstract void setAdapter(T adapter);
/**
* This method is not supported and throws an UnsupportedOperationException when called.
*
* @param child Ignored.
*
* @throws UnsupportedOperationException Every time this method is invoked.
*/
@Override
public void addView(View child) {
throw new UnsupportedOperationException("addView(View) is not supported in AdapterView");
}
/**
* This method is not supported and throws an UnsupportedOperationException when called.
*
* @param child Ignored.
* @param index Ignored.
*
* @throws UnsupportedOperationException Every time this method is invoked.
*/
@Override
public void addView(View child, int index) {
throw new UnsupportedOperationException("addView(View, int) is not supported in AdapterView");
}
/**
* This method is not supported and throws an UnsupportedOperationException when called.
*
* @param child Ignored.
* @param params Ignored.
*
* @throws UnsupportedOperationException Every time this method is invoked.
*/
@Override
public void addView(View child, LayoutParams params) {
throw new UnsupportedOperationException("addView(View, LayoutParams) "
+ "is not supported in AdapterView");
}
/**
* This method is not supported and throws an UnsupportedOperationException when called.
*
* @param child Ignored.
* @param index Ignored.
* @param params Ignored.
*
* @throws UnsupportedOperationException Every time this method is invoked.
*/
@Override
public void addView(View child, int index, LayoutParams params) {
throw new UnsupportedOperationException("addView(View, int, LayoutParams) "
+ "is not supported in AdapterView");
}
/**
* This method is not supported and throws an UnsupportedOperationException when called.
*
* @param child Ignored.
*
* @throws UnsupportedOperationException Every time this method is invoked.
*/
@Override
public void removeView(View child) {
throw new UnsupportedOperationException("removeView(View) is not supported in AdapterView");
}
/**
* This method is not supported and throws an UnsupportedOperationException when called.
*
* @param index Ignored.
*
* @throws UnsupportedOperationException Every time this method is invoked.
*/
@Override
public void removeViewAt(int index) {
throw new UnsupportedOperationException("removeViewAt(int) is not supported in AdapterView");
}
/**
* This method is not supported and throws an UnsupportedOperationException when called.
*
* @throws UnsupportedOperationException Every time this method is invoked.
*/
@Override
public void removeAllViews() {
throw new UnsupportedOperationException("removeAllViews() is not supported in AdapterView");
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
mLayoutHeight = getHeight();
}
/**
* Return the position of the currently selected item within the adapter's data set
*
* @return int Position (starting at 0), or {@link #INVALID_POSITION} if there is nothing selected.
*/
@ViewDebug.CapturedViewProperty
public int getSelectedItemPosition() {
return mNextSelectedPosition;
}
/**
* @return The id corresponding to the currently selected item, or {@link #INVALID_ROW_ID}
* if nothing is selected.
*/
@ViewDebug.CapturedViewProperty
public long getSelectedItemId() {
return mNextSelectedRowId;
}
/**
* @return The view corresponding to the currently selected item, or null
* if nothing is selected
*/
public abstract View getSelectedView();
/**
* @return The data corresponding to the currently selected item, or
* null if there is nothing selected.
*/
public Object getSelectedItem() {
T adapter = getAdapter();
int selection = getSelectedItemPosition();
if (adapter != null && adapter.getCount() > 0 && selection >= 0) {
return adapter.getItem(selection);
} else {
return null;
}
}
/**
* @return The number of items owned by the Adapter associated with this
* AdapterView. (This is the number of data items, which may be
* larger than the number of visible views.)
*/
@ViewDebug.CapturedViewProperty
public int getCount() {
return mItemCount;
}
/**
* Get the position within the adapter's data set for the view, where view is a an adapter item
* or a descendant of an adapter item.
*
* @param view an adapter item, or a descendant of an adapter item. This must be visible in this
* AdapterView at the time of the call.
* @return the position within the adapter's data set of the view, or {@link #INVALID_POSITION}
* if the view does not correspond to a list item (or it is not currently visible).
*/
public int getPositionForView(View view) {
View listItem = view;
try {
View v;
while (!(v = (View) listItem.getParent()).equals(this)) {
listItem = v;
}
} catch (ClassCastException e) {
// We made it up to the window without find this list view
return INVALID_POSITION;
}
// Search the children for the list item
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
if (getChildAt(i).equals(listItem)) {
return mFirstPosition + i;
}
}
// Child not found!
return INVALID_POSITION;
}
/**
* Returns the position within the adapter's data set for the first item
* displayed on screen.
*
* @return The position within the adapter's data set
*/
public int getFirstVisiblePosition() {
return mFirstPosition;
}
/**
* Returns the position within the adapter's data set for the last item
* displayed on screen.
*
* @return The position within the adapter's data set
*/
public int getLastVisiblePosition() {
return mFirstPosition + getChildCount() - 1;
}
/**
* Sets the currently selected item. To support accessibility subclasses that
* override this method must invoke the overriden super method first.
*
* @param position Index (starting at 0) of the data item to be selected.
*/
public abstract void setSelection(int position);
/**
* Sets the view to show if the adapter is empty
*/
//@android.view.RemotableViewMethod
public void setEmptyView(View emptyView) {
mEmptyView = emptyView;
getImportantForAccessibility(emptyView);
final T adapter = getAdapter();
final boolean empty = ((adapter == null) || adapter.isEmpty());
updateEmptyStatus(empty);
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
private void getImportantForAccessibility(View emptyView) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
// If not explicitly specified this view is important for accessibility.
if (emptyView != null
&& emptyView.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
emptyView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
}
}
}
/**
* When the current adapter is empty, the AdapterView can display a special view
* call the empty view. The empty view is used to provide feedback to the user
* that no data is available in this AdapterView.
*
* @return The view to show if the adapter is empty.
*/
public View getEmptyView() {
return mEmptyView;
}
/**
* Indicates whether this view is in filter mode. Filter mode can for instance
* be enabled by a user when typing on the keyboard.
*
* @return True if the view is in filter mode, false otherwise.
*/
boolean isInFilterMode() {
return false;
}
@Override
public void setFocusable(boolean focusable) {
final T adapter = getAdapter();
final boolean empty = adapter == null || adapter.getCount() == 0;
mDesiredFocusableState = focusable;
if (!focusable) {
mDesiredFocusableInTouchModeState = false;
}
super.setFocusable(focusable && (!empty || isInFilterMode()));
}
@Override
public void setFocusableInTouchMode(boolean focusable) {
final T adapter = getAdapter();
final boolean empty = adapter == null || adapter.getCount() == 0;
mDesiredFocusableInTouchModeState = focusable;
if (focusable) {
mDesiredFocusableState = true;
}
super.setFocusableInTouchMode(focusable && (!empty || isInFilterMode()));
}
void checkFocus() {
final T adapter = getAdapter();
final boolean empty = adapter == null || adapter.getCount() == 0;
final boolean focusable = !empty || isInFilterMode();
// 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((adapter == null) || adapter.isEmpty());
}
}
/**
* Update the status of the list based on the empty parameter. If empty is true and
* we have an empty view, display it. In all the other cases, make sure that the listview
* is VISIBLE and that the empty view is GONE (if it's not null).
*/
@SuppressLint("WrongCall")
private void updateEmptyStatus(boolean empty) {
if (isInFilterMode()) {
empty = false;
}
if (empty) {
if (mEmptyView != null) {
mEmptyView.setVisibility(View.VISIBLE);
setVisibility(View.GONE);
} else {
// If the caller just removed our empty view, make sure the list view is visible
setVisibility(View.VISIBLE);
}
// We are now GONE, so pending layouts will not be dispatched.
// Force one here to make sure that the state of the list matches
// the state of the adapter.
if (mDataChanged) {
final int left = getLeft();
final int top = getTop();
final int right = getRight();
final int bottom = getBottom();
this.onLayout(false, left, top, right, bottom);
}
} else {
if (mEmptyView != null) mEmptyView.setVisibility(View.GONE);
setVisibility(View.VISIBLE);
}
}
/**
* Gets the data associated with the specified position in the list.
*
* @param position Which data to get
* @return The data associated with the specified position in the list
*/
public Object getItemAtPosition(int position) {
T adapter = getAdapter();
return (adapter == null || position < 0) ? null : adapter.getItem(position);
}
public long getItemIdAtPosition(int position) {
T adapter = getAdapter();
return (adapter == null || position < 0) ? INVALID_ROW_ID : adapter.getItemId(position);
}
@Override
public void setOnClickListener(OnClickListener l) {
throw new RuntimeException("Don't call setOnClickListener for an AdapterView. "
+ "You probably want setOnItemClickListener instead");
}
/**
* Override to prevent freezing of any views created by the adapter.
*/
@Override
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
dispatchFreezeSelfOnly(container);
}
/**
* Override to prevent thawing of any views created by the adapter.
*/
@Override
protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
dispatchThawSelfOnly(container);
}
class AdapterDataSetObserver extends DataSetObserver {
private Parcelable mInstanceState = null;
@Override
public void onChanged() {
mDataChanged = true;
mOldItemCount = mItemCount;
mItemCount = getAdapter().getCount();
// Detect the case where a cursor that was previously invalidated has
// been repopulated with new data.
if (AdapterView.this.getAdapter().hasStableIds() && mInstanceState != null
&& mOldItemCount == 0 && mItemCount > 0) {
AdapterView.this.onRestoreInstanceState(mInstanceState);
mInstanceState = null;
} else {
rememberSyncState();
}
checkFocus();
requestLayout();
}
@Override
public void onInvalidated() {
mDataChanged = true;
if (AdapterView.this.getAdapter().hasStableIds()) {
// Remember the current state for the case where our hosting activity is being
// stopped and later restarted
mInstanceState = AdapterView.this.onSaveInstanceState();
}
// Data is invalid so we should reset our state
mOldItemCount = mItemCount;
mItemCount = 0;
mSelectedPosition = INVALID_POSITION;
mSelectedRowId = INVALID_ROW_ID;
mNextSelectedPosition = INVALID_POSITION;
mNextSelectedRowId = INVALID_ROW_ID;
mNeedSync = false;
checkFocus();
requestLayout();
}
public void clearSavedState() {
mInstanceState = null;
}
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
mIsAttached = true;
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
removeCallbacks(mSelectionNotifier);
mIsAttached = false;
}
private class SelectionNotifier implements Runnable {
public void run() {
if (mDataChanged) {
// Data has changed between when this SelectionNotifier
// was posted and now. We need to wait until the AdapterView
// has been synched to the new data.
if (getAdapter() != null) {
post(this);
}
} else {
fireOnSelected();
performAccessibilityActionsOnSelected();
}
}
}
void selectionChanged() {
if (mOnItemSelectedListener != null
|| getAccessibilityManager().isEnabled()) {
if (mInLayout || mBlockLayoutRequests) {
// If we are in a layout traversal, defer notification
// by posting. This ensures that the view tree is
// in a consistent state and is able to accomodate
// new layout or invalidate requests.
if (mSelectionNotifier == null) {
mSelectionNotifier = new SelectionNotifier();
}
post(mSelectionNotifier);
} else {
fireOnSelected();
performAccessibilityActionsOnSelected();
}
}
}
private void fireOnSelected() {
if (mOnItemSelectedListener == null) {
return;
}
final int selection = getSelectedItemPosition();
if (selection >= 0) {
View v = getSelectedView();
mOnItemSelectedListener.onItemSelected(this, v, selection,
getAdapter().getItemId(selection));
} else {
mOnItemSelectedListener.onNothingSelected(this);
}
}
private void performAccessibilityActionsOnSelected() {
if (!getAccessibilityManager().isEnabled()) {
return;
}
final int position = getSelectedItemPosition();
if (position >= 0) {
// we fire selection events here not in View
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
}
}
@Override
public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
View selectedView = getSelectedView();
if (selectedView != null && selectedView.getVisibility() == VISIBLE
&& selectedView.dispatchPopulateAccessibilityEvent(event)) {
return true;
}
return false;
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
@Override
public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) {
if (super.onRequestSendAccessibilityEvent(child, event)) {
// Add a record for ourselves as well.
AccessibilityEvent record = AccessibilityEvent.obtain();
onInitializeAccessibilityEvent(record);
// Populate with the text of the requesting child.
child.dispatchPopulateAccessibilityEvent(record);
event.appendRecord(record);
return true;
}
return false;
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
@Override
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
super.onInitializeAccessibilityNodeInfo(info);
info.setClassName(AdapterView.class.getName());
info.setScrollable(isScrollableForAccessibility());
View selectedView = getSelectedView();
if (selectedView != null) {
info.setEnabled(selectedView.isEnabled());
}
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
@Override
public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
super.onInitializeAccessibilityEvent(event);
event.setClassName(AdapterView.class.getName());
event.setScrollable(isScrollableForAccessibility());
View selectedView = getSelectedView();
if (selectedView != null) {
event.setEnabled(selectedView.isEnabled());
}
event.setCurrentItemIndex(getSelectedItemPosition());
event.setFromIndex(getFirstVisiblePosition());
event.setToIndex(getLastVisiblePosition());
event.setItemCount(getCount());
}
private boolean isScrollableForAccessibility() {
T adapter = getAdapter();
if (adapter != null) {
final int itemCount = adapter.getCount();
return itemCount > 0
&& (getFirstVisiblePosition() > 0 || getLastVisiblePosition() < itemCount - 1);
}
return false;
}
@Override
protected boolean canAnimate() {
return super.canAnimate() && mItemCount > 0;
}
void handleDataChanged() {
final int count = mItemCount;
boolean found = false;
if (count > 0) {
int newPos;
// Find the row we are supposed to sync to
if (mNeedSync) {
// Update this first, since setNextSelectedPositionInt inspects
// it
mNeedSync = false;
// See if we can find a position in the new data with the same
// id as the old selection
newPos = findSyncPosition();
if (newPos >= 0) {
// Verify that new selection is selectable
int selectablePos = lookForSelectablePosition(newPos, true);
if (selectablePos == newPos) {
// Same row id is selected
setNextSelectedPositionInt(newPos);
found = true;
}
}
}
if (!found) {
// Try to use the same position if we can't find matching data
newPos = getSelectedItemPosition();
// Pin position to the available range
if (newPos >= count) {
newPos = count - 1;
}
if (newPos < 0) {
newPos = 0;
}
// Make sure we select something selectable -- first look down
int selectablePos = lookForSelectablePosition(newPos, true);
if (selectablePos < 0) {
// Looking down didn't work -- try looking up
selectablePos = lookForSelectablePosition(newPos, false);
}
if (selectablePos >= 0) {
setNextSelectedPositionInt(selectablePos);
checkSelectionChanged();
found = true;
}
}
}
if (!found) {
// Nothing is selected
mSelectedPosition = INVALID_POSITION;
mSelectedRowId = INVALID_ROW_ID;
mNextSelectedPosition = INVALID_POSITION;
mNextSelectedRowId = INVALID_ROW_ID;
mNeedSync = false;
checkSelectionChanged();
}
//TODO: Hmm, we do not know the old state so this is sub-optimal
//TODO: Im not sure why this was there (jm)
// notifyAccessibilityStateChanged();
}
void checkSelectionChanged() {
if ((mSelectedPosition != mOldSelectedPosition) || (mSelectedRowId != mOldSelectedRowId)) {
selectionChanged();
mOldSelectedPosition = mSelectedPosition;
mOldSelectedRowId = mSelectedRowId;
}
}
/**
* Searches the adapter for a position matching mSyncRowId. The search starts at mSyncPosition
* and then alternates between moving up and moving down until 1) we find the right position, or
* 2) we run out of time, or 3) we have looked at every position
*
* @return Position of the row that matches mSyncRowId, or {@link #INVALID_POSITION} if it can't
* be found
*/
int findSyncPosition() {
int count = mItemCount;
if (count == 0) {
return INVALID_POSITION;
}
long idToMatch = mSyncRowId;
int seed = mSyncPosition;
// If there isn't a selection don't hunt for it
if (idToMatch == INVALID_ROW_ID) {
return INVALID_POSITION;
}
// Pin seed to reasonable values
seed = Math.max(0, seed);
seed = Math.min(count - 1, seed);
long endTime = SystemClock.uptimeMillis() + SYNC_MAX_DURATION_MILLIS;
long rowId;
// first position scanned so far
int first = seed;
// last position scanned so far
int last = seed;
// True if we should move down on the next iteration
boolean next = false;
// True when we have looked at the first item in the data
boolean hitFirst;
// True when we have looked at the last item in the data
boolean hitLast;
// Get the item ID locally (instead of getItemIdAtPosition), so
// we need the adapter
T adapter = getAdapter();
if (adapter == null) {
return INVALID_POSITION;
}
while (SystemClock.uptimeMillis() <= endTime) {
rowId = adapter.getItemId(seed);
if (rowId == idToMatch) {
// Found it!
return seed;
}
hitLast = last == count - 1;
hitFirst = first == 0;
if (hitLast && hitFirst) {
// Looked at everything
break;
}
if (hitFirst || (next && !hitLast)) {
// Either we hit the top, or we are trying to move down
last++;
seed = last;
// Try going up next time
next = false;
} else if (hitLast || (!next && !hitFirst)) {
// Either we hit the bottom, or we are trying to move up
first--;
seed = first;
// Try going down next time
next = true;
}
}
return INVALID_POSITION;
}
/**
* Find a position that can be selected (i.e., is not a separator).
*
* @param position The starting position to look at.
* @param lookDown Whether to look down for other positions.
* @return The next selectable position starting at position and then searching either up or
* down. Returns {@link #INVALID_POSITION} if nothing can be found.
*/
int lookForSelectablePosition(int position, boolean lookDown) {
return position;
}
/**
* Utility to keep mSelectedPosition and mSelectedRowId in sync
* @param position Our current position
*/
void setSelectedPositionInt(int position) {
mSelectedPosition = position;
mSelectedRowId = getItemIdAtPosition(position);
}
/**
* Utility to keep mNextSelectedPosition and mNextSelectedRowId in sync
* @param position Intended value for mSelectedPosition the next time we go
* through layout
*/
void setNextSelectedPositionInt(int position) {
mNextSelectedPosition = position;
mNextSelectedRowId = getItemIdAtPosition(position);
// If we are trying to sync to the selection, update that too
if (mNeedSync && mSyncMode == SYNC_SELECTED_POSITION && position >= 0) {
mSyncPosition = position;
mSyncRowId = mNextSelectedRowId;
}
}
/**
* Remember enough information to restore the screen state when the data has
* changed.
*
*/
void rememberSyncState() {
if (getChildCount() > 0) {
mNeedSync = true;
mSyncHeight = mLayoutHeight;
if (mSelectedPosition >= 0) {
// Sync the selection state
View v = getChildAt(mSelectedPosition - mFirstPosition);
mSyncRowId = mNextSelectedRowId;
mSyncPosition = mNextSelectedPosition;
if (v != null) {
mSpecificTop = v.getTop();
}
mSyncMode = SYNC_SELECTED_POSITION;
} else {
// Sync the based on the offset of the first view
View v = getChildAt(0);
T adapter = getAdapter();
if (mFirstPosition >= 0 && mFirstPosition < adapter.getCount()) {
mSyncRowId = adapter.getItemId(mFirstPosition);
} else {
mSyncRowId = NO_ID;
}
mSyncPosition = mFirstPosition;
if (v != null) {
mSpecificTop = v.getTop();
}
mSyncMode = SYNC_FIRST_POSITION;
}
}
}
protected AccessibilityManager getAccessibilityManager() {
final Context context = getContext();
return (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
}
// Unhide View method (j.m.)
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public boolean isInScrollingContainerUnhide() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
ViewParent p = getParent();
while (p != null && p instanceof ViewGroup) {
if (((ViewGroup) p).shouldDelayChildPressedState()) {
return true;
}
p = p.getParent();
}
return false;
} else {
return false;
}
}
// Unhide View method (j.m.)
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
protected void invalidateParentIfNeededUnhide() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
final ViewParent parent = getParent();
if (isHardwareAccelerated() && parent instanceof View) {
((View) parent).invalidate();
}
}
}
// Unhide ViewGroup method (j.m.)
public void offsetChildrenTopAndBottomUnhide(int offset) {
final int count = getChildCount();
for (int i = 0; i < count; i++) {
final View v = getChildAt(i);
assert v != null;
v.offsetTopAndBottom(offset);
}
}
// Unhide ViewGroup method (j.m.)
public void offsetChildrenLeftAndRightUnhide(int offset) {
final int count = getChildCount();
for (int i = 0; i < count; i++) {
final View v = getChildAt(i);
assert v != null;
v.offsetLeftAndRight(offset);
}
}
// Unhide ViewGroup method (j.m.)
@Override
public void setClipToPadding(boolean clipToPadding) {
super.setClipToPadding(clipToPadding);
mClipToPadding = clipToPadding;
}
// Unhide ViewGroup method (j.m.)
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
mDisallowIntercept = disallowIntercept;
super.requestDisallowInterceptTouchEvent(disallowIntercept);
}
// Unhide View method (j.m.)
protected void invalidateParentCachesUnhide() {
final ViewParent parent = getParent();
if (parent instanceof View) {
((View) parent).invalidate();
}
}
// Unhide View method (j.m.)
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
protected boolean performButtonActionOnTouchDownUnhide(MotionEvent event) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
if ((event.getButtonState() & MotionEvent.BUTTON_SECONDARY) != 0) {
if (showContextMenuUnhide(event.getX(), event.getY(), event.getMetaState())) {
return true;
}
}
}
return false;
}
// Unhide View method (j.m.)
public boolean showContextMenuUnhide(float x, float y, int metaState) {
return showContextMenu();
}
// Unhide View/ViewGroup method (j.m.)
public static void dispatchStartTemporaryDetach(View view) {
// TODO its very hard to implement those (j.m.)
// clearAccessibilityFocus();
// clearDisplayList();
view.onStartTemporaryDetach();
if (view instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) view;
final int childCount = viewGroup.getChildCount();
for (int i = 0; i < childCount; ++i) {
final View childAt = viewGroup.getChildAt(i);
AdapterView.dispatchStartTemporaryDetach(childAt);
}
}
}
// Unhide View method (j.m.)
protected float getVerticalScrollFactorUnhide() {
if (mVerticalScrollFactor == 0) {
TypedValue outValue = new TypedValue();
final Context context = getContext();
if (!context.getTheme().resolveAttribute(
R.attr.listPreferredItemHeight, outValue, true)) {
throw new IllegalStateException(
"Expected theme to define listPreferredItemHeight.");
}
mVerticalScrollFactor = outValue.getDimension(
context.getResources().getDisplayMetrics());
}
return mVerticalScrollFactor;
}
// Unhide View method (j.m.)
protected float getHorizontalScrollFactorUnhide() {
if (mHorizontalScrollFactor == 0) {
TypedValue outValue = new TypedValue();
final Context context = getContext();
if (!context.getTheme().resolveAttribute(
R.attr.listPreferredItemHeight, outValue, true)) {
throw new IllegalStateException(
"Expected theme to define listPreferredItemHeight.");
}
mHorizontalScrollFactor = outValue.getDimension(
context.getResources().getDisplayMetrics());
}
return mHorizontalScrollFactor;
}
// Unhide View method (j.m.)
protected boolean hasOpaqueScrollbarsUnhide() {
// TODO I actually do not know what to return... just return false (j.m.)
return false;
}
// Compat View method (j.m.)
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public boolean isHardwareAcceleratedCompat() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
return isHardwareAccelerated();
} else {
return false;
}
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
private class ActionModeCallbackWrapper implements android.view.ActionMode.Callback {
private AdapterView<?> mAdapterView;
private final android.support.v7.view.ActionMode.Callback mCallback;
private ActionModeWrapper mActionModeWrapper;
public ActionModeCallbackWrapper(AdapterView<?> adapterView,
android.support.v7.view.ActionMode.Callback callback) {
mAdapterView = adapterView;
mCallback = callback;
}
@Override
public boolean onCreateActionMode(android.view.ActionMode mode, android.view.Menu menu) {
//See ActionBarSherlockNative#startActionMode
mActionMode = mActionModeWrapper = new ActionModeWrapper(mAdapterView, mode);
return mCallback.onCreateActionMode(mActionMode, mActionMode.getMenu());
}
@Override
public boolean onPrepareActionMode(android.view.ActionMode mode, android.view.Menu menu) {
return mCallback.onPrepareActionMode(mActionMode, mActionMode.getMenu());
}
@Override
public boolean onActionItemClicked(android.view.ActionMode mode,
android.view.MenuItem item) {
return mCallback.onActionItemClicked(mActionMode, item);
}
@Override
public void onDestroyActionMode(android.view.ActionMode mode) {
mCallback.onDestroyActionMode(mActionMode);
}
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
private static class ActionModeWrapper extends android.support.v7.view.ActionMode {
private final android.view.ActionMode mActionMode;
private Menu mMenu = null;
private final AdapterView<?> mAdapterView;
public ActionModeWrapper(AdapterView<?> adapterView, ActionMode mode) {
mAdapterView = adapterView;
mActionMode = mode;
}
@Override
public void setTitle(CharSequence title) {
mActionMode.setTitle(title);
}
@Override
public void setTitle(int resId) {
mActionMode.setTitle(resId);
}
@Override
public void setSubtitle(CharSequence subtitle) {
mActionMode.setSubtitle(subtitle);
}
@Override
public void setSubtitle(int resId) {
mActionMode.setSubtitle(resId);
}
@Override
public void setCustomView(View view) {
mActionMode.setCustomView(view);
}
@Override
public void invalidate() {
mActionMode.invalidate();
}
@Override
public void finish() {
mActionMode.finish();
}
@Override
public Menu getMenu() {
if (mMenu == null) {
mMenu = mActionMode.getMenu();
}
return mMenu;
}
@Override
public CharSequence getTitle() {
return mActionMode.getTitle();
}
@Override
public CharSequence getSubtitle() {
return mActionMode.getSubtitle();
}
@Override
public View getCustomView() {
return mActionMode.getCustomView();
}
@Override
public MenuInflater getMenuInflater() {
return mAdapterView.getMenuInflater();
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
@Override
public void setTag(Object tag) {
mActionMode.setTag(tag);
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
@Override
public Object getTag() {
return mActionMode.getTag();
}
}
// Compat StateSet method (j.m.)
public static final int[] NOTHING_COMPAT = new int[] { 0 };
private android.support.v7.view.ActionMode.Callback mOldCallback = null;
private ActionMode.Callback mNewCallback = null;
/** Reference to our custom menu inflater which supports action items. */
protected MenuInflater mMenuInflater;
public MenuInflater getMenuInflater() {
final Context context = getContext();
// Make sure that action views can get an appropriate theme.
if (mMenuInflater == null) {
mMenuInflater = new MenuInflater(context);
}
return mMenuInflater;
}
private android.support.v7.view.ActionMode mActionMode;
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
protected android.support.v7.view.ActionMode startActionModeCompat(
final android.support.v7.view.ActionMode.Callback callback) {
if (callback == null) {
throw new IllegalArgumentException("Callback could not be null");
}
if (mActionMode != null) {
mActionMode.finish();
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
ActionMode.Callback newCallback;
if (mOldCallback == callback) {
newCallback = mNewCallback;
} else {
newCallback = mNewCallback = new ActionModeCallbackWrapper(this, callback);
}
startActionMode(newCallback);
return mActionMode;
}
final Context context = getContext();
if (context instanceof ActionBarActivity) {
ActionBarActivity activity = (ActionBarActivity)context;
mActionMode = activity.startSupportActionMode(callback);
return mActionMode;
}
return null;
}
abstract void reportScrollStateChange(int newState);
}