/* * 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 it.sephiroth.android.library.widget; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.Context; import android.database.DataSetObserver; import android.os.Parcelable; import android.os.SystemClock; import android.util.AttributeSet; import android.util.Log; import android.util.SparseArray; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; import android.view.SoundEffectConstants; import android.view.View; import android.view.ViewDebug; import android.view.ViewGroup; 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}. */ 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") protected int mFirstPosition = 0; /** * The offset in pixels from the left of the AdapterView to the left of the view to select during the next layout. */ protected int mSpecificLeft; /** * Position from which to start looking for mSyncRowId */ protected int mSyncPosition; /** * Col id to look for when data has changed */ protected long mSyncColId = INVALID_COL_ID; /** * Width of the view when mSyncPosition and mSyncColId where set */ protected long mSyncWidth; /** * True if we need to sync to mSyncColId */ protected 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 mLayoutWidth; /** * 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. */ protected 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 * @hide */ public boolean mDataChanged; /** * The position within the adapter's data set of the item to select during the next layout. */ @ViewDebug.ExportedProperty(category = "list") protected int mNextSelectedPosition = INVALID_POSITION; /** * The item id of the item to select during the next layout. */ protected long mNextSelectedColId = INVALID_COL_ID; /** * The position within the adapter's data set of the currently selected item. */ @ViewDebug.ExportedProperty(category = "list") protected int mSelectedPosition = INVALID_POSITION; /** * The item id of the currently selected item. */ protected long mSelectedColId = INVALID_COL_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") protected int mItemCount; /** * The number of items in the adapter before a data changed event occurred. */ protected int mOldItemCount; AccessibilityManager mAccessibilityManager; /** * 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 col id */ public static final long INVALID_COL_ID = Long.MIN_VALUE; /** * The last selected position we used when notifying */ protected int mOldSelectedPosition = INVALID_POSITION; /** * The id of the last selected position we used when notifying */ protected long mOldSelectedColId = INVALID_COL_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. */ protected boolean mBlockLayoutRequests = false; 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 ); if( android.os.Build.VERSION.SDK_INT >= 16 ) { // If not explicitly specified this view is important for accessibility. if ( getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO ) { setImportantForAccessibility( IMPORTANT_FOR_ACCESSIBILITY_YES ); } } mAccessibilityManager = (AccessibilityManager) getContext().getSystemService( Context.ACCESSIBILITY_SERVICE ); } /** * 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 col 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 col 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 col 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 col 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 col 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 ) { mLayoutWidth = getWidth(); } /** * 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 mNextSelectedColId; } /** * @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 */ public void setEmptyView( View emptyView ) { mEmptyView = emptyView; if( android.os.Build.VERSION.SDK_INT >= 16 ) { // 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 ); } } final T adapter = getAdapter(); final boolean empty = ( ( adapter == null ) || adapter.isEmpty() ); updateEmptyStatus( empty ); } /** * 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() ) ); } protected 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 ) { this.onLayout( false, getLeft(), getTop(), getRight(), getBottom() ); } } 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_COL_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() { Log.i( VIEW_LOG_TAG, "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 ) { Log.d( VIEW_LOG_TAG, "calling onRestoreInstanceState"); AdapterView.this.onRestoreInstanceState( mInstanceState ); mInstanceState = null; } else { Log.d( VIEW_LOG_TAG, "else calling rememberSyncState" ); rememberSyncState(); } checkFocus(); requestLayout(); } @Override public void onInvalidated() { Log.i( VIEW_LOG_TAG, "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; mSelectedColId = INVALID_COL_ID; mNextSelectedPosition = INVALID_POSITION; mNextSelectedColId = INVALID_COL_ID; mNeedSync = false; checkFocus(); requestLayout(); } public void clearSavedState() { mInstanceState = null; } } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); removeCallbacks( mSelectionNotifier ); } private class SelectionNotifier implements Runnable { @Override 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 || mAccessibilityManager.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 ( !mAccessibilityManager.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(14) @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(14) @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(14) @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 col 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 col 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; mSelectedColId = INVALID_COL_ID; mNextSelectedPosition = INVALID_POSITION; mNextSelectedColId = INVALID_COL_ID; mNeedSync = false; checkSelectionChanged(); } // TODO: Hmm, we do not know the old state so this is sub-optimal // TODO: implement this ( WTF Google, why you use the @hide tag?? ) // notifyAccessibilityStateChanged(); } protected void checkSelectionChanged() { if ( ( mSelectedPosition != mOldSelectedPosition ) || ( mSelectedColId != mOldSelectedColId ) ) { selectionChanged(); mOldSelectedPosition = mSelectedPosition; mOldSelectedColId = mSelectedColId; } } /** * Searches the adapter for a position matching mSyncColId. 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 col that matches mSyncColId, or {@link #INVALID_POSITION} if it can't be found */ int findSyncPosition() { int count = mItemCount; if ( count == 0 ) { return INVALID_POSITION; } long idToMatch = mSyncColId; int seed = mSyncPosition; // If there isn't a selection don't hunt for it if ( idToMatch == INVALID_COL_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 colId; // 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 ) { colId = adapter.getItemId( seed ); if ( colId == 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. */ protected int lookForSelectablePosition( int position, boolean lookDown ) { return position; } /** * Utility to keep mSelectedPosition and mSelectedColId in sync * * @param position * Our current position */ protected void setSelectedPositionInt( int position ) { mSelectedPosition = position; mSelectedColId = getItemIdAtPosition( position ); } /** * Utility to keep mNextSelectedPosition and mNextSelectedColId in sync * * @param position * Intended value for mSelectedPosition the next time we go through layout */ protected void setNextSelectedPositionInt( int position ) { mNextSelectedPosition = position; mNextSelectedColId = getItemIdAtPosition( position ); // If we are trying to sync to the selection, update that too if ( mNeedSync && mSyncMode == SYNC_SELECTED_POSITION && position >= 0 ) { mSyncPosition = position; mSyncColId = mNextSelectedColId; } } /** * Remember enough information to restore the screen state when the data has changed. * @hide */ public void rememberSyncState() { if ( getChildCount() > 0 ) { mNeedSync = true; mSyncWidth = mLayoutWidth; if ( mSelectedPosition >= 0 ) { // Sync the selection state View v = getChildAt( mSelectedPosition - mFirstPosition ); mSyncColId = mNextSelectedColId; mSyncPosition = mNextSelectedPosition; if ( v != null ) { mSpecificLeft = v.getLeft(); } 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() ) { mSyncColId = adapter.getItemId( mFirstPosition ); } else { mSyncColId = NO_ID; } mSyncPosition = mFirstPosition; if ( v != null ) { mSpecificLeft = v.getLeft(); } mSyncMode = SYNC_FIRST_POSITION; } } } }