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