OSDN Git Service

Merge "DO NOT MERGE AdapterView does not set setCurrentItemIndex in accessibility...
[android-x86/frameworks-base.git] / core / java / android / widget / AdapterView.java
1 /*
2  * Copyright (C) 2006 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package android.widget;
18
19 import android.content.Context;
20 import android.database.DataSetObserver;
21 import android.os.Parcelable;
22 import android.os.SystemClock;
23 import android.util.AttributeSet;
24 import android.util.SparseArray;
25 import android.view.ContextMenu;
26 import android.view.ContextMenu.ContextMenuInfo;
27 import android.view.SoundEffectConstants;
28 import android.view.View;
29 import android.view.ViewDebug;
30 import android.view.ViewGroup;
31 import android.view.accessibility.AccessibilityEvent;
32 import android.view.accessibility.AccessibilityNodeInfo;
33
34
35 /**
36  * An AdapterView is a view whose children are determined by an {@link Adapter}.
37  *
38  * <p>
39  * See {@link ListView}, {@link GridView}, {@link Spinner} and
40  *      {@link Gallery} for commonly used subclasses of AdapterView.
41  */
42 public abstract class AdapterView<T extends Adapter> extends ViewGroup {
43
44     /**
45      * The item view type returned by {@link Adapter#getItemViewType(int)} when
46      * the adapter does not want the item's view recycled.
47      */
48     public static final int ITEM_VIEW_TYPE_IGNORE = -1;
49
50     /**
51      * The item view type returned by {@link Adapter#getItemViewType(int)} when
52      * the item is a header or footer.
53      */
54     public static final int ITEM_VIEW_TYPE_HEADER_OR_FOOTER = -2;
55
56     /**
57      * The position of the first child displayed
58      */
59     @ViewDebug.ExportedProperty(category = "scrolling")
60     int mFirstPosition = 0;
61
62     /**
63      * The offset in pixels from the top of the AdapterView to the top
64      * of the view to select during the next layout.
65      */
66     int mSpecificTop;
67
68     /**
69      * Position from which to start looking for mSyncRowId
70      */
71     int mSyncPosition;
72
73     /**
74      * Row id to look for when data has changed
75      */
76     long mSyncRowId = INVALID_ROW_ID;
77
78     /**
79      * Height of the view when mSyncPosition and mSyncRowId where set
80      */
81     long mSyncHeight;
82
83     /**
84      * True if we need to sync to mSyncRowId
85      */
86     boolean mNeedSync = false;
87
88     /**
89      * Indicates whether to sync based on the selection or position. Possible
90      * values are {@link #SYNC_SELECTED_POSITION} or
91      * {@link #SYNC_FIRST_POSITION}.
92      */
93     int mSyncMode;
94
95     /**
96      * Our height after the last layout
97      */
98     private int mLayoutHeight;
99
100     /**
101      * Sync based on the selected child
102      */
103     static final int SYNC_SELECTED_POSITION = 0;
104
105     /**
106      * Sync based on the first child displayed
107      */
108     static final int SYNC_FIRST_POSITION = 1;
109
110     /**
111      * Maximum amount of time to spend in {@link #findSyncPosition()}
112      */
113     static final int SYNC_MAX_DURATION_MILLIS = 100;
114
115     /**
116      * Indicates that this view is currently being laid out.
117      */
118     boolean mInLayout = false;
119
120     /**
121      * The listener that receives notifications when an item is selected.
122      */
123     OnItemSelectedListener mOnItemSelectedListener;
124
125     /**
126      * The listener that receives notifications when an item is clicked.
127      */
128     OnItemClickListener mOnItemClickListener;
129
130     /**
131      * The listener that receives notifications when an item is long clicked.
132      */
133     OnItemLongClickListener mOnItemLongClickListener;
134
135     /**
136      * True if the data has changed since the last layout
137      */
138     boolean mDataChanged;
139
140     /**
141      * The position within the adapter's data set of the item to select
142      * during the next layout.
143      */
144     @ViewDebug.ExportedProperty(category = "list")
145     int mNextSelectedPosition = INVALID_POSITION;
146
147     /**
148      * The item id of the item to select during the next layout.
149      */
150     long mNextSelectedRowId = INVALID_ROW_ID;
151
152     /**
153      * The position within the adapter's data set of the currently selected item.
154      */
155     @ViewDebug.ExportedProperty(category = "list")
156     int mSelectedPosition = INVALID_POSITION;
157
158     /**
159      * The item id of the currently selected item.
160      */
161     long mSelectedRowId = INVALID_ROW_ID;
162
163     /**
164      * View to show if there are no items to show.
165      */
166     private View mEmptyView;
167
168     /**
169      * The number of items in the current adapter.
170      */
171     @ViewDebug.ExportedProperty(category = "list")
172     int mItemCount;
173
174     /**
175      * The number of items in the adapter before a data changed event occurred.
176      */
177     int mOldItemCount;
178
179     /**
180      * Represents an invalid position. All valid positions are in the range 0 to 1 less than the
181      * number of items in the current adapter.
182      */
183     public static final int INVALID_POSITION = -1;
184
185     /**
186      * Represents an empty or invalid row id
187      */
188     public static final long INVALID_ROW_ID = Long.MIN_VALUE;
189
190     /**
191      * The last selected position we used when notifying
192      */
193     int mOldSelectedPosition = INVALID_POSITION;
194     
195     /**
196      * The id of the last selected position we used when notifying
197      */
198     long mOldSelectedRowId = INVALID_ROW_ID;
199
200     /**
201      * Indicates what focusable state is requested when calling setFocusable().
202      * In addition to this, this view has other criteria for actually
203      * determining the focusable state (such as whether its empty or the text
204      * filter is shown).
205      *
206      * @see #setFocusable(boolean)
207      * @see #checkFocus()
208      */
209     private boolean mDesiredFocusableState;
210     private boolean mDesiredFocusableInTouchModeState;
211
212     private SelectionNotifier mSelectionNotifier;
213     /**
214      * When set to true, calls to requestLayout() will not propagate up the parent hierarchy.
215      * This is used to layout the children during a layout pass.
216      */
217     boolean mBlockLayoutRequests = false;
218
219     public AdapterView(Context context) {
220         super(context);
221     }
222
223     public AdapterView(Context context, AttributeSet attrs) {
224         super(context, attrs);
225     }
226
227     public AdapterView(Context context, AttributeSet attrs, int defStyle) {
228         super(context, attrs, defStyle);
229     }
230
231     /**
232      * Interface definition for a callback to be invoked when an item in this
233      * AdapterView has been clicked.
234      */
235     public interface OnItemClickListener {
236
237         /**
238          * Callback method to be invoked when an item in this AdapterView has
239          * been clicked.
240          * <p>
241          * Implementers can call getItemAtPosition(position) if they need
242          * to access the data associated with the selected item.
243          *
244          * @param parent The AdapterView where the click happened.
245          * @param view The view within the AdapterView that was clicked (this
246          *            will be a view provided by the adapter)
247          * @param position The position of the view in the adapter.
248          * @param id The row id of the item that was clicked.
249          */
250         void onItemClick(AdapterView<?> parent, View view, int position, long id);
251     }
252
253     /**
254      * Register a callback to be invoked when an item in this AdapterView has
255      * been clicked.
256      *
257      * @param listener The callback that will be invoked.
258      */
259     public void setOnItemClickListener(OnItemClickListener listener) {
260         mOnItemClickListener = listener;
261     }
262
263     /**
264      * @return The callback to be invoked with an item in this AdapterView has
265      *         been clicked, or null id no callback has been set.
266      */
267     public final OnItemClickListener getOnItemClickListener() {
268         return mOnItemClickListener;
269     }
270
271     /**
272      * Call the OnItemClickListener, if it is defined.
273      *
274      * @param view The view within the AdapterView that was clicked.
275      * @param position The position of the view in the adapter.
276      * @param id The row id of the item that was clicked.
277      * @return True if there was an assigned OnItemClickListener that was
278      *         called, false otherwise is returned.
279      */
280     public boolean performItemClick(View view, int position, long id) {
281         if (mOnItemClickListener != null) {
282             playSoundEffect(SoundEffectConstants.CLICK);
283             if (view != null) {
284                 view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
285             }
286             mOnItemClickListener.onItemClick(this, view, position, id);
287             return true;
288         }
289
290         return false;
291     }
292
293     /**
294      * Interface definition for a callback to be invoked when an item in this
295      * view has been clicked and held.
296      */
297     public interface OnItemLongClickListener {
298         /**
299          * Callback method to be invoked when an item in this view has been
300          * clicked and held.
301          *
302          * Implementers can call getItemAtPosition(position) if they need to access
303          * the data associated with the selected item.
304          *
305          * @param parent The AbsListView where the click happened
306          * @param view The view within the AbsListView that was clicked
307          * @param position The position of the view in the list
308          * @param id The row id of the item that was clicked
309          *
310          * @return true if the callback consumed the long click, false otherwise
311          */
312         boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id);
313     }
314
315
316     /**
317      * Register a callback to be invoked when an item in this AdapterView has
318      * been clicked and held
319      *
320      * @param listener The callback that will run
321      */
322     public void setOnItemLongClickListener(OnItemLongClickListener listener) {
323         if (!isLongClickable()) {
324             setLongClickable(true);
325         }
326         mOnItemLongClickListener = listener;
327     }
328
329     /**
330      * @return The callback to be invoked with an item in this AdapterView has
331      *         been clicked and held, or null id no callback as been set.
332      */
333     public final OnItemLongClickListener getOnItemLongClickListener() {
334         return mOnItemLongClickListener;
335     }
336
337     /**
338      * Interface definition for a callback to be invoked when
339      * an item in this view has been selected.
340      */
341     public interface OnItemSelectedListener {
342         /**
343          * <p>Callback method to be invoked when an item in this view has been
344          * selected. This callback is invoked only when the newly selected
345          * position is different from the previously selected position or if
346          * there was no selected item.</p>
347          *
348          * Impelmenters can call getItemAtPosition(position) if they need to access the
349          * data associated with the selected item.
350          *
351          * @param parent The AdapterView where the selection happened
352          * @param view The view within the AdapterView that was clicked
353          * @param position The position of the view in the adapter
354          * @param id The row id of the item that is selected
355          */
356         void onItemSelected(AdapterView<?> parent, View view, int position, long id);
357
358         /**
359          * Callback method to be invoked when the selection disappears from this
360          * view. The selection can disappear for instance when touch is activated
361          * or when the adapter becomes empty.
362          *
363          * @param parent The AdapterView that now contains no selected item.
364          */
365         void onNothingSelected(AdapterView<?> parent);
366     }
367
368
369     /**
370      * Register a callback to be invoked when an item in this AdapterView has
371      * been selected.
372      *
373      * @param listener The callback that will run
374      */
375     public void setOnItemSelectedListener(OnItemSelectedListener listener) {
376         mOnItemSelectedListener = listener;
377     }
378
379     public final OnItemSelectedListener getOnItemSelectedListener() {
380         return mOnItemSelectedListener;
381     }
382
383     /**
384      * Extra menu information provided to the
385      * {@link android.view.View.OnCreateContextMenuListener#onCreateContextMenu(ContextMenu, View, ContextMenuInfo) }
386      * callback when a context menu is brought up for this AdapterView.
387      *
388      */
389     public static class AdapterContextMenuInfo implements ContextMenu.ContextMenuInfo {
390
391         public AdapterContextMenuInfo(View targetView, int position, long id) {
392             this.targetView = targetView;
393             this.position = position;
394             this.id = id;
395         }
396
397         /**
398          * The child view for which the context menu is being displayed. This
399          * will be one of the children of this AdapterView.
400          */
401         public View targetView;
402
403         /**
404          * The position in the adapter for which the context menu is being
405          * displayed.
406          */
407         public int position;
408
409         /**
410          * The row id of the item for which the context menu is being displayed.
411          */
412         public long id;
413     }
414
415     /**
416      * Returns the adapter currently associated with this widget.
417      *
418      * @return The adapter used to provide this view's content.
419      */
420     public abstract T getAdapter();
421
422     /**
423      * Sets the adapter that provides the data and the views to represent the data
424      * in this widget.
425      *
426      * @param adapter The adapter to use to create this view's content.
427      */
428     public abstract void setAdapter(T adapter);
429
430     /**
431      * This method is not supported and throws an UnsupportedOperationException when called.
432      *
433      * @param child Ignored.
434      *
435      * @throws UnsupportedOperationException Every time this method is invoked.
436      */
437     @Override
438     public void addView(View child) {
439         throw new UnsupportedOperationException("addView(View) is not supported in AdapterView");
440     }
441
442     /**
443      * This method is not supported and throws an UnsupportedOperationException when called.
444      *
445      * @param child Ignored.
446      * @param index Ignored.
447      *
448      * @throws UnsupportedOperationException Every time this method is invoked.
449      */
450     @Override
451     public void addView(View child, int index) {
452         throw new UnsupportedOperationException("addView(View, int) is not supported in AdapterView");
453     }
454
455     /**
456      * This method is not supported and throws an UnsupportedOperationException when called.
457      *
458      * @param child Ignored.
459      * @param params Ignored.
460      *
461      * @throws UnsupportedOperationException Every time this method is invoked.
462      */
463     @Override
464     public void addView(View child, LayoutParams params) {
465         throw new UnsupportedOperationException("addView(View, LayoutParams) "
466                 + "is not supported in AdapterView");
467     }
468
469     /**
470      * This method is not supported and throws an UnsupportedOperationException when called.
471      *
472      * @param child Ignored.
473      * @param index Ignored.
474      * @param params Ignored.
475      *
476      * @throws UnsupportedOperationException Every time this method is invoked.
477      */
478     @Override
479     public void addView(View child, int index, LayoutParams params) {
480         throw new UnsupportedOperationException("addView(View, int, LayoutParams) "
481                 + "is not supported in AdapterView");
482     }
483
484     /**
485      * This method is not supported and throws an UnsupportedOperationException when called.
486      *
487      * @param child Ignored.
488      *
489      * @throws UnsupportedOperationException Every time this method is invoked.
490      */
491     @Override
492     public void removeView(View child) {
493         throw new UnsupportedOperationException("removeView(View) is not supported in AdapterView");
494     }
495
496     /**
497      * This method is not supported and throws an UnsupportedOperationException when called.
498      *
499      * @param index Ignored.
500      *
501      * @throws UnsupportedOperationException Every time this method is invoked.
502      */
503     @Override
504     public void removeViewAt(int index) {
505         throw new UnsupportedOperationException("removeViewAt(int) is not supported in AdapterView");
506     }
507
508     /**
509      * This method is not supported and throws an UnsupportedOperationException when called.
510      *
511      * @throws UnsupportedOperationException Every time this method is invoked.
512      */
513     @Override
514     public void removeAllViews() {
515         throw new UnsupportedOperationException("removeAllViews() is not supported in AdapterView");
516     }
517
518     @Override
519     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
520         mLayoutHeight = getHeight();
521     }
522
523     /**
524      * Return the position of the currently selected item within the adapter's data set
525      *
526      * @return int Position (starting at 0), or {@link #INVALID_POSITION} if there is nothing selected.
527      */
528     @ViewDebug.CapturedViewProperty
529     public int getSelectedItemPosition() {
530         return mNextSelectedPosition;
531     }
532
533     /**
534      * @return The id corresponding to the currently selected item, or {@link #INVALID_ROW_ID}
535      * if nothing is selected.
536      */
537     @ViewDebug.CapturedViewProperty
538     public long getSelectedItemId() {
539         return mNextSelectedRowId;
540     }
541
542     /**
543      * @return The view corresponding to the currently selected item, or null
544      * if nothing is selected
545      */
546     public abstract View getSelectedView();
547
548     /**
549      * @return The data corresponding to the currently selected item, or
550      * null if there is nothing selected.
551      */
552     public Object getSelectedItem() {
553         T adapter = getAdapter();
554         int selection = getSelectedItemPosition();
555         if (adapter != null && adapter.getCount() > 0 && selection >= 0) {
556             return adapter.getItem(selection);
557         } else {
558             return null;
559         }
560     }
561
562     /**
563      * @return The number of items owned by the Adapter associated with this
564      *         AdapterView. (This is the number of data items, which may be
565      *         larger than the number of visible views.)
566      */
567     @ViewDebug.CapturedViewProperty
568     public int getCount() {
569         return mItemCount;
570     }
571
572     /**
573      * Get the position within the adapter's data set for the view, where view is a an adapter item
574      * or a descendant of an adapter item.
575      *
576      * @param view an adapter item, or a descendant of an adapter item. This must be visible in this
577      *        AdapterView at the time of the call.
578      * @return the position within the adapter's data set of the view, or {@link #INVALID_POSITION}
579      *         if the view does not correspond to a list item (or it is not currently visible).
580      */
581     public int getPositionForView(View view) {
582         View listItem = view;
583         try {
584             View v;
585             while (!(v = (View) listItem.getParent()).equals(this)) {
586                 listItem = v;
587             }
588         } catch (ClassCastException e) {
589             // We made it up to the window without find this list view
590             return INVALID_POSITION;
591         }
592
593         // Search the children for the list item
594         final int childCount = getChildCount();
595         for (int i = 0; i < childCount; i++) {
596             if (getChildAt(i).equals(listItem)) {
597                 return mFirstPosition + i;
598             }
599         }
600
601         // Child not found!
602         return INVALID_POSITION;
603     }
604
605     /**
606      * Returns the position within the adapter's data set for the first item
607      * displayed on screen.
608      *
609      * @return The position within the adapter's data set
610      */
611     public int getFirstVisiblePosition() {
612         return mFirstPosition;
613     }
614
615     /**
616      * Returns the position within the adapter's data set for the last item
617      * displayed on screen.
618      *
619      * @return The position within the adapter's data set
620      */
621     public int getLastVisiblePosition() {
622         return mFirstPosition + getChildCount() - 1;
623     }
624
625     /**
626      * Sets the currently selected item. To support accessibility subclasses that
627      * override this method must invoke the overriden super method first.
628      *
629      * @param position Index (starting at 0) of the data item to be selected.
630      */
631     public abstract void setSelection(int position);
632
633     /**
634      * Sets the view to show if the adapter is empty
635      */
636     @android.view.RemotableViewMethod
637     public void setEmptyView(View emptyView) {
638         mEmptyView = emptyView;
639
640         final T adapter = getAdapter();
641         final boolean empty = ((adapter == null) || adapter.isEmpty());
642         updateEmptyStatus(empty);
643     }
644
645     /**
646      * When the current adapter is empty, the AdapterView can display a special view
647      * call the empty view. The empty view is used to provide feedback to the user
648      * that no data is available in this AdapterView.
649      *
650      * @return The view to show if the adapter is empty.
651      */
652     public View getEmptyView() {
653         return mEmptyView;
654     }
655
656     /**
657      * Indicates whether this view is in filter mode. Filter mode can for instance
658      * be enabled by a user when typing on the keyboard.
659      *
660      * @return True if the view is in filter mode, false otherwise.
661      */
662     boolean isInFilterMode() {
663         return false;
664     }
665
666     @Override
667     public void setFocusable(boolean focusable) {
668         final T adapter = getAdapter();
669         final boolean empty = adapter == null || adapter.getCount() == 0;
670
671         mDesiredFocusableState = focusable;
672         if (!focusable) {
673             mDesiredFocusableInTouchModeState = false;
674         }
675
676         super.setFocusable(focusable && (!empty || isInFilterMode()));
677     }
678
679     @Override
680     public void setFocusableInTouchMode(boolean focusable) {
681         final T adapter = getAdapter();
682         final boolean empty = adapter == null || adapter.getCount() == 0;
683
684         mDesiredFocusableInTouchModeState = focusable;
685         if (focusable) {
686             mDesiredFocusableState = true;
687         }
688
689         super.setFocusableInTouchMode(focusable && (!empty || isInFilterMode()));
690     }
691
692     void checkFocus() {
693         final T adapter = getAdapter();
694         final boolean empty = adapter == null || adapter.getCount() == 0;
695         final boolean focusable = !empty || isInFilterMode();
696         // The order in which we set focusable in touch mode/focusable may matter
697         // for the client, see View.setFocusableInTouchMode() comments for more
698         // details
699         super.setFocusableInTouchMode(focusable && mDesiredFocusableInTouchModeState);
700         super.setFocusable(focusable && mDesiredFocusableState);
701         if (mEmptyView != null) {
702             updateEmptyStatus((adapter == null) || adapter.isEmpty());
703         }
704     }
705
706     /**
707      * Update the status of the list based on the empty parameter.  If empty is true and
708      * we have an empty view, display it.  In all the other cases, make sure that the listview
709      * is VISIBLE and that the empty view is GONE (if it's not null).
710      */
711     private void updateEmptyStatus(boolean empty) {
712         if (isInFilterMode()) {
713             empty = false;
714         }
715
716         if (empty) {
717             if (mEmptyView != null) {
718                 mEmptyView.setVisibility(View.VISIBLE);
719                 setVisibility(View.GONE);
720             } else {
721                 // If the caller just removed our empty view, make sure the list view is visible
722                 setVisibility(View.VISIBLE);
723             }
724
725             // We are now GONE, so pending layouts will not be dispatched.
726             // Force one here to make sure that the state of the list matches
727             // the state of the adapter.
728             if (mDataChanged) {           
729                 this.onLayout(false, mLeft, mTop, mRight, mBottom); 
730             }
731         } else {
732             if (mEmptyView != null) mEmptyView.setVisibility(View.GONE);
733             setVisibility(View.VISIBLE);
734         }
735     }
736
737     /**
738      * Gets the data associated with the specified position in the list.
739      *
740      * @param position Which data to get
741      * @return The data associated with the specified position in the list
742      */
743     public Object getItemAtPosition(int position) {
744         T adapter = getAdapter();
745         return (adapter == null || position < 0) ? null : adapter.getItem(position);
746     }
747
748     public long getItemIdAtPosition(int position) {
749         T adapter = getAdapter();
750         return (adapter == null || position < 0) ? INVALID_ROW_ID : adapter.getItemId(position);
751     }
752
753     @Override
754     public void setOnClickListener(OnClickListener l) {
755         throw new RuntimeException("Don't call setOnClickListener for an AdapterView. "
756                 + "You probably want setOnItemClickListener instead");
757     }
758
759     /**
760      * Override to prevent freezing of any views created by the adapter.
761      */
762     @Override
763     protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
764         dispatchFreezeSelfOnly(container);
765     }
766
767     /**
768      * Override to prevent thawing of any views created by the adapter.
769      */
770     @Override
771     protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
772         dispatchThawSelfOnly(container);
773     }
774
775     class AdapterDataSetObserver extends DataSetObserver {
776
777         private Parcelable mInstanceState = null;
778
779         @Override
780         public void onChanged() {
781             mDataChanged = true;
782             mOldItemCount = mItemCount;
783             mItemCount = getAdapter().getCount();
784
785             // Detect the case where a cursor that was previously invalidated has
786             // been repopulated with new data.
787             if (AdapterView.this.getAdapter().hasStableIds() && mInstanceState != null
788                     && mOldItemCount == 0 && mItemCount > 0) {
789                 AdapterView.this.onRestoreInstanceState(mInstanceState);
790                 mInstanceState = null;
791             } else {
792                 rememberSyncState();
793             }
794             checkFocus();
795             requestLayout();
796         }
797
798         @Override
799         public void onInvalidated() {
800             mDataChanged = true;
801
802             if (AdapterView.this.getAdapter().hasStableIds()) {
803                 // Remember the current state for the case where our hosting activity is being
804                 // stopped and later restarted
805                 mInstanceState = AdapterView.this.onSaveInstanceState();
806             }
807
808             // Data is invalid so we should reset our state
809             mOldItemCount = mItemCount;
810             mItemCount = 0;
811             mSelectedPosition = INVALID_POSITION;
812             mSelectedRowId = INVALID_ROW_ID;
813             mNextSelectedPosition = INVALID_POSITION;
814             mNextSelectedRowId = INVALID_ROW_ID;
815             mNeedSync = false;
816
817             checkFocus();
818             requestLayout();
819         }
820
821         public void clearSavedState() {
822             mInstanceState = null;
823         }
824     }
825
826     @Override
827     protected void onDetachedFromWindow() {
828         super.onDetachedFromWindow();
829         removeCallbacks(mSelectionNotifier);
830     }
831
832     private class SelectionNotifier implements Runnable {
833         public void run() {
834             if (mDataChanged) {
835                 // Data has changed between when this SelectionNotifier
836                 // was posted and now. We need to wait until the AdapterView
837                 // has been synched to the new data.
838                 if (getAdapter() != null) {
839                     post(this);
840                 }
841             } else {
842                 fireOnSelected();
843             }
844         }
845     }
846
847     void selectionChanged() {
848         if (mOnItemSelectedListener != null) {
849             if (mInLayout || mBlockLayoutRequests) {
850                 // If we are in a layout traversal, defer notification
851                 // by posting. This ensures that the view tree is
852                 // in a consistent state and is able to accomodate
853                 // new layout or invalidate requests.
854                 if (mSelectionNotifier == null) {
855                     mSelectionNotifier = new SelectionNotifier();
856                 }
857                 post(mSelectionNotifier);
858             } else {
859                 fireOnSelected();
860             }
861         }
862
863         // we fire selection events here not in View
864         if (mSelectedPosition != ListView.INVALID_POSITION && isShown() && !isInTouchMode()) {
865             sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
866         }
867     }
868
869     private void fireOnSelected() {
870         if (mOnItemSelectedListener == null)
871             return;
872
873         int selection = this.getSelectedItemPosition();
874         if (selection >= 0) {
875             View v = getSelectedView();
876             mOnItemSelectedListener.onItemSelected(this, v, selection,
877                     getAdapter().getItemId(selection));
878         } else {
879             mOnItemSelectedListener.onNothingSelected(this);
880         }
881     }
882
883     @Override
884     public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
885         View selectedView = getSelectedView();
886         if (selectedView != null && selectedView.getVisibility() == VISIBLE
887                 && selectedView.dispatchPopulateAccessibilityEvent(event)) {
888             return true;
889         }
890         return false;
891     }
892
893     @Override
894     public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) {
895         if (super.onRequestSendAccessibilityEvent(child, event)) {
896             // Add a record for ourselves as well.
897             AccessibilityEvent record = AccessibilityEvent.obtain();
898             onInitializeAccessibilityEvent(record);
899             // Populate with the text of the requesting child.
900             child.dispatchPopulateAccessibilityEvent(record);
901             event.appendRecord(record);
902             return true;
903         }
904         return false;
905     }
906
907     @Override
908     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
909         super.onInitializeAccessibilityNodeInfo(info);
910         info.setScrollable(isScrollableForAccessibility());
911         View selectedView = getSelectedView();
912         if (selectedView != null) {
913             info.setEnabled(selectedView.isEnabled());
914         }
915     }
916
917     @Override
918     public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
919         super.onInitializeAccessibilityEvent(event);
920         event.setScrollable(isScrollableForAccessibility());
921         View selectedView = getSelectedView();
922         if (selectedView != null) {
923             event.setEnabled(selectedView.isEnabled());
924         }
925         event.setCurrentItemIndex(getSelectedItemPosition());
926         event.setFromIndex(getFirstVisiblePosition());
927         event.setToIndex(getLastVisiblePosition());
928         event.setItemCount(getAdapter().getCount());
929     }
930
931     private boolean isScrollableForAccessibility() {
932         T adapter = getAdapter();
933         if (adapter != null) {
934             final int itemCount = adapter.getCount();
935             return itemCount > 0
936                 && (getFirstVisiblePosition() > 0 || getLastVisiblePosition() < itemCount - 1);
937         }
938         return false;
939     }
940
941     @Override
942     protected boolean canAnimate() {
943         return super.canAnimate() && mItemCount > 0;
944     }
945
946     void handleDataChanged() {
947         final int count = mItemCount;
948         boolean found = false;
949
950         if (count > 0) {
951
952             int newPos;
953
954             // Find the row we are supposed to sync to
955             if (mNeedSync) {
956                 // Update this first, since setNextSelectedPositionInt inspects
957                 // it
958                 mNeedSync = false;
959
960                 // See if we can find a position in the new data with the same
961                 // id as the old selection
962                 newPos = findSyncPosition();
963                 if (newPos >= 0) {
964                     // Verify that new selection is selectable
965                     int selectablePos = lookForSelectablePosition(newPos, true);
966                     if (selectablePos == newPos) {
967                         // Same row id is selected
968                         setNextSelectedPositionInt(newPos);
969                         found = true;
970                     }
971                 }
972             }
973             if (!found) {
974                 // Try to use the same position if we can't find matching data
975                 newPos = getSelectedItemPosition();
976
977                 // Pin position to the available range
978                 if (newPos >= count) {
979                     newPos = count - 1;
980                 }
981                 if (newPos < 0) {
982                     newPos = 0;
983                 }
984
985                 // Make sure we select something selectable -- first look down
986                 int selectablePos = lookForSelectablePosition(newPos, true);
987                 if (selectablePos < 0) {
988                     // Looking down didn't work -- try looking up
989                     selectablePos = lookForSelectablePosition(newPos, false);
990                 }
991                 if (selectablePos >= 0) {
992                     setNextSelectedPositionInt(selectablePos);
993                     checkSelectionChanged();
994                     found = true;
995                 }
996             }
997         }
998         if (!found) {
999             // Nothing is selected
1000             mSelectedPosition = INVALID_POSITION;
1001             mSelectedRowId = INVALID_ROW_ID;
1002             mNextSelectedPosition = INVALID_POSITION;
1003             mNextSelectedRowId = INVALID_ROW_ID;
1004             mNeedSync = false;
1005             checkSelectionChanged();
1006         }
1007     }
1008
1009     void checkSelectionChanged() {
1010         if ((mSelectedPosition != mOldSelectedPosition) || (mSelectedRowId != mOldSelectedRowId)) {
1011             selectionChanged();
1012             mOldSelectedPosition = mSelectedPosition;
1013             mOldSelectedRowId = mSelectedRowId;
1014         }
1015     }
1016
1017     /**
1018      * Searches the adapter for a position matching mSyncRowId. The search starts at mSyncPosition
1019      * and then alternates between moving up and moving down until 1) we find the right position, or
1020      * 2) we run out of time, or 3) we have looked at every position
1021      *
1022      * @return Position of the row that matches mSyncRowId, or {@link #INVALID_POSITION} if it can't
1023      *         be found
1024      */
1025     int findSyncPosition() {
1026         int count = mItemCount;
1027
1028         if (count == 0) {
1029             return INVALID_POSITION;
1030         }
1031
1032         long idToMatch = mSyncRowId;
1033         int seed = mSyncPosition;
1034
1035         // If there isn't a selection don't hunt for it
1036         if (idToMatch == INVALID_ROW_ID) {
1037             return INVALID_POSITION;
1038         }
1039
1040         // Pin seed to reasonable values
1041         seed = Math.max(0, seed);
1042         seed = Math.min(count - 1, seed);
1043
1044         long endTime = SystemClock.uptimeMillis() + SYNC_MAX_DURATION_MILLIS;
1045
1046         long rowId;
1047
1048         // first position scanned so far
1049         int first = seed;
1050
1051         // last position scanned so far
1052         int last = seed;
1053
1054         // True if we should move down on the next iteration
1055         boolean next = false;
1056
1057         // True when we have looked at the first item in the data
1058         boolean hitFirst;
1059
1060         // True when we have looked at the last item in the data
1061         boolean hitLast;
1062
1063         // Get the item ID locally (instead of getItemIdAtPosition), so
1064         // we need the adapter
1065         T adapter = getAdapter();
1066         if (adapter == null) {
1067             return INVALID_POSITION;
1068         }
1069
1070         while (SystemClock.uptimeMillis() <= endTime) {
1071             rowId = adapter.getItemId(seed);
1072             if (rowId == idToMatch) {
1073                 // Found it!
1074                 return seed;
1075             }
1076
1077             hitLast = last == count - 1;
1078             hitFirst = first == 0;
1079
1080             if (hitLast && hitFirst) {
1081                 // Looked at everything
1082                 break;
1083             }
1084
1085             if (hitFirst || (next && !hitLast)) {
1086                 // Either we hit the top, or we are trying to move down
1087                 last++;
1088                 seed = last;
1089                 // Try going up next time
1090                 next = false;
1091             } else if (hitLast || (!next && !hitFirst)) {
1092                 // Either we hit the bottom, or we are trying to move up
1093                 first--;
1094                 seed = first;
1095                 // Try going down next time
1096                 next = true;
1097             }
1098
1099         }
1100
1101         return INVALID_POSITION;
1102     }
1103
1104     /**
1105      * Find a position that can be selected (i.e., is not a separator).
1106      *
1107      * @param position The starting position to look at.
1108      * @param lookDown Whether to look down for other positions.
1109      * @return The next selectable position starting at position and then searching either up or
1110      *         down. Returns {@link #INVALID_POSITION} if nothing can be found.
1111      */
1112     int lookForSelectablePosition(int position, boolean lookDown) {
1113         return position;
1114     }
1115
1116     /**
1117      * Utility to keep mSelectedPosition and mSelectedRowId in sync
1118      * @param position Our current position
1119      */
1120     void setSelectedPositionInt(int position) {
1121         mSelectedPosition = position;
1122         mSelectedRowId = getItemIdAtPosition(position);
1123     }
1124
1125     /**
1126      * Utility to keep mNextSelectedPosition and mNextSelectedRowId in sync
1127      * @param position Intended value for mSelectedPosition the next time we go
1128      * through layout
1129      */
1130     void setNextSelectedPositionInt(int position) {
1131         mNextSelectedPosition = position;
1132         mNextSelectedRowId = getItemIdAtPosition(position);
1133         // If we are trying to sync to the selection, update that too
1134         if (mNeedSync && mSyncMode == SYNC_SELECTED_POSITION && position >= 0) {
1135             mSyncPosition = position;
1136             mSyncRowId = mNextSelectedRowId;
1137         }
1138     }
1139
1140     /**
1141      * Remember enough information to restore the screen state when the data has
1142      * changed.
1143      *
1144      */
1145     void rememberSyncState() {
1146         if (getChildCount() > 0) {
1147             mNeedSync = true;
1148             mSyncHeight = mLayoutHeight;
1149             if (mSelectedPosition >= 0) {
1150                 // Sync the selection state
1151                 View v = getChildAt(mSelectedPosition - mFirstPosition);
1152                 mSyncRowId = mNextSelectedRowId;
1153                 mSyncPosition = mNextSelectedPosition;
1154                 if (v != null) {
1155                     mSpecificTop = v.getTop();
1156                 }
1157                 mSyncMode = SYNC_SELECTED_POSITION;
1158             } else {
1159                 // Sync the based on the offset of the first view
1160                 View v = getChildAt(0);
1161                 T adapter = getAdapter();
1162                 if (mFirstPosition >= 0 && mFirstPosition < adapter.getCount()) {
1163                     mSyncRowId = adapter.getItemId(mFirstPosition);
1164                 } else {
1165                     mSyncRowId = NO_ID;
1166                 }
1167                 mSyncPosition = mFirstPosition;
1168                 if (v != null) {
1169                     mSpecificTop = v.getTop();
1170                 }
1171                 mSyncMode = SYNC_FIRST_POSITION;
1172             }
1173         }
1174     }
1175 }