AdapterView.java revision f18c8ddd0f466de4b2f819fbbd612949bd71c532
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        view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
281
282        if (mOnItemClickListener != null) {
283            playSoundEffect(SoundEffectConstants.CLICK);
284            mOnItemClickListener.onItemClick(this, view, position, id);
285            return true;
286        }
287
288        return false;
289    }
290
291    /**
292     * Interface definition for a callback to be invoked when an item in this
293     * view has been clicked and held.
294     */
295    public interface OnItemLongClickListener {
296        /**
297         * Callback method to be invoked when an item in this view has been
298         * clicked and held.
299         *
300         * Implementers can call getItemAtPosition(position) if they need to access
301         * the data associated with the selected item.
302         *
303         * @param parent The AbsListView where the click happened
304         * @param view The view within the AbsListView that was clicked
305         * @param position The position of the view in the list
306         * @param id The row id of the item that was clicked
307         *
308         * @return true if the callback consumed the long click, false otherwise
309         */
310        boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id);
311    }
312
313
314    /**
315     * Register a callback to be invoked when an item in this AdapterView has
316     * been clicked and held
317     *
318     * @param listener The callback that will run
319     */
320    public void setOnItemLongClickListener(OnItemLongClickListener listener) {
321        if (!isLongClickable()) {
322            setLongClickable(true);
323        }
324        mOnItemLongClickListener = listener;
325    }
326
327    /**
328     * @return The callback to be invoked with an item in this AdapterView has
329     *         been clicked and held, or null id no callback as been set.
330     */
331    public final OnItemLongClickListener getOnItemLongClickListener() {
332        return mOnItemLongClickListener;
333    }
334
335    /**
336     * Interface definition for a callback to be invoked when
337     * an item in this view has been selected.
338     */
339    public interface OnItemSelectedListener {
340        /**
341         * <p>Callback method to be invoked when an item in this view has been
342         * selected. This callback is invoked only when the newly selected
343         * position is different from the previously selected position or if
344         * there was no selected item.</p>
345         *
346         * Impelmenters can call getItemAtPosition(position) if they need to access the
347         * data associated with the selected item.
348         *
349         * @param parent The AdapterView where the selection happened
350         * @param view The view within the AdapterView that was clicked
351         * @param position The position of the view in the adapter
352         * @param id The row id of the item that is selected
353         */
354        void onItemSelected(AdapterView<?> parent, View view, int position, long id);
355
356        /**
357         * Callback method to be invoked when the selection disappears from this
358         * view. The selection can disappear for instance when touch is activated
359         * or when the adapter becomes empty.
360         *
361         * @param parent The AdapterView that now contains no selected item.
362         */
363        void onNothingSelected(AdapterView<?> parent);
364    }
365
366
367    /**
368     * Register a callback to be invoked when an item in this AdapterView has
369     * been selected.
370     *
371     * @param listener The callback that will run
372     */
373    public void setOnItemSelectedListener(OnItemSelectedListener listener) {
374        mOnItemSelectedListener = listener;
375    }
376
377    public final OnItemSelectedListener getOnItemSelectedListener() {
378        return mOnItemSelectedListener;
379    }
380
381    /**
382     * Extra menu information provided to the
383     * {@link android.view.View.OnCreateContextMenuListener#onCreateContextMenu(ContextMenu, View, ContextMenuInfo) }
384     * callback when a context menu is brought up for this AdapterView.
385     *
386     */
387    public static class AdapterContextMenuInfo implements ContextMenu.ContextMenuInfo {
388
389        public AdapterContextMenuInfo(View targetView, int position, long id) {
390            this.targetView = targetView;
391            this.position = position;
392            this.id = id;
393        }
394
395        /**
396         * The child view for which the context menu is being displayed. This
397         * will be one of the children of this AdapterView.
398         */
399        public View targetView;
400
401        /**
402         * The position in the adapter for which the context menu is being
403         * displayed.
404         */
405        public int position;
406
407        /**
408         * The row id of the item for which the context menu is being displayed.
409         */
410        public long id;
411    }
412
413    /**
414     * Returns the adapter currently associated with this widget.
415     *
416     * @return The adapter used to provide this view's content.
417     */
418    public abstract T getAdapter();
419
420    /**
421     * Sets the adapter that provides the data and the views to represent the data
422     * in this widget.
423     *
424     * @param adapter The adapter to use to create this view's content.
425     */
426    public abstract void setAdapter(T adapter);
427
428    /**
429     * This method is not supported and throws an UnsupportedOperationException when called.
430     *
431     * @param child Ignored.
432     *
433     * @throws UnsupportedOperationException Every time this method is invoked.
434     */
435    @Override
436    public void addView(View child) {
437        throw new UnsupportedOperationException("addView(View) is not supported in AdapterView");
438    }
439
440    /**
441     * This method is not supported and throws an UnsupportedOperationException when called.
442     *
443     * @param child Ignored.
444     * @param index Ignored.
445     *
446     * @throws UnsupportedOperationException Every time this method is invoked.
447     */
448    @Override
449    public void addView(View child, int index) {
450        throw new UnsupportedOperationException("addView(View, int) is not supported in AdapterView");
451    }
452
453    /**
454     * This method is not supported and throws an UnsupportedOperationException when called.
455     *
456     * @param child Ignored.
457     * @param params Ignored.
458     *
459     * @throws UnsupportedOperationException Every time this method is invoked.
460     */
461    @Override
462    public void addView(View child, LayoutParams params) {
463        throw new UnsupportedOperationException("addView(View, LayoutParams) "
464                + "is not supported in AdapterView");
465    }
466
467    /**
468     * This method is not supported and throws an UnsupportedOperationException when called.
469     *
470     * @param child Ignored.
471     * @param index Ignored.
472     * @param params Ignored.
473     *
474     * @throws UnsupportedOperationException Every time this method is invoked.
475     */
476    @Override
477    public void addView(View child, int index, LayoutParams params) {
478        throw new UnsupportedOperationException("addView(View, int, LayoutParams) "
479                + "is not supported in AdapterView");
480    }
481
482    /**
483     * This method is not supported and throws an UnsupportedOperationException when called.
484     *
485     * @param child Ignored.
486     *
487     * @throws UnsupportedOperationException Every time this method is invoked.
488     */
489    @Override
490    public void removeView(View child) {
491        throw new UnsupportedOperationException("removeView(View) is not supported in AdapterView");
492    }
493
494    /**
495     * This method is not supported and throws an UnsupportedOperationException when called.
496     *
497     * @param index Ignored.
498     *
499     * @throws UnsupportedOperationException Every time this method is invoked.
500     */
501    @Override
502    public void removeViewAt(int index) {
503        throw new UnsupportedOperationException("removeViewAt(int) is not supported in AdapterView");
504    }
505
506    /**
507     * This method is not supported and throws an UnsupportedOperationException when called.
508     *
509     * @throws UnsupportedOperationException Every time this method is invoked.
510     */
511    @Override
512    public void removeAllViews() {
513        throw new UnsupportedOperationException("removeAllViews() is not supported in AdapterView");
514    }
515
516    @Override
517    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
518        mLayoutHeight = getHeight();
519    }
520
521    /**
522     * Return the position of the currently selected item within the adapter's data set
523     *
524     * @return int Position (starting at 0), or {@link #INVALID_POSITION} if there is nothing selected.
525     */
526    @ViewDebug.CapturedViewProperty
527    public int getSelectedItemPosition() {
528        return mNextSelectedPosition;
529    }
530
531    /**
532     * @return The id corresponding to the currently selected item, or {@link #INVALID_ROW_ID}
533     * if nothing is selected.
534     */
535    @ViewDebug.CapturedViewProperty
536    public long getSelectedItemId() {
537        return mNextSelectedRowId;
538    }
539
540    /**
541     * @return The view corresponding to the currently selected item, or null
542     * if nothing is selected
543     */
544    public abstract View getSelectedView();
545
546    /**
547     * @return The data corresponding to the currently selected item, or
548     * null if there is nothing selected.
549     */
550    public Object getSelectedItem() {
551        T adapter = getAdapter();
552        int selection = getSelectedItemPosition();
553        if (adapter != null && adapter.getCount() > 0 && selection >= 0) {
554            return adapter.getItem(selection);
555        } else {
556            return null;
557        }
558    }
559
560    /**
561     * @return The number of items owned by the Adapter associated with this
562     *         AdapterView. (This is the number of data items, which may be
563     *         larger than the number of visible views.)
564     */
565    @ViewDebug.CapturedViewProperty
566    public int getCount() {
567        return mItemCount;
568    }
569
570    /**
571     * Get the position within the adapter's data set for the view, where view is a an adapter item
572     * or a descendant of an adapter item.
573     *
574     * @param view an adapter item, or a descendant of an adapter item. This must be visible in this
575     *        AdapterView at the time of the call.
576     * @return the position within the adapter's data set of the view, or {@link #INVALID_POSITION}
577     *         if the view does not correspond to a list item (or it is not currently visible).
578     */
579    public int getPositionForView(View view) {
580        View listItem = view;
581        try {
582            View v;
583            while (!(v = (View) listItem.getParent()).equals(this)) {
584                listItem = v;
585            }
586        } catch (ClassCastException e) {
587            // We made it up to the window without find this list view
588            return INVALID_POSITION;
589        }
590
591        // Search the children for the list item
592        final int childCount = getChildCount();
593        for (int i = 0; i < childCount; i++) {
594            if (getChildAt(i).equals(listItem)) {
595                return mFirstPosition + i;
596            }
597        }
598
599        // Child not found!
600        return INVALID_POSITION;
601    }
602
603    /**
604     * Returns the position within the adapter's data set for the first item
605     * displayed on screen.
606     *
607     * @return The position within the adapter's data set
608     */
609    public int getFirstVisiblePosition() {
610        return mFirstPosition;
611    }
612
613    /**
614     * Returns the position within the adapter's data set for the last item
615     * displayed on screen.
616     *
617     * @return The position within the adapter's data set
618     */
619    public int getLastVisiblePosition() {
620        return mFirstPosition + getChildCount() - 1;
621    }
622
623    /**
624     * Sets the currently selected item. To support accessibility subclasses that
625     * override this method must invoke the overriden super method first.
626     *
627     * @param position Index (starting at 0) of the data item to be selected.
628     */
629    public abstract void setSelection(int position);
630
631    /**
632     * Sets the view to show if the adapter is empty
633     */
634    @android.view.RemotableViewMethod
635    public void setEmptyView(View emptyView) {
636        mEmptyView = emptyView;
637
638        final T adapter = getAdapter();
639        final boolean empty = ((adapter == null) || adapter.isEmpty());
640        updateEmptyStatus(empty);
641    }
642
643    /**
644     * When the current adapter is empty, the AdapterView can display a special view
645     * call the empty view. The empty view is used to provide feedback to the user
646     * that no data is available in this AdapterView.
647     *
648     * @return The view to show if the adapter is empty.
649     */
650    public View getEmptyView() {
651        return mEmptyView;
652    }
653
654    /**
655     * Indicates whether this view is in filter mode. Filter mode can for instance
656     * be enabled by a user when typing on the keyboard.
657     *
658     * @return True if the view is in filter mode, false otherwise.
659     */
660    boolean isInFilterMode() {
661        return false;
662    }
663
664    @Override
665    public void setFocusable(boolean focusable) {
666        final T adapter = getAdapter();
667        final boolean empty = adapter == null || adapter.getCount() == 0;
668
669        mDesiredFocusableState = focusable;
670        if (!focusable) {
671            mDesiredFocusableInTouchModeState = false;
672        }
673
674        super.setFocusable(focusable && (!empty || isInFilterMode()));
675    }
676
677    @Override
678    public void setFocusableInTouchMode(boolean focusable) {
679        final T adapter = getAdapter();
680        final boolean empty = adapter == null || adapter.getCount() == 0;
681
682        mDesiredFocusableInTouchModeState = focusable;
683        if (focusable) {
684            mDesiredFocusableState = true;
685        }
686
687        super.setFocusableInTouchMode(focusable && (!empty || isInFilterMode()));
688    }
689
690    void checkFocus() {
691        final T adapter = getAdapter();
692        final boolean empty = adapter == null || adapter.getCount() == 0;
693        final boolean focusable = !empty || isInFilterMode();
694        // The order in which we set focusable in touch mode/focusable may matter
695        // for the client, see View.setFocusableInTouchMode() comments for more
696        // details
697        super.setFocusableInTouchMode(focusable && mDesiredFocusableInTouchModeState);
698        super.setFocusable(focusable && mDesiredFocusableState);
699        if (mEmptyView != null) {
700            updateEmptyStatus((adapter == null) || adapter.isEmpty());
701        }
702    }
703
704    /**
705     * Update the status of the list based on the empty parameter.  If empty is true and
706     * we have an empty view, display it.  In all the other cases, make sure that the listview
707     * is VISIBLE and that the empty view is GONE (if it's not null).
708     */
709    private void updateEmptyStatus(boolean empty) {
710        if (isInFilterMode()) {
711            empty = false;
712        }
713
714        if (empty) {
715            if (mEmptyView != null) {
716                mEmptyView.setVisibility(View.VISIBLE);
717                setVisibility(View.GONE);
718            } else {
719                // If the caller just removed our empty view, make sure the list view is visible
720                setVisibility(View.VISIBLE);
721            }
722
723            // We are now GONE, so pending layouts will not be dispatched.
724            // Force one here to make sure that the state of the list matches
725            // the state of the adapter.
726            if (mDataChanged) {
727                this.onLayout(false, mLeft, mTop, mRight, mBottom);
728            }
729        } else {
730            if (mEmptyView != null) mEmptyView.setVisibility(View.GONE);
731            setVisibility(View.VISIBLE);
732        }
733    }
734
735    /**
736     * Gets the data associated with the specified position in the list.
737     *
738     * @param position Which data to get
739     * @return The data associated with the specified position in the list
740     */
741    public Object getItemAtPosition(int position) {
742        T adapter = getAdapter();
743        return (adapter == null || position < 0) ? null : adapter.getItem(position);
744    }
745
746    public long getItemIdAtPosition(int position) {
747        T adapter = getAdapter();
748        return (adapter == null || position < 0) ? INVALID_ROW_ID : adapter.getItemId(position);
749    }
750
751    @Override
752    public void setOnClickListener(OnClickListener l) {
753        throw new RuntimeException("Don't call setOnClickListener for an AdapterView. "
754                + "You probably want setOnItemClickListener instead");
755    }
756
757    /**
758     * Override to prevent freezing of any views created by the adapter.
759     */
760    @Override
761    protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
762        dispatchFreezeSelfOnly(container);
763    }
764
765    /**
766     * Override to prevent thawing of any views created by the adapter.
767     */
768    @Override
769    protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
770        dispatchThawSelfOnly(container);
771    }
772
773    class AdapterDataSetObserver extends DataSetObserver {
774
775        private Parcelable mInstanceState = null;
776
777        @Override
778        public void onChanged() {
779            mDataChanged = true;
780            mOldItemCount = mItemCount;
781            mItemCount = getAdapter().getCount();
782
783            // Detect the case where a cursor that was previously invalidated has
784            // been repopulated with new data.
785            if (AdapterView.this.getAdapter().hasStableIds() && mInstanceState != null
786                    && mOldItemCount == 0 && mItemCount > 0) {
787                AdapterView.this.onRestoreInstanceState(mInstanceState);
788                mInstanceState = null;
789            } else {
790                rememberSyncState();
791            }
792            checkFocus();
793            requestLayout();
794        }
795
796        @Override
797        public void onInvalidated() {
798            mDataChanged = true;
799
800            if (AdapterView.this.getAdapter().hasStableIds()) {
801                // Remember the current state for the case where our hosting activity is being
802                // stopped and later restarted
803                mInstanceState = AdapterView.this.onSaveInstanceState();
804            }
805
806            // Data is invalid so we should reset our state
807            mOldItemCount = mItemCount;
808            mItemCount = 0;
809            mSelectedPosition = INVALID_POSITION;
810            mSelectedRowId = INVALID_ROW_ID;
811            mNextSelectedPosition = INVALID_POSITION;
812            mNextSelectedRowId = INVALID_ROW_ID;
813            mNeedSync = false;
814
815            checkFocus();
816            requestLayout();
817        }
818
819        public void clearSavedState() {
820            mInstanceState = null;
821        }
822    }
823
824    @Override
825    protected void onDetachedFromWindow() {
826        super.onDetachedFromWindow();
827        removeCallbacks(mSelectionNotifier);
828    }
829
830    private class SelectionNotifier implements Runnable {
831        public void run() {
832            if (mDataChanged) {
833                // Data has changed between when this SelectionNotifier
834                // was posted and now. We need to wait until the AdapterView
835                // has been synched to the new data.
836                if (getAdapter() != null) {
837                    post(this);
838                }
839            } else {
840                fireOnSelected();
841            }
842        }
843    }
844
845    void selectionChanged() {
846        if (mOnItemSelectedListener != null) {
847            if (mInLayout || mBlockLayoutRequests) {
848                // If we are in a layout traversal, defer notification
849                // by posting. This ensures that the view tree is
850                // in a consistent state and is able to accomodate
851                // new layout or invalidate requests.
852                if (mSelectionNotifier == null) {
853                    mSelectionNotifier = new SelectionNotifier();
854                }
855                post(mSelectionNotifier);
856            } else {
857                fireOnSelected();
858            }
859        }
860
861        // we fire selection events here not in View
862        if (mSelectedPosition != ListView.INVALID_POSITION && isShown() && !isInTouchMode()) {
863            sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
864        }
865    }
866
867    private void fireOnSelected() {
868        if (mOnItemSelectedListener == null)
869            return;
870
871        int selection = this.getSelectedItemPosition();
872        if (selection >= 0) {
873            View v = getSelectedView();
874            mOnItemSelectedListener.onItemSelected(this, v, selection,
875                    getAdapter().getItemId(selection));
876        } else {
877            mOnItemSelectedListener.onNothingSelected(this);
878        }
879    }
880
881    @Override
882    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
883        // This is an exceptional case which occurs when a window gets the
884        // focus and sends a focus event via its focused child to announce
885        // current focus/selection. AdapterView fires selection but not focus
886        // events so we change the event type here.
887        if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED) {
888            event.setEventType(AccessibilityEvent.TYPE_VIEW_SELECTED);
889        }
890
891        View selectedView = getSelectedView();
892        if (selectedView != null && selectedView.getVisibility() == VISIBLE) {
893            // We first get a chance to populate the event.
894            onPopulateAccessibilityEvent(event);
895        }
896        return false;
897    }
898
899    @Override
900    public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
901        super.onPopulateAccessibilityEvent(event);
902        // We send selection events only from AdapterView to avoid
903        // generation of such event for each child.
904        getSelectedView().dispatchPopulateAccessibilityEvent(event);
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