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