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