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