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