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