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