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