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