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