AbsListView.java revision 0a77ce277c6ed2aa25bbea5f8cd5687c0720cb68
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 com.android.internal.R;
20
21import android.content.Context;
22import android.content.res.Resources;
23import android.content.res.TypedArray;
24import android.graphics.Canvas;
25import android.graphics.Rect;
26import android.graphics.drawable.Drawable;
27import android.graphics.drawable.TransitionDrawable;
28import android.os.Debug;
29import android.os.Handler;
30import android.os.Parcel;
31import android.os.Parcelable;
32import android.text.Editable;
33import android.text.TextUtils;
34import android.text.TextWatcher;
35import android.util.AttributeSet;
36import android.util.Log;
37import android.view.ContextMenu.ContextMenuInfo;
38import android.view.Gravity;
39import android.view.HapticFeedbackConstants;
40import android.view.KeyEvent;
41import android.view.LayoutInflater;
42import android.view.MotionEvent;
43import android.view.VelocityTracker;
44import android.view.View;
45import android.view.ViewConfiguration;
46import android.view.ViewDebug;
47import android.view.ViewGroup;
48import android.view.ViewTreeObserver;
49import android.view.animation.AnimationUtils;
50import android.view.inputmethod.BaseInputConnection;
51import android.view.inputmethod.EditorInfo;
52import android.view.inputmethod.InputConnection;
53import android.view.inputmethod.InputConnectionWrapper;
54import android.view.inputmethod.InputMethodManager;
55
56import java.util.ArrayList;
57import java.util.List;
58
59/**
60 * Base class that can be used to implement virtualized lists of items. A list does
61 * not have a spatial definition here. For instance, subclases of this class can
62 * display the content of the list in a grid, in a carousel, as stack, etc.
63 *
64 * @attr ref android.R.styleable#AbsListView_listSelector
65 * @attr ref android.R.styleable#AbsListView_drawSelectorOnTop
66 * @attr ref android.R.styleable#AbsListView_stackFromBottom
67 * @attr ref android.R.styleable#AbsListView_scrollingCache
68 * @attr ref android.R.styleable#AbsListView_textFilterEnabled
69 * @attr ref android.R.styleable#AbsListView_transcriptMode
70 * @attr ref android.R.styleable#AbsListView_cacheColorHint
71 * @attr ref android.R.styleable#AbsListView_fastScrollEnabled
72 * @attr ref android.R.styleable#AbsListView_smoothScrollbar
73 */
74public abstract class AbsListView extends AdapterView<ListAdapter> implements TextWatcher,
75        ViewTreeObserver.OnGlobalLayoutListener, Filter.FilterListener,
76        ViewTreeObserver.OnTouchModeChangeListener {
77
78    /**
79     * Disables the transcript mode.
80     *
81     * @see #setTranscriptMode(int)
82     */
83    public static final int TRANSCRIPT_MODE_DISABLED = 0;
84    /**
85     * The list will automatically scroll to the bottom when a data set change
86     * notification is received and only if the last item is already visible
87     * on screen.
88     *
89     * @see #setTranscriptMode(int)
90     */
91    public static final int TRANSCRIPT_MODE_NORMAL = 1;
92    /**
93     * The list will automatically scroll to the bottom, no matter what items
94     * are currently visible.
95     *
96     * @see #setTranscriptMode(int)
97     */
98    public static final int TRANSCRIPT_MODE_ALWAYS_SCROLL = 2;
99
100    /**
101     * Indicates that we are not in the middle of a touch gesture
102     */
103    static final int TOUCH_MODE_REST = -1;
104
105    /**
106     * Indicates we just received the touch event and we are waiting to see if the it is a tap or a
107     * scroll gesture.
108     */
109    static final int TOUCH_MODE_DOWN = 0;
110
111    /**
112     * Indicates the touch has been recognized as a tap and we are now waiting to see if the touch
113     * is a longpress
114     */
115    static final int TOUCH_MODE_TAP = 1;
116
117    /**
118     * Indicates we have waited for everything we can wait for, but the user's finger is still down
119     */
120    static final int TOUCH_MODE_DONE_WAITING = 2;
121
122    /**
123     * Indicates the touch gesture is a scroll
124     */
125    static final int TOUCH_MODE_SCROLL = 3;
126
127    /**
128     * Indicates the view is in the process of being flung
129     */
130    static final int TOUCH_MODE_FLING = 4;
131
132    /**
133     * Indicates the touch gesture is an overscroll - a scroll beyond the beginning or end.
134     */
135    static final int TOUCH_MODE_OVERSCROLL = 5;
136
137    /**
138     * Indicates the view is being flung outside of normal content bounds
139     * and will spring back.
140     */
141    static final int TOUCH_MODE_OVERFLING = 6;
142
143    /**
144     * Regular layout - usually an unsolicited layout from the view system
145     */
146    static final int LAYOUT_NORMAL = 0;
147
148    /**
149     * Show the first item
150     */
151    static final int LAYOUT_FORCE_TOP = 1;
152
153    /**
154     * Force the selected item to be on somewhere on the screen
155     */
156    static final int LAYOUT_SET_SELECTION = 2;
157
158    /**
159     * Show the last item
160     */
161    static final int LAYOUT_FORCE_BOTTOM = 3;
162
163    /**
164     * Make a mSelectedItem appear in a specific location and build the rest of
165     * the views from there. The top is specified by mSpecificTop.
166     */
167    static final int LAYOUT_SPECIFIC = 4;
168
169    /**
170     * Layout to sync as a result of a data change. Restore mSyncPosition to have its top
171     * at mSpecificTop
172     */
173    static final int LAYOUT_SYNC = 5;
174
175    /**
176     * Layout as a result of using the navigation keys
177     */
178    static final int LAYOUT_MOVE_SELECTION = 6;
179
180    /**
181     * Controls how the next layout will happen
182     */
183    int mLayoutMode = LAYOUT_NORMAL;
184
185    /**
186     * Should be used by subclasses to listen to changes in the dataset
187     */
188    AdapterDataSetObserver mDataSetObserver;
189
190    /**
191     * The adapter containing the data to be displayed by this view
192     */
193    ListAdapter mAdapter;
194
195    /**
196     * Indicates whether the list selector should be drawn on top of the children or behind
197     */
198    boolean mDrawSelectorOnTop = false;
199
200    /**
201     * The drawable used to draw the selector
202     */
203    Drawable mSelector;
204
205    /**
206     * Defines the selector's location and dimension at drawing time
207     */
208    Rect mSelectorRect = new Rect();
209
210    /**
211     * The data set used to store unused views that should be reused during the next layout
212     * to avoid creating new ones
213     */
214    final RecycleBin mRecycler = new RecycleBin();
215
216    /**
217     * The selection's left padding
218     */
219    int mSelectionLeftPadding = 0;
220
221    /**
222     * The selection's top padding
223     */
224    int mSelectionTopPadding = 0;
225
226    /**
227     * The selection's right padding
228     */
229    int mSelectionRightPadding = 0;
230
231    /**
232     * The selection's bottom padding
233     */
234    int mSelectionBottomPadding = 0;
235
236    /**
237     * This view's padding
238     */
239    Rect mListPadding = new Rect();
240
241    /**
242     * Subclasses must retain their measure spec from onMeasure() into this member
243     */
244    int mWidthMeasureSpec = 0;
245
246    /**
247     * The top scroll indicator
248     */
249    View mScrollUp;
250
251    /**
252     * The down scroll indicator
253     */
254    View mScrollDown;
255
256    /**
257     * When the view is scrolling, this flag is set to true to indicate subclasses that
258     * the drawing cache was enabled on the children
259     */
260    boolean mCachingStarted;
261
262    /**
263     * The position of the view that received the down motion event
264     */
265    int mMotionPosition;
266
267    /**
268     * The offset to the top of the mMotionPosition view when the down motion event was received
269     */
270    int mMotionViewOriginalTop;
271
272    /**
273     * The desired offset to the top of the mMotionPosition view after a scroll
274     */
275    int mMotionViewNewTop;
276
277    /**
278     * The X value associated with the the down motion event
279     */
280    int mMotionX;
281
282    /**
283     * The Y value associated with the the down motion event
284     */
285    int mMotionY;
286
287    /**
288     * One of TOUCH_MODE_REST, TOUCH_MODE_DOWN, TOUCH_MODE_TAP, TOUCH_MODE_SCROLL, or
289     * TOUCH_MODE_DONE_WAITING
290     */
291    int mTouchMode = TOUCH_MODE_REST;
292
293    /**
294     * Y value from on the previous motion event (if any)
295     */
296    int mLastY;
297
298    /**
299     * How far the finger moved before we started scrolling
300     */
301    int mMotionCorrection;
302
303    /**
304     * Determines speed during touch scrolling
305     */
306    private VelocityTracker mVelocityTracker;
307
308    /**
309     * Handles one frame of a fling
310     */
311    private FlingRunnable mFlingRunnable;
312
313    /**
314     * Handles scrolling between positions within the list.
315     */
316    private PositionScroller mPositionScroller;
317
318    /**
319     * The offset in pixels form the top of the AdapterView to the top
320     * of the currently selected view. Used to save and restore state.
321     */
322    int mSelectedTop = 0;
323
324    /**
325     * Indicates whether the list is stacked from the bottom edge or
326     * the top edge.
327     */
328    boolean mStackFromBottom;
329
330    /**
331     * When set to true, the list automatically discards the children's
332     * bitmap cache after scrolling.
333     */
334    boolean mScrollingCacheEnabled;
335
336    /**
337     * Whether or not to enable the fast scroll feature on this list
338     */
339    boolean mFastScrollEnabled;
340
341    /**
342     * Optional callback to notify client when scroll position has changed
343     */
344    private OnScrollListener mOnScrollListener;
345
346    /**
347     * Keeps track of our accessory window
348     */
349    PopupWindow mPopup;
350
351    /**
352     * Used with type filter window
353     */
354    EditText mTextFilter;
355
356    /**
357     * Indicates whether to use pixels-based or position-based scrollbar
358     * properties.
359     */
360    private boolean mSmoothScrollbarEnabled = true;
361
362    /**
363     * Indicates that this view supports filtering
364     */
365    private boolean mTextFilterEnabled;
366
367    /**
368     * Indicates that this view is currently displaying a filtered view of the data
369     */
370    private boolean mFiltered;
371
372    /**
373     * Rectangle used for hit testing children
374     */
375    private Rect mTouchFrame;
376
377    /**
378     * The position to resurrect the selected position to.
379     */
380    int mResurrectToPosition = INVALID_POSITION;
381
382    private ContextMenuInfo mContextMenuInfo = null;
383
384    /**
385     * Maximum distance to record overscroll
386     */
387    int mOverscrollMax;
388
389    /**
390     * Content height divided by this is the overscroll limit.
391     */
392    static final int OVERSCROLL_LIMIT_DIVISOR = 3;
393
394    /**
395     * Used to request a layout when we changed touch mode
396     */
397    private static final int TOUCH_MODE_UNKNOWN = -1;
398    private static final int TOUCH_MODE_ON = 0;
399    private static final int TOUCH_MODE_OFF = 1;
400
401    private int mLastTouchMode = TOUCH_MODE_UNKNOWN;
402
403    private static final boolean PROFILE_SCROLLING = false;
404    private boolean mScrollProfilingStarted = false;
405
406    private static final boolean PROFILE_FLINGING = false;
407    private boolean mFlingProfilingStarted = false;
408
409    /**
410     * The last CheckForLongPress runnable we posted, if any
411     */
412    private CheckForLongPress mPendingCheckForLongPress;
413
414    /**
415     * The last CheckForTap runnable we posted, if any
416     */
417    private Runnable mPendingCheckForTap;
418
419    /**
420     * The last CheckForKeyLongPress runnable we posted, if any
421     */
422    private CheckForKeyLongPress mPendingCheckForKeyLongPress;
423
424    /**
425     * Acts upon click
426     */
427    private AbsListView.PerformClick mPerformClick;
428
429    /**
430     * This view is in transcript mode -- it shows the bottom of the list when the data
431     * changes
432     */
433    private int mTranscriptMode;
434
435    /**
436     * Indicates that this list is always drawn on top of a solid, single-color, opaque
437     * background
438     */
439    private int mCacheColorHint;
440
441    /**
442     * The select child's view (from the adapter's getView) is enabled.
443     */
444    private boolean mIsChildViewEnabled;
445
446    /**
447     * The last scroll state reported to clients through {@link OnScrollListener}.
448     */
449    private int mLastScrollState = OnScrollListener.SCROLL_STATE_IDLE;
450
451    /**
452     * Helper object that renders and controls the fast scroll thumb.
453     */
454    private FastScroller mFastScroller;
455
456    private boolean mGlobalLayoutListenerAddedFilter;
457
458    private int mTouchSlop;
459    private float mDensityScale;
460
461    private InputConnection mDefInputConnection;
462    private InputConnectionWrapper mPublicInputConnection;
463
464    private Runnable mClearScrollingCache;
465    private int mMinimumVelocity;
466    private int mMaximumVelocity;
467
468    final boolean[] mIsScrap = new boolean[1];
469
470    // True when the popup should be hidden because of a call to
471    // dispatchDisplayHint()
472    private boolean mPopupHidden;
473
474    /**
475     * ID of the active pointer. This is used to retain consistency during
476     * drags/flings if multiple pointers are used.
477     */
478    private int mActivePointerId = INVALID_POINTER;
479
480    /**
481     * Sentinel value for no current active pointer.
482     * Used by {@link #mActivePointerId}.
483     */
484    private static final int INVALID_POINTER = -1;
485
486    /**
487     * Maximum distance to overscroll by during edge effects
488     */
489    int mOverscrollDistance;
490
491    /**
492     * Maximum distance to overfling during edge effects
493     */
494    int mOverflingDistance;
495
496    // These two EdgeGlows are always set and used together.
497    // Checking one for null is as good as checking both.
498
499    /**
500     * Tracks the state of the top edge glow.
501     */
502    private EdgeGlow mEdgeGlowTop;
503
504    /**
505     * Tracks the state of the bottom edge glow.
506     */
507    private EdgeGlow mEdgeGlowBottom;
508
509    /**
510     * Interface definition for a callback to be invoked when the list or grid
511     * has been scrolled.
512     */
513    public interface OnScrollListener {
514
515        /**
516         * The view is not scrolling. Note navigating the list using the trackball counts as
517         * being in the idle state since these transitions are not animated.
518         */
519        public static int SCROLL_STATE_IDLE = 0;
520
521        /**
522         * The user is scrolling using touch, and their finger is still on the screen
523         */
524        public static int SCROLL_STATE_TOUCH_SCROLL = 1;
525
526        /**
527         * The user had previously been scrolling using touch and had performed a fling. The
528         * animation is now coasting to a stop
529         */
530        public static int SCROLL_STATE_FLING = 2;
531
532        /**
533         * Callback method to be invoked while the list view or grid view is being scrolled. If the
534         * view is being scrolled, this method will be called before the next frame of the scroll is
535         * rendered. In particular, it will be called before any calls to
536         * {@link Adapter#getView(int, View, ViewGroup)}.
537         *
538         * @param view The view whose scroll state is being reported
539         *
540         * @param scrollState The current scroll state. One of {@link #SCROLL_STATE_IDLE},
541         * {@link #SCROLL_STATE_TOUCH_SCROLL} or {@link #SCROLL_STATE_IDLE}.
542         */
543        public void onScrollStateChanged(AbsListView view, int scrollState);
544
545        /**
546         * Callback method to be invoked when the list or grid has been scrolled. This will be
547         * called after the scroll has completed
548         * @param view The view whose scroll state is being reported
549         * @param firstVisibleItem the index of the first visible cell (ignore if
550         *        visibleItemCount == 0)
551         * @param visibleItemCount the number of visible cells
552         * @param totalItemCount the number of items in the list adaptor
553         */
554        public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
555                int totalItemCount);
556    }
557
558    public AbsListView(Context context) {
559        super(context);
560        initAbsListView();
561
562        setVerticalScrollBarEnabled(true);
563        TypedArray a = context.obtainStyledAttributes(R.styleable.View);
564        initializeScrollbars(a);
565        a.recycle();
566    }
567
568    public AbsListView(Context context, AttributeSet attrs) {
569        this(context, attrs, com.android.internal.R.attr.absListViewStyle);
570    }
571
572    public AbsListView(Context context, AttributeSet attrs, int defStyle) {
573        super(context, attrs, defStyle);
574        initAbsListView();
575
576        TypedArray a = context.obtainStyledAttributes(attrs,
577                com.android.internal.R.styleable.AbsListView, defStyle, 0);
578
579        Drawable d = a.getDrawable(com.android.internal.R.styleable.AbsListView_listSelector);
580        if (d != null) {
581            setSelector(d);
582        }
583
584        mDrawSelectorOnTop = a.getBoolean(
585                com.android.internal.R.styleable.AbsListView_drawSelectorOnTop, false);
586
587        boolean stackFromBottom = a.getBoolean(R.styleable.AbsListView_stackFromBottom, false);
588        setStackFromBottom(stackFromBottom);
589
590        boolean scrollingCacheEnabled = a.getBoolean(R.styleable.AbsListView_scrollingCache, true);
591        setScrollingCacheEnabled(scrollingCacheEnabled);
592
593        boolean useTextFilter = a.getBoolean(R.styleable.AbsListView_textFilterEnabled, false);
594        setTextFilterEnabled(useTextFilter);
595
596        int transcriptMode = a.getInt(R.styleable.AbsListView_transcriptMode,
597                TRANSCRIPT_MODE_DISABLED);
598        setTranscriptMode(transcriptMode);
599
600        int color = a.getColor(R.styleable.AbsListView_cacheColorHint, 0);
601        setCacheColorHint(color);
602
603        boolean enableFastScroll = a.getBoolean(R.styleable.AbsListView_fastScrollEnabled, false);
604        setFastScrollEnabled(enableFastScroll);
605
606        boolean smoothScrollbar = a.getBoolean(R.styleable.AbsListView_smoothScrollbar, true);
607        setSmoothScrollbarEnabled(smoothScrollbar);
608
609        a.recycle();
610    }
611
612    private void initAbsListView() {
613        // Setting focusable in touch mode will set the focusable property to true
614        setClickable(true);
615        setFocusableInTouchMode(true);
616        setWillNotDraw(false);
617        setAlwaysDrawnWithCacheEnabled(false);
618        setScrollingCacheEnabled(true);
619
620        final ViewConfiguration configuration = ViewConfiguration.get(mContext);
621        mTouchSlop = configuration.getScaledTouchSlop();
622        mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
623        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
624        mOverscrollDistance = configuration.getScaledOverscrollDistance();
625        mOverflingDistance = configuration.getScaledOverflingDistance();
626
627        mDensityScale = getContext().getResources().getDisplayMetrics().density;
628    }
629
630    @Override
631    public void setOverscrollMode(int mode) {
632        if (mode != OVERSCROLL_NEVER) {
633            if (mEdgeGlowTop == null) {
634                final Resources res = getContext().getResources();
635                final Drawable edge = res.getDrawable(R.drawable.edge_light);
636                final Drawable glow = res.getDrawable(R.drawable.overscroll_glow);
637                mEdgeGlowTop = new EdgeGlow(edge, glow);
638                mEdgeGlowBottom = new EdgeGlow(edge, glow);
639            }
640        } else {
641            mEdgeGlowTop = null;
642            mEdgeGlowBottom = null;
643        }
644        super.setOverscrollMode(mode);
645    }
646
647    /**
648     * Enables fast scrolling by letting the user quickly scroll through lists by
649     * dragging the fast scroll thumb. The adapter attached to the list may want
650     * to implement {@link SectionIndexer} if it wishes to display alphabet preview and
651     * jump between sections of the list.
652     * @see SectionIndexer
653     * @see #isFastScrollEnabled()
654     * @param enabled whether or not to enable fast scrolling
655     */
656    public void setFastScrollEnabled(boolean enabled) {
657        mFastScrollEnabled = enabled;
658        if (enabled) {
659            if (mFastScroller == null) {
660                mFastScroller = new FastScroller(getContext(), this);
661            }
662        } else {
663            if (mFastScroller != null) {
664                mFastScroller.stop();
665                mFastScroller = null;
666            }
667        }
668    }
669
670    /**
671     * Returns the current state of the fast scroll feature.
672     * @see #setFastScrollEnabled(boolean)
673     * @return true if fast scroll is enabled, false otherwise
674     */
675    @ViewDebug.ExportedProperty
676    public boolean isFastScrollEnabled() {
677        return mFastScrollEnabled;
678    }
679
680    /**
681     * If fast scroll is visible, then don't draw the vertical scrollbar.
682     * @hide
683     */
684    @Override
685    protected boolean isVerticalScrollBarHidden() {
686        return mFastScroller != null && mFastScroller.isVisible();
687    }
688
689    /**
690     * When smooth scrollbar is enabled, the position and size of the scrollbar thumb
691     * is computed based on the number of visible pixels in the visible items. This
692     * however assumes that all list items have the same height. If you use a list in
693     * which items have different heights, the scrollbar will change appearance as the
694     * user scrolls through the list. To avoid this issue, you need to disable this
695     * property.
696     *
697     * When smooth scrollbar is disabled, the position and size of the scrollbar thumb
698     * is based solely on the number of items in the adapter and the position of the
699     * visible items inside the adapter. This provides a stable scrollbar as the user
700     * navigates through a list of items with varying heights.
701     *
702     * @param enabled Whether or not to enable smooth scrollbar.
703     *
704     * @see #setSmoothScrollbarEnabled(boolean)
705     * @attr ref android.R.styleable#AbsListView_smoothScrollbar
706     */
707    public void setSmoothScrollbarEnabled(boolean enabled) {
708        mSmoothScrollbarEnabled = enabled;
709    }
710
711    /**
712     * Returns the current state of the fast scroll feature.
713     *
714     * @return True if smooth scrollbar is enabled is enabled, false otherwise.
715     *
716     * @see #setSmoothScrollbarEnabled(boolean)
717     */
718    @ViewDebug.ExportedProperty
719    public boolean isSmoothScrollbarEnabled() {
720        return mSmoothScrollbarEnabled;
721    }
722
723    /**
724     * Set the listener that will receive notifications every time the list scrolls.
725     *
726     * @param l the scroll listener
727     */
728    public void setOnScrollListener(OnScrollListener l) {
729        mOnScrollListener = l;
730        invokeOnItemScrollListener();
731    }
732
733    /**
734     * Notify our scroll listener (if there is one) of a change in scroll state
735     */
736    void invokeOnItemScrollListener() {
737        if (mFastScroller != null) {
738            mFastScroller.onScroll(this, mFirstPosition, getChildCount(), mItemCount);
739        }
740        if (mOnScrollListener != null) {
741            mOnScrollListener.onScroll(this, mFirstPosition, getChildCount(), mItemCount);
742        }
743    }
744
745    /**
746     * Indicates whether the children's drawing cache is used during a scroll.
747     * By default, the drawing cache is enabled but this will consume more memory.
748     *
749     * @return true if the scrolling cache is enabled, false otherwise
750     *
751     * @see #setScrollingCacheEnabled(boolean)
752     * @see View#setDrawingCacheEnabled(boolean)
753     */
754    @ViewDebug.ExportedProperty
755    public boolean isScrollingCacheEnabled() {
756        return mScrollingCacheEnabled;
757    }
758
759    /**
760     * Enables or disables the children's drawing cache during a scroll.
761     * By default, the drawing cache is enabled but this will use more memory.
762     *
763     * When the scrolling cache is enabled, the caches are kept after the
764     * first scrolling. You can manually clear the cache by calling
765     * {@link android.view.ViewGroup#setChildrenDrawingCacheEnabled(boolean)}.
766     *
767     * @param enabled true to enable the scroll cache, false otherwise
768     *
769     * @see #isScrollingCacheEnabled()
770     * @see View#setDrawingCacheEnabled(boolean)
771     */
772    public void setScrollingCacheEnabled(boolean enabled) {
773        if (mScrollingCacheEnabled && !enabled) {
774            clearScrollingCache();
775        }
776        mScrollingCacheEnabled = enabled;
777    }
778
779    /**
780     * Enables or disables the type filter window. If enabled, typing when
781     * this view has focus will filter the children to match the users input.
782     * Note that the {@link Adapter} used by this view must implement the
783     * {@link Filterable} interface.
784     *
785     * @param textFilterEnabled true to enable type filtering, false otherwise
786     *
787     * @see Filterable
788     */
789    public void setTextFilterEnabled(boolean textFilterEnabled) {
790        mTextFilterEnabled = textFilterEnabled;
791    }
792
793    /**
794     * Indicates whether type filtering is enabled for this view
795     *
796     * @return true if type filtering is enabled, false otherwise
797     *
798     * @see #setTextFilterEnabled(boolean)
799     * @see Filterable
800     */
801    @ViewDebug.ExportedProperty
802    public boolean isTextFilterEnabled() {
803        return mTextFilterEnabled;
804    }
805
806    @Override
807    public void getFocusedRect(Rect r) {
808        View view = getSelectedView();
809        if (view != null && view.getParent() == this) {
810            // the focused rectangle of the selected view offset into the
811            // coordinate space of this view.
812            view.getFocusedRect(r);
813            offsetDescendantRectToMyCoords(view, r);
814        } else {
815            // otherwise, just the norm
816            super.getFocusedRect(r);
817        }
818    }
819
820    private void useDefaultSelector() {
821        setSelector(getResources().getDrawable(
822                com.android.internal.R.drawable.list_selector_background));
823    }
824
825    /**
826     * Indicates whether the content of this view is pinned to, or stacked from,
827     * the bottom edge.
828     *
829     * @return true if the content is stacked from the bottom edge, false otherwise
830     */
831    @ViewDebug.ExportedProperty
832    public boolean isStackFromBottom() {
833        return mStackFromBottom;
834    }
835
836    /**
837     * When stack from bottom is set to true, the list fills its content starting from
838     * the bottom of the view.
839     *
840     * @param stackFromBottom true to pin the view's content to the bottom edge,
841     *        false to pin the view's content to the top edge
842     */
843    public void setStackFromBottom(boolean stackFromBottom) {
844        if (mStackFromBottom != stackFromBottom) {
845            mStackFromBottom = stackFromBottom;
846            requestLayoutIfNecessary();
847        }
848    }
849
850    void requestLayoutIfNecessary() {
851        if (getChildCount() > 0) {
852            resetList();
853            requestLayout();
854            invalidate();
855        }
856    }
857
858    static class SavedState extends BaseSavedState {
859        long selectedId;
860        long firstId;
861        int viewTop;
862        int position;
863        int height;
864        String filter;
865
866        /**
867         * Constructor called from {@link AbsListView#onSaveInstanceState()}
868         */
869        SavedState(Parcelable superState) {
870            super(superState);
871        }
872
873        /**
874         * Constructor called from {@link #CREATOR}
875         */
876        private SavedState(Parcel in) {
877            super(in);
878            selectedId = in.readLong();
879            firstId = in.readLong();
880            viewTop = in.readInt();
881            position = in.readInt();
882            height = in.readInt();
883            filter = in.readString();
884        }
885
886        @Override
887        public void writeToParcel(Parcel out, int flags) {
888            super.writeToParcel(out, flags);
889            out.writeLong(selectedId);
890            out.writeLong(firstId);
891            out.writeInt(viewTop);
892            out.writeInt(position);
893            out.writeInt(height);
894            out.writeString(filter);
895        }
896
897        @Override
898        public String toString() {
899            return "AbsListView.SavedState{"
900                    + Integer.toHexString(System.identityHashCode(this))
901                    + " selectedId=" + selectedId
902                    + " firstId=" + firstId
903                    + " viewTop=" + viewTop
904                    + " position=" + position
905                    + " height=" + height
906                    + " filter=" + filter + "}";
907        }
908
909        public static final Parcelable.Creator<SavedState> CREATOR
910                = new Parcelable.Creator<SavedState>() {
911            public SavedState createFromParcel(Parcel in) {
912                return new SavedState(in);
913            }
914
915            public SavedState[] newArray(int size) {
916                return new SavedState[size];
917            }
918        };
919    }
920
921    @Override
922    public Parcelable onSaveInstanceState() {
923        /*
924         * This doesn't really make sense as the place to dismiss the
925         * popups, but there don't seem to be any other useful hooks
926         * that happen early enough to keep from getting complaints
927         * about having leaked the window.
928         */
929        dismissPopup();
930
931        Parcelable superState = super.onSaveInstanceState();
932
933        SavedState ss = new SavedState(superState);
934
935        boolean haveChildren = getChildCount() > 0;
936        long selectedId = getSelectedItemId();
937        ss.selectedId = selectedId;
938        ss.height = getHeight();
939
940        if (selectedId >= 0) {
941            // Remember the selection
942            ss.viewTop = mSelectedTop;
943            ss.position = getSelectedItemPosition();
944            ss.firstId = INVALID_POSITION;
945        } else {
946            if (haveChildren) {
947                // Remember the position of the first child
948                View v = getChildAt(0);
949                ss.viewTop = v.getTop();
950                ss.position = mFirstPosition;
951                ss.firstId = mAdapter.getItemId(mFirstPosition);
952            } else {
953                ss.viewTop = 0;
954                ss.firstId = INVALID_POSITION;
955                ss.position = 0;
956            }
957        }
958
959        ss.filter = null;
960        if (mFiltered) {
961            final EditText textFilter = mTextFilter;
962            if (textFilter != null) {
963                Editable filterText = textFilter.getText();
964                if (filterText != null) {
965                    ss.filter = filterText.toString();
966                }
967            }
968        }
969
970        return ss;
971    }
972
973    @Override
974    public void onRestoreInstanceState(Parcelable state) {
975        SavedState ss = (SavedState) state;
976
977        super.onRestoreInstanceState(ss.getSuperState());
978        mDataChanged = true;
979
980        mSyncHeight = ss.height;
981
982        if (ss.selectedId >= 0) {
983            mNeedSync = true;
984            mSyncRowId = ss.selectedId;
985            mSyncPosition = ss.position;
986            mSpecificTop = ss.viewTop;
987            mSyncMode = SYNC_SELECTED_POSITION;
988        } else if (ss.firstId >= 0) {
989            setSelectedPositionInt(INVALID_POSITION);
990            // Do this before setting mNeedSync since setNextSelectedPosition looks at mNeedSync
991            setNextSelectedPositionInt(INVALID_POSITION);
992            mNeedSync = true;
993            mSyncRowId = ss.firstId;
994            mSyncPosition = ss.position;
995            mSpecificTop = ss.viewTop;
996            mSyncMode = SYNC_FIRST_POSITION;
997        }
998
999        setFilterText(ss.filter);
1000
1001        requestLayout();
1002    }
1003
1004    private boolean acceptFilter() {
1005        return mTextFilterEnabled && getAdapter() instanceof Filterable &&
1006                ((Filterable) getAdapter()).getFilter() != null;
1007    }
1008
1009    /**
1010     * Sets the initial value for the text filter.
1011     * @param filterText The text to use for the filter.
1012     *
1013     * @see #setTextFilterEnabled
1014     */
1015    public void setFilterText(String filterText) {
1016        // TODO: Should we check for acceptFilter()?
1017        if (mTextFilterEnabled && !TextUtils.isEmpty(filterText)) {
1018            createTextFilter(false);
1019            // This is going to call our listener onTextChanged, but we might not
1020            // be ready to bring up a window yet
1021            mTextFilter.setText(filterText);
1022            mTextFilter.setSelection(filterText.length());
1023            if (mAdapter instanceof Filterable) {
1024                // if mPopup is non-null, then onTextChanged will do the filtering
1025                if (mPopup == null) {
1026                    Filter f = ((Filterable) mAdapter).getFilter();
1027                    f.filter(filterText);
1028                }
1029                // Set filtered to true so we will display the filter window when our main
1030                // window is ready
1031                mFiltered = true;
1032                mDataSetObserver.clearSavedState();
1033            }
1034        }
1035    }
1036
1037    /**
1038     * Returns the list's text filter, if available.
1039     * @return the list's text filter or null if filtering isn't enabled
1040     */
1041    public CharSequence getTextFilter() {
1042        if (mTextFilterEnabled && mTextFilter != null) {
1043            return mTextFilter.getText();
1044        }
1045        return null;
1046    }
1047
1048    @Override
1049    protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
1050        super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
1051        if (gainFocus && mSelectedPosition < 0 && !isInTouchMode()) {
1052            resurrectSelection();
1053        }
1054    }
1055
1056    @Override
1057    public void requestLayout() {
1058        if (!mBlockLayoutRequests && !mInLayout) {
1059            super.requestLayout();
1060        }
1061    }
1062
1063    /**
1064     * The list is empty. Clear everything out.
1065     */
1066    void resetList() {
1067        removeAllViewsInLayout();
1068        mFirstPosition = 0;
1069        mDataChanged = false;
1070        mNeedSync = false;
1071        mOldSelectedPosition = INVALID_POSITION;
1072        mOldSelectedRowId = INVALID_ROW_ID;
1073        setSelectedPositionInt(INVALID_POSITION);
1074        setNextSelectedPositionInt(INVALID_POSITION);
1075        mSelectedTop = 0;
1076        mSelectorRect.setEmpty();
1077        invalidate();
1078    }
1079
1080    @Override
1081    protected int computeVerticalScrollExtent() {
1082        final int count = getChildCount();
1083        if (count > 0) {
1084            if (mSmoothScrollbarEnabled) {
1085                int extent = count * 100;
1086
1087                View view = getChildAt(0);
1088                final int top = view.getTop();
1089                int height = view.getHeight();
1090                if (height > 0) {
1091                    extent += (top * 100) / height;
1092                }
1093
1094                view = getChildAt(count - 1);
1095                final int bottom = view.getBottom();
1096                height = view.getHeight();
1097                if (height > 0) {
1098                    extent -= ((bottom - getHeight()) * 100) / height;
1099                }
1100
1101                return extent;
1102            } else {
1103                return 1;
1104            }
1105        }
1106        return 0;
1107    }
1108
1109    @Override
1110    protected int computeVerticalScrollOffset() {
1111        final int firstPosition = mFirstPosition;
1112        final int childCount = getChildCount();
1113        if (firstPosition >= 0 && childCount > 0) {
1114            if (mSmoothScrollbarEnabled) {
1115                final View view = getChildAt(0);
1116                final int top = view.getTop();
1117                int height = view.getHeight();
1118                if (height > 0) {
1119                    return Math.max(firstPosition * 100 - (top * 100) / height +
1120                            (int)((float)mScrollY / getHeight() * mItemCount * 100), 0);
1121                }
1122            } else {
1123                int index;
1124                final int count = mItemCount;
1125                if (firstPosition == 0) {
1126                    index = 0;
1127                } else if (firstPosition + childCount == count) {
1128                    index = count;
1129                } else {
1130                    index = firstPosition + childCount / 2;
1131                }
1132                return (int) (firstPosition + childCount * (index / (float) count));
1133            }
1134        }
1135        return 0;
1136    }
1137
1138    @Override
1139    protected int computeVerticalScrollRange() {
1140        int result;
1141        if (mSmoothScrollbarEnabled) {
1142            result = Math.max(mItemCount * 100, 0);
1143            if (mScrollY != 0) {
1144                // Compensate for overscroll
1145                result += Math.abs((int) ((float) mScrollY / getHeight() * mItemCount * 100));
1146            }
1147        } else {
1148            result = mItemCount;
1149        }
1150        return result;
1151    }
1152
1153    @Override
1154    protected float getTopFadingEdgeStrength() {
1155        final int count = getChildCount();
1156        final float fadeEdge = super.getTopFadingEdgeStrength();
1157        if (count == 0) {
1158            return fadeEdge;
1159        } else {
1160            if (mFirstPosition > 0) {
1161                return 1.0f;
1162            }
1163
1164            final int top = getChildAt(0).getTop();
1165            final float fadeLength = (float) getVerticalFadingEdgeLength();
1166            return top < mPaddingTop ? (float) -(top - mPaddingTop) / fadeLength : fadeEdge;
1167        }
1168    }
1169
1170    @Override
1171    protected float getBottomFadingEdgeStrength() {
1172        final int count = getChildCount();
1173        final float fadeEdge = super.getBottomFadingEdgeStrength();
1174        if (count == 0) {
1175            return fadeEdge;
1176        } else {
1177            if (mFirstPosition + count - 1 < mItemCount - 1) {
1178                return 1.0f;
1179            }
1180
1181            final int bottom = getChildAt(count - 1).getBottom();
1182            final int height = getHeight();
1183            final float fadeLength = (float) getVerticalFadingEdgeLength();
1184            return bottom > height - mPaddingBottom ?
1185                    (float) (bottom - height + mPaddingBottom) / fadeLength : fadeEdge;
1186        }
1187    }
1188
1189    @Override
1190    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1191        if (mSelector == null) {
1192            useDefaultSelector();
1193        }
1194        final Rect listPadding = mListPadding;
1195        listPadding.left = mSelectionLeftPadding + mPaddingLeft;
1196        listPadding.top = mSelectionTopPadding + mPaddingTop;
1197        listPadding.right = mSelectionRightPadding + mPaddingRight;
1198        listPadding.bottom = mSelectionBottomPadding + mPaddingBottom;
1199    }
1200
1201    /**
1202     * Subclasses should NOT override this method but
1203     *  {@link #layoutChildren()} instead.
1204     */
1205    @Override
1206    protected void onLayout(boolean changed, int l, int t, int r, int b) {
1207        super.onLayout(changed, l, t, r, b);
1208        mInLayout = true;
1209        if (changed) {
1210            int childCount = getChildCount();
1211            for (int i = 0; i < childCount; i++) {
1212                getChildAt(i).forceLayout();
1213            }
1214            mRecycler.markChildrenDirty();
1215        }
1216
1217        layoutChildren();
1218        mInLayout = false;
1219
1220        mOverscrollMax = (b - t) / OVERSCROLL_LIMIT_DIVISOR;
1221    }
1222
1223    /**
1224     * @hide
1225     */
1226    @Override
1227    protected boolean setFrame(int left, int top, int right, int bottom) {
1228        final boolean changed = super.setFrame(left, top, right, bottom);
1229
1230        if (changed) {
1231            // Reposition the popup when the frame has changed. This includes
1232            // translating the widget, not just changing its dimension. The
1233            // filter popup needs to follow the widget.
1234            final boolean visible = getWindowVisibility() == View.VISIBLE;
1235            if (mFiltered && visible && mPopup != null && mPopup.isShowing()) {
1236                positionPopup();
1237            }
1238        }
1239
1240        return changed;
1241    }
1242
1243    /**
1244     * Subclasses must override this method to layout their children.
1245     */
1246    protected void layoutChildren() {
1247    }
1248
1249    void updateScrollIndicators() {
1250        if (mScrollUp != null) {
1251            boolean canScrollUp;
1252            // 0th element is not visible
1253            canScrollUp = mFirstPosition > 0;
1254
1255            // ... Or top of 0th element is not visible
1256            if (!canScrollUp) {
1257                if (getChildCount() > 0) {
1258                    View child = getChildAt(0);
1259                    canScrollUp = child.getTop() < mListPadding.top;
1260                }
1261            }
1262
1263            mScrollUp.setVisibility(canScrollUp ? View.VISIBLE : View.INVISIBLE);
1264        }
1265
1266        if (mScrollDown != null) {
1267            boolean canScrollDown;
1268            int count = getChildCount();
1269
1270            // Last item is not visible
1271            canScrollDown = (mFirstPosition + count) < mItemCount;
1272
1273            // ... Or bottom of the last element is not visible
1274            if (!canScrollDown && count > 0) {
1275                View child = getChildAt(count - 1);
1276                canScrollDown = child.getBottom() > mBottom - mListPadding.bottom;
1277            }
1278
1279            mScrollDown.setVisibility(canScrollDown ? View.VISIBLE : View.INVISIBLE);
1280        }
1281    }
1282
1283    @Override
1284    @ViewDebug.ExportedProperty
1285    public View getSelectedView() {
1286        if (mItemCount > 0 && mSelectedPosition >= 0) {
1287            return getChildAt(mSelectedPosition - mFirstPosition);
1288        } else {
1289            return null;
1290        }
1291    }
1292
1293    /**
1294     * List padding is the maximum of the normal view's padding and the padding of the selector.
1295     *
1296     * @see android.view.View#getPaddingTop()
1297     * @see #getSelector()
1298     *
1299     * @return The top list padding.
1300     */
1301    public int getListPaddingTop() {
1302        return mListPadding.top;
1303    }
1304
1305    /**
1306     * List padding is the maximum of the normal view's padding and the padding of the selector.
1307     *
1308     * @see android.view.View#getPaddingBottom()
1309     * @see #getSelector()
1310     *
1311     * @return The bottom list padding.
1312     */
1313    public int getListPaddingBottom() {
1314        return mListPadding.bottom;
1315    }
1316
1317    /**
1318     * List padding is the maximum of the normal view's padding and the padding of the selector.
1319     *
1320     * @see android.view.View#getPaddingLeft()
1321     * @see #getSelector()
1322     *
1323     * @return The left list padding.
1324     */
1325    public int getListPaddingLeft() {
1326        return mListPadding.left;
1327    }
1328
1329    /**
1330     * List padding is the maximum of the normal view's padding and the padding of the selector.
1331     *
1332     * @see android.view.View#getPaddingRight()
1333     * @see #getSelector()
1334     *
1335     * @return The right list padding.
1336     */
1337    public int getListPaddingRight() {
1338        return mListPadding.right;
1339    }
1340
1341    /**
1342     * Get a view and have it show the data associated with the specified
1343     * position. This is called when we have already discovered that the view is
1344     * not available for reuse in the recycle bin. The only choices left are
1345     * converting an old view or making a new one.
1346     *
1347     * @param position The position to display
1348     * @param isScrap Array of at least 1 boolean, the first entry will become true if
1349     *                the returned view was taken from the scrap heap, false if otherwise.
1350     *
1351     * @return A view displaying the data associated with the specified position
1352     */
1353    View obtainView(int position, boolean[] isScrap) {
1354        isScrap[0] = false;
1355        View scrapView;
1356
1357        scrapView = mRecycler.getScrapView(position);
1358
1359        View child;
1360        if (scrapView != null) {
1361            if (ViewDebug.TRACE_RECYCLER) {
1362                ViewDebug.trace(scrapView, ViewDebug.RecyclerTraceType.RECYCLE_FROM_SCRAP_HEAP,
1363                        position, -1);
1364            }
1365
1366            child = mAdapter.getView(position, scrapView, this);
1367
1368            if (ViewDebug.TRACE_RECYCLER) {
1369                ViewDebug.trace(child, ViewDebug.RecyclerTraceType.BIND_VIEW,
1370                        position, getChildCount());
1371            }
1372
1373            if (child != scrapView) {
1374                mRecycler.addScrapView(scrapView);
1375                if (mCacheColorHint != 0) {
1376                    child.setDrawingCacheBackgroundColor(mCacheColorHint);
1377                }
1378                if (ViewDebug.TRACE_RECYCLER) {
1379                    ViewDebug.trace(scrapView, ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP,
1380                            position, -1);
1381                }
1382            } else {
1383                isScrap[0] = true;
1384                child.dispatchFinishTemporaryDetach();
1385            }
1386        } else {
1387            child = mAdapter.getView(position, null, this);
1388            if (mCacheColorHint != 0) {
1389                child.setDrawingCacheBackgroundColor(mCacheColorHint);
1390            }
1391            if (ViewDebug.TRACE_RECYCLER) {
1392                ViewDebug.trace(child, ViewDebug.RecyclerTraceType.NEW_VIEW,
1393                        position, getChildCount());
1394            }
1395        }
1396
1397        return child;
1398    }
1399
1400    void positionSelector(View sel) {
1401        final Rect selectorRect = mSelectorRect;
1402        selectorRect.set(sel.getLeft(), sel.getTop(), sel.getRight(), sel.getBottom());
1403        positionSelector(selectorRect.left, selectorRect.top, selectorRect.right,
1404                selectorRect.bottom);
1405
1406        final boolean isChildViewEnabled = mIsChildViewEnabled;
1407        if (sel.isEnabled() != isChildViewEnabled) {
1408            mIsChildViewEnabled = !isChildViewEnabled;
1409            refreshDrawableState();
1410        }
1411    }
1412
1413    private void positionSelector(int l, int t, int r, int b) {
1414        mSelectorRect.set(l - mSelectionLeftPadding, t - mSelectionTopPadding, r
1415                + mSelectionRightPadding, b + mSelectionBottomPadding);
1416    }
1417
1418    @Override
1419    protected void dispatchDraw(Canvas canvas) {
1420        int saveCount = 0;
1421        final boolean clipToPadding = (mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK;
1422        if (clipToPadding) {
1423            saveCount = canvas.save();
1424            final int scrollX = mScrollX;
1425            final int scrollY = mScrollY;
1426            canvas.clipRect(scrollX + mPaddingLeft, scrollY + mPaddingTop,
1427                    scrollX + mRight - mLeft - mPaddingRight,
1428                    scrollY + mBottom - mTop - mPaddingBottom);
1429            mGroupFlags &= ~CLIP_TO_PADDING_MASK;
1430        }
1431
1432        final boolean drawSelectorOnTop = mDrawSelectorOnTop;
1433        if (!drawSelectorOnTop) {
1434            drawSelector(canvas);
1435        }
1436
1437        super.dispatchDraw(canvas);
1438
1439        if (drawSelectorOnTop) {
1440            drawSelector(canvas);
1441        }
1442
1443        if (clipToPadding) {
1444            canvas.restoreToCount(saveCount);
1445            mGroupFlags |= CLIP_TO_PADDING_MASK;
1446        }
1447    }
1448
1449    @Override
1450    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
1451        if (getChildCount() > 0) {
1452            mDataChanged = true;
1453            rememberSyncState();
1454        }
1455
1456        if (mFastScroller != null) {
1457            mFastScroller.onSizeChanged(w, h, oldw, oldh);
1458        }
1459    }
1460
1461    /**
1462     * @return True if the current touch mode requires that we draw the selector in the pressed
1463     *         state.
1464     */
1465    boolean touchModeDrawsInPressedState() {
1466        // FIXME use isPressed for this
1467        switch (mTouchMode) {
1468        case TOUCH_MODE_TAP:
1469        case TOUCH_MODE_DONE_WAITING:
1470            return true;
1471        default:
1472            return false;
1473        }
1474    }
1475
1476    /**
1477     * Indicates whether this view is in a state where the selector should be drawn. This will
1478     * happen if we have focus but are not in touch mode, or we are in the middle of displaying
1479     * the pressed state for an item.
1480     *
1481     * @return True if the selector should be shown
1482     */
1483    boolean shouldShowSelector() {
1484        return (hasFocus() && !isInTouchMode()) || touchModeDrawsInPressedState();
1485    }
1486
1487    private void drawSelector(Canvas canvas) {
1488        if (shouldShowSelector() && mSelectorRect != null && !mSelectorRect.isEmpty()) {
1489            final Drawable selector = mSelector;
1490            selector.setBounds(mSelectorRect);
1491            selector.draw(canvas);
1492        }
1493    }
1494
1495    /**
1496     * Controls whether the selection highlight drawable should be drawn on top of the item or
1497     * behind it.
1498     *
1499     * @param onTop If true, the selector will be drawn on the item it is highlighting. The default
1500     *        is false.
1501     *
1502     * @attr ref android.R.styleable#AbsListView_drawSelectorOnTop
1503     */
1504    public void setDrawSelectorOnTop(boolean onTop) {
1505        mDrawSelectorOnTop = onTop;
1506    }
1507
1508    /**
1509     * Set a Drawable that should be used to highlight the currently selected item.
1510     *
1511     * @param resID A Drawable resource to use as the selection highlight.
1512     *
1513     * @attr ref android.R.styleable#AbsListView_listSelector
1514     */
1515    public void setSelector(int resID) {
1516        setSelector(getResources().getDrawable(resID));
1517    }
1518
1519    public void setSelector(Drawable sel) {
1520        if (mSelector != null) {
1521            mSelector.setCallback(null);
1522            unscheduleDrawable(mSelector);
1523        }
1524        mSelector = sel;
1525        Rect padding = new Rect();
1526        sel.getPadding(padding);
1527        mSelectionLeftPadding = padding.left;
1528        mSelectionTopPadding = padding.top;
1529        mSelectionRightPadding = padding.right;
1530        mSelectionBottomPadding = padding.bottom;
1531        sel.setCallback(this);
1532        sel.setState(getDrawableState());
1533    }
1534
1535    /**
1536     * Returns the selector {@link android.graphics.drawable.Drawable} that is used to draw the
1537     * selection in the list.
1538     *
1539     * @return the drawable used to display the selector
1540     */
1541    public Drawable getSelector() {
1542        return mSelector;
1543    }
1544
1545    /**
1546     * Sets the selector state to "pressed" and posts a CheckForKeyLongPress to see if
1547     * this is a long press.
1548     */
1549    void keyPressed() {
1550        if (!isEnabled() || !isClickable()) {
1551            return;
1552        }
1553
1554        Drawable selector = mSelector;
1555        Rect selectorRect = mSelectorRect;
1556        if (selector != null && (isFocused() || touchModeDrawsInPressedState())
1557                && selectorRect != null && !selectorRect.isEmpty()) {
1558
1559            final View v = getChildAt(mSelectedPosition - mFirstPosition);
1560
1561            if (v != null) {
1562                if (v.hasFocusable()) return;
1563                v.setPressed(true);
1564            }
1565            setPressed(true);
1566
1567            final boolean longClickable = isLongClickable();
1568            Drawable d = selector.getCurrent();
1569            if (d != null && d instanceof TransitionDrawable) {
1570                if (longClickable) {
1571                    ((TransitionDrawable) d).startTransition(
1572                            ViewConfiguration.getLongPressTimeout());
1573                } else {
1574                    ((TransitionDrawable) d).resetTransition();
1575                }
1576            }
1577            if (longClickable && !mDataChanged) {
1578                if (mPendingCheckForKeyLongPress == null) {
1579                    mPendingCheckForKeyLongPress = new CheckForKeyLongPress();
1580                }
1581                mPendingCheckForKeyLongPress.rememberWindowAttachCount();
1582                postDelayed(mPendingCheckForKeyLongPress, ViewConfiguration.getLongPressTimeout());
1583            }
1584        }
1585    }
1586
1587    public void setScrollIndicators(View up, View down) {
1588        mScrollUp = up;
1589        mScrollDown = down;
1590    }
1591
1592    @Override
1593    protected void drawableStateChanged() {
1594        super.drawableStateChanged();
1595        if (mSelector != null) {
1596            mSelector.setState(getDrawableState());
1597        }
1598    }
1599
1600    @Override
1601    protected int[] onCreateDrawableState(int extraSpace) {
1602        // If the child view is enabled then do the default behavior.
1603        if (mIsChildViewEnabled) {
1604            // Common case
1605            return super.onCreateDrawableState(extraSpace);
1606        }
1607
1608        // The selector uses this View's drawable state. The selected child view
1609        // is disabled, so we need to remove the enabled state from the drawable
1610        // states.
1611        final int enabledState = ENABLED_STATE_SET[0];
1612
1613        // If we don't have any extra space, it will return one of the static state arrays,
1614        // and clearing the enabled state on those arrays is a bad thing!  If we specify
1615        // we need extra space, it will create+copy into a new array that safely mutable.
1616        int[] state = super.onCreateDrawableState(extraSpace + 1);
1617        int enabledPos = -1;
1618        for (int i = state.length - 1; i >= 0; i--) {
1619            if (state[i] == enabledState) {
1620                enabledPos = i;
1621                break;
1622            }
1623        }
1624
1625        // Remove the enabled state
1626        if (enabledPos >= 0) {
1627            System.arraycopy(state, enabledPos + 1, state, enabledPos,
1628                    state.length - enabledPos - 1);
1629        }
1630
1631        return state;
1632    }
1633
1634    @Override
1635    public boolean verifyDrawable(Drawable dr) {
1636        return mSelector == dr || super.verifyDrawable(dr);
1637    }
1638
1639    @Override
1640    protected void onAttachedToWindow() {
1641        super.onAttachedToWindow();
1642
1643        final ViewTreeObserver treeObserver = getViewTreeObserver();
1644        if (treeObserver != null) {
1645            treeObserver.addOnTouchModeChangeListener(this);
1646            if (mTextFilterEnabled && mPopup != null && !mGlobalLayoutListenerAddedFilter) {
1647                treeObserver.addOnGlobalLayoutListener(this);
1648            }
1649        }
1650    }
1651
1652    @Override
1653    protected void onDetachedFromWindow() {
1654        super.onDetachedFromWindow();
1655
1656        // Dismiss the popup in case onSaveInstanceState() was not invoked
1657        dismissPopup();
1658
1659        // Detach any view left in the scrap heap
1660        mRecycler.clear();
1661
1662        final ViewTreeObserver treeObserver = getViewTreeObserver();
1663        if (treeObserver != null) {
1664            treeObserver.removeOnTouchModeChangeListener(this);
1665            if (mTextFilterEnabled && mPopup != null) {
1666                treeObserver.removeGlobalOnLayoutListener(this);
1667                mGlobalLayoutListenerAddedFilter = false;
1668            }
1669        }
1670    }
1671
1672    @Override
1673    public void onWindowFocusChanged(boolean hasWindowFocus) {
1674        super.onWindowFocusChanged(hasWindowFocus);
1675
1676        final int touchMode = isInTouchMode() ? TOUCH_MODE_ON : TOUCH_MODE_OFF;
1677
1678        if (!hasWindowFocus) {
1679            setChildrenDrawingCacheEnabled(false);
1680            if (mFlingRunnable != null) {
1681                removeCallbacks(mFlingRunnable);
1682                // let the fling runnable report it's new state which
1683                // should be idle
1684                mFlingRunnable.endFling();
1685                if (mScrollY != 0) {
1686                    mScrollY = 0;
1687                    invalidate();
1688                }
1689            }
1690            // Always hide the type filter
1691            dismissPopup();
1692
1693            if (touchMode == TOUCH_MODE_OFF) {
1694                // Remember the last selected element
1695                mResurrectToPosition = mSelectedPosition;
1696            }
1697        } else {
1698            if (mFiltered && !mPopupHidden) {
1699                // Show the type filter only if a filter is in effect
1700                showPopup();
1701            }
1702
1703            // If we changed touch mode since the last time we had focus
1704            if (touchMode != mLastTouchMode && mLastTouchMode != TOUCH_MODE_UNKNOWN) {
1705                // If we come back in trackball mode, we bring the selection back
1706                if (touchMode == TOUCH_MODE_OFF) {
1707                    // This will trigger a layout
1708                    resurrectSelection();
1709
1710                // If we come back in touch mode, then we want to hide the selector
1711                } else {
1712                    hideSelector();
1713                    mLayoutMode = LAYOUT_NORMAL;
1714                    layoutChildren();
1715                }
1716            }
1717        }
1718
1719        mLastTouchMode = touchMode;
1720    }
1721
1722    /**
1723     * Creates the ContextMenuInfo returned from {@link #getContextMenuInfo()}. This
1724     * methods knows the view, position and ID of the item that received the
1725     * long press.
1726     *
1727     * @param view The view that received the long press.
1728     * @param position The position of the item that received the long press.
1729     * @param id The ID of the item that received the long press.
1730     * @return The extra information that should be returned by
1731     *         {@link #getContextMenuInfo()}.
1732     */
1733    ContextMenuInfo createContextMenuInfo(View view, int position, long id) {
1734        return new AdapterContextMenuInfo(view, position, id);
1735    }
1736
1737    /**
1738     * A base class for Runnables that will check that their view is still attached to
1739     * the original window as when the Runnable was created.
1740     *
1741     */
1742    private class WindowRunnnable {
1743        private int mOriginalAttachCount;
1744
1745        public void rememberWindowAttachCount() {
1746            mOriginalAttachCount = getWindowAttachCount();
1747        }
1748
1749        public boolean sameWindow() {
1750            return hasWindowFocus() && getWindowAttachCount() == mOriginalAttachCount;
1751        }
1752    }
1753
1754    private class PerformClick extends WindowRunnnable implements Runnable {
1755        View mChild;
1756        int mClickMotionPosition;
1757
1758        public void run() {
1759            // The data has changed since we posted this action in the event queue,
1760            // bail out before bad things happen
1761            if (mDataChanged) return;
1762
1763            final ListAdapter adapter = mAdapter;
1764            final int motionPosition = mClickMotionPosition;
1765            if (adapter != null && mItemCount > 0 &&
1766                    motionPosition != INVALID_POSITION &&
1767                    motionPosition < adapter.getCount() && sameWindow()) {
1768                performItemClick(mChild, motionPosition, adapter.getItemId(motionPosition));
1769            }
1770        }
1771    }
1772
1773    private class CheckForLongPress extends WindowRunnnable implements Runnable {
1774        public void run() {
1775            final int motionPosition = mMotionPosition;
1776            final View child = getChildAt(motionPosition - mFirstPosition);
1777            if (child != null) {
1778                final int longPressPosition = mMotionPosition;
1779                final long longPressId = mAdapter.getItemId(mMotionPosition);
1780
1781                boolean handled = false;
1782                if (sameWindow() && !mDataChanged) {
1783                    handled = performLongPress(child, longPressPosition, longPressId);
1784                }
1785                if (handled) {
1786                    mTouchMode = TOUCH_MODE_REST;
1787                    setPressed(false);
1788                    child.setPressed(false);
1789                } else {
1790                    mTouchMode = TOUCH_MODE_DONE_WAITING;
1791                }
1792
1793            }
1794        }
1795    }
1796
1797    private class CheckForKeyLongPress extends WindowRunnnable implements Runnable {
1798        public void run() {
1799            if (isPressed() && mSelectedPosition >= 0) {
1800                int index = mSelectedPosition - mFirstPosition;
1801                View v = getChildAt(index);
1802
1803                if (!mDataChanged) {
1804                    boolean handled = false;
1805                    if (sameWindow()) {
1806                        handled = performLongPress(v, mSelectedPosition, mSelectedRowId);
1807                    }
1808                    if (handled) {
1809                        setPressed(false);
1810                        v.setPressed(false);
1811                    }
1812                } else {
1813                    setPressed(false);
1814                    if (v != null) v.setPressed(false);
1815                }
1816            }
1817        }
1818    }
1819
1820    private boolean performLongPress(final View child,
1821            final int longPressPosition, final long longPressId) {
1822        boolean handled = false;
1823
1824        if (mOnItemLongClickListener != null) {
1825            handled = mOnItemLongClickListener.onItemLongClick(AbsListView.this, child,
1826                    longPressPosition, longPressId);
1827        }
1828        if (!handled) {
1829            mContextMenuInfo = createContextMenuInfo(child, longPressPosition, longPressId);
1830            handled = super.showContextMenuForChild(AbsListView.this);
1831        }
1832        if (handled) {
1833            performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
1834        }
1835        return handled;
1836    }
1837
1838    @Override
1839    protected ContextMenuInfo getContextMenuInfo() {
1840        return mContextMenuInfo;
1841    }
1842
1843    @Override
1844    public boolean showContextMenuForChild(View originalView) {
1845        final int longPressPosition = getPositionForView(originalView);
1846        if (longPressPosition >= 0) {
1847            final long longPressId = mAdapter.getItemId(longPressPosition);
1848            boolean handled = false;
1849
1850            if (mOnItemLongClickListener != null) {
1851                handled = mOnItemLongClickListener.onItemLongClick(AbsListView.this, originalView,
1852                        longPressPosition, longPressId);
1853            }
1854            if (!handled) {
1855                mContextMenuInfo = createContextMenuInfo(
1856                        getChildAt(longPressPosition - mFirstPosition),
1857                        longPressPosition, longPressId);
1858                handled = super.showContextMenuForChild(originalView);
1859            }
1860
1861            return handled;
1862        }
1863        return false;
1864    }
1865
1866    @Override
1867    public boolean onKeyDown(int keyCode, KeyEvent event) {
1868        return false;
1869    }
1870
1871    @Override
1872    public boolean onKeyUp(int keyCode, KeyEvent event) {
1873        switch (keyCode) {
1874        case KeyEvent.KEYCODE_DPAD_CENTER:
1875        case KeyEvent.KEYCODE_ENTER:
1876            if (!isEnabled()) {
1877                return true;
1878            }
1879            if (isClickable() && isPressed() &&
1880                    mSelectedPosition >= 0 && mAdapter != null &&
1881                    mSelectedPosition < mAdapter.getCount()) {
1882
1883                final View view = getChildAt(mSelectedPosition - mFirstPosition);
1884                if (view != null) {
1885                    performItemClick(view, mSelectedPosition, mSelectedRowId);
1886                    view.setPressed(false);
1887                }
1888                setPressed(false);
1889                return true;
1890            }
1891            break;
1892        }
1893        return super.onKeyUp(keyCode, event);
1894    }
1895
1896    @Override
1897    protected void dispatchSetPressed(boolean pressed) {
1898        // Don't dispatch setPressed to our children. We call setPressed on ourselves to
1899        // get the selector in the right state, but we don't want to press each child.
1900    }
1901
1902    /**
1903     * Maps a point to a position in the list.
1904     *
1905     * @param x X in local coordinate
1906     * @param y Y in local coordinate
1907     * @return The position of the item which contains the specified point, or
1908     *         {@link #INVALID_POSITION} if the point does not intersect an item.
1909     */
1910    public int pointToPosition(int x, int y) {
1911        Rect frame = mTouchFrame;
1912        if (frame == null) {
1913            mTouchFrame = new Rect();
1914            frame = mTouchFrame;
1915        }
1916
1917        final int count = getChildCount();
1918        for (int i = count - 1; i >= 0; i--) {
1919            final View child = getChildAt(i);
1920            if (child.getVisibility() == View.VISIBLE) {
1921                child.getHitRect(frame);
1922                if (frame.contains(x, y)) {
1923                    return mFirstPosition + i;
1924                }
1925            }
1926        }
1927        return INVALID_POSITION;
1928    }
1929
1930
1931    /**
1932     * Maps a point to a the rowId of the item which intersects that point.
1933     *
1934     * @param x X in local coordinate
1935     * @param y Y in local coordinate
1936     * @return The rowId of the item which contains the specified point, or {@link #INVALID_ROW_ID}
1937     *         if the point does not intersect an item.
1938     */
1939    public long pointToRowId(int x, int y) {
1940        int position = pointToPosition(x, y);
1941        if (position >= 0) {
1942            return mAdapter.getItemId(position);
1943        }
1944        return INVALID_ROW_ID;
1945    }
1946
1947    final class CheckForTap implements Runnable {
1948        public void run() {
1949            if (mTouchMode == TOUCH_MODE_DOWN) {
1950                mTouchMode = TOUCH_MODE_TAP;
1951                final View child = getChildAt(mMotionPosition - mFirstPosition);
1952                if (child != null && !child.hasFocusable()) {
1953                    mLayoutMode = LAYOUT_NORMAL;
1954
1955                    if (!mDataChanged) {
1956                        layoutChildren();
1957                        child.setPressed(true);
1958                        positionSelector(child);
1959                        setPressed(true);
1960
1961                        final int longPressTimeout = ViewConfiguration.getLongPressTimeout();
1962                        final boolean longClickable = isLongClickable();
1963
1964                        if (mSelector != null) {
1965                            Drawable d = mSelector.getCurrent();
1966                            if (d != null && d instanceof TransitionDrawable) {
1967                                if (longClickable) {
1968                                    ((TransitionDrawable) d).startTransition(longPressTimeout);
1969                                } else {
1970                                    ((TransitionDrawable) d).resetTransition();
1971                                }
1972                            }
1973                        }
1974
1975                        if (longClickable) {
1976                            if (mPendingCheckForLongPress == null) {
1977                                mPendingCheckForLongPress = new CheckForLongPress();
1978                            }
1979                            mPendingCheckForLongPress.rememberWindowAttachCount();
1980                            postDelayed(mPendingCheckForLongPress, longPressTimeout);
1981                        } else {
1982                            mTouchMode = TOUCH_MODE_DONE_WAITING;
1983                        }
1984                    } else {
1985                        mTouchMode = TOUCH_MODE_DONE_WAITING;
1986                    }
1987                }
1988            }
1989        }
1990    }
1991
1992    private boolean startScrollIfNeeded(int deltaY) {
1993        // Check if we have moved far enough that it looks more like a
1994        // scroll than a tap
1995        final int distance = Math.abs(deltaY);
1996        final boolean overscroll = mScrollY != 0;
1997        if (overscroll || distance > mTouchSlop) {
1998            createScrollingCache();
1999            mTouchMode = overscroll ? TOUCH_MODE_OVERSCROLL : TOUCH_MODE_SCROLL;
2000            mMotionCorrection = deltaY;
2001            final Handler handler = getHandler();
2002            // Handler should not be null unless the AbsListView is not attached to a
2003            // window, which would make it very hard to scroll it... but the monkeys
2004            // say it's possible.
2005            if (handler != null) {
2006                handler.removeCallbacks(mPendingCheckForLongPress);
2007            }
2008            setPressed(false);
2009            View motionView = getChildAt(mMotionPosition - mFirstPosition);
2010            if (motionView != null) {
2011                motionView.setPressed(false);
2012            }
2013            reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
2014            // Time to start stealing events! Once we've stolen them, don't let anyone
2015            // steal from us
2016            requestDisallowInterceptTouchEvent(true);
2017            return true;
2018        }
2019
2020        return false;
2021    }
2022
2023    public void onTouchModeChanged(boolean isInTouchMode) {
2024        if (isInTouchMode) {
2025            // Get rid of the selection when we enter touch mode
2026            hideSelector();
2027            // Layout, but only if we already have done so previously.
2028            // (Otherwise may clobber a LAYOUT_SYNC layout that was requested to restore
2029            // state.)
2030            if (getHeight() > 0 && getChildCount() > 0) {
2031                // We do not lose focus initiating a touch (since AbsListView is focusable in
2032                // touch mode). Force an initial layout to get rid of the selection.
2033                layoutChildren();
2034            }
2035        } else {
2036            int touchMode = mTouchMode;
2037            if (touchMode == TOUCH_MODE_OVERSCROLL || touchMode == TOUCH_MODE_OVERFLING) {
2038                if (mFlingRunnable != null) {
2039                    mFlingRunnable.endFling();
2040                }
2041
2042                if (mScrollY != 0) {
2043                    mScrollY = 0;
2044                    invalidate();
2045                }
2046            }
2047        }
2048    }
2049
2050    @Override
2051    public boolean onTouchEvent(MotionEvent ev) {
2052        if (!isEnabled()) {
2053            // A disabled view that is clickable still consumes the touch
2054            // events, it just doesn't respond to them.
2055            return isClickable() || isLongClickable();
2056        }
2057
2058        if (mFastScroller != null) {
2059            boolean intercepted = mFastScroller.onTouchEvent(ev);
2060            if (intercepted) {
2061                return true;
2062            }
2063        }
2064
2065        final int action = ev.getAction();
2066
2067        View v;
2068        int deltaY;
2069
2070        if (mVelocityTracker == null) {
2071            mVelocityTracker = VelocityTracker.obtain();
2072        }
2073        mVelocityTracker.addMovement(ev);
2074
2075        switch (action & MotionEvent.ACTION_MASK) {
2076        case MotionEvent.ACTION_DOWN: {
2077            switch (mTouchMode) {
2078            case TOUCH_MODE_OVERFLING: {
2079                mFlingRunnable.endFling();
2080                mTouchMode = TOUCH_MODE_OVERSCROLL;
2081                mMotionY = mLastY = (int) ev.getY();
2082                mMotionCorrection = 0;
2083                mActivePointerId = ev.getPointerId(0);
2084                break;
2085            }
2086
2087            default: {
2088                mActivePointerId = ev.getPointerId(0);
2089                final int x = (int) ev.getX();
2090                final int y = (int) ev.getY();
2091                int motionPosition = pointToPosition(x, y);
2092                if (!mDataChanged) {
2093                    if ((mTouchMode != TOUCH_MODE_FLING) && (motionPosition >= 0)
2094                            && (getAdapter().isEnabled(motionPosition))) {
2095                        // User clicked on an actual view (and was not stopping a fling). It might be a
2096                        // click or a scroll. Assume it is a click until proven otherwise
2097                        mTouchMode = TOUCH_MODE_DOWN;
2098                        // FIXME Debounce
2099                        if (mPendingCheckForTap == null) {
2100                            mPendingCheckForTap = new CheckForTap();
2101                        }
2102                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
2103                    } else {
2104                        if (ev.getEdgeFlags() != 0 && motionPosition < 0) {
2105                            // If we couldn't find a view to click on, but the down event was touching
2106                            // the edge, we will bail out and try again. This allows the edge correcting
2107                            // code in ViewRoot to try to find a nearby view to select
2108                            return false;
2109                        }
2110
2111                        if (mTouchMode == TOUCH_MODE_FLING) {
2112                            // Stopped a fling. It is a scroll.
2113                            createScrollingCache();
2114                            mTouchMode = TOUCH_MODE_SCROLL;
2115                            mMotionCorrection = 0;
2116                            motionPosition = findMotionRow(y);
2117                            reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
2118                        }
2119                    }
2120                }
2121
2122                if (motionPosition >= 0) {
2123                    // Remember where the motion event started
2124                    v = getChildAt(motionPosition - mFirstPosition);
2125                    mMotionViewOriginalTop = v.getTop();
2126                }
2127                mMotionX = x;
2128                mMotionY = y;
2129                mMotionPosition = motionPosition;
2130                mLastY = Integer.MIN_VALUE;
2131                break;
2132            }
2133            }
2134            break;
2135        }
2136
2137        case MotionEvent.ACTION_MOVE: {
2138            final int pointerIndex = ev.findPointerIndex(mActivePointerId);
2139            final int y = (int) ev.getY(pointerIndex);
2140            deltaY = y - mMotionY;
2141            switch (mTouchMode) {
2142            case TOUCH_MODE_DOWN:
2143            case TOUCH_MODE_TAP:
2144            case TOUCH_MODE_DONE_WAITING:
2145                // Check if we have moved far enough that it looks more like a
2146                // scroll than a tap
2147                startScrollIfNeeded(deltaY);
2148                break;
2149            case TOUCH_MODE_SCROLL:
2150                if (PROFILE_SCROLLING) {
2151                    if (!mScrollProfilingStarted) {
2152                        Debug.startMethodTracing("AbsListViewScroll");
2153                        mScrollProfilingStarted = true;
2154                    }
2155                }
2156
2157                if (y != mLastY) {
2158                    // We may be here after stopping a fling and continuing to scroll.
2159                    // If so, we haven't disallowed intercepting touch events yet.
2160                    // Make sure that we do so in case we're in a parent that can intercept.
2161                    if ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) == 0 &&
2162                            Math.abs(deltaY) > mTouchSlop) {
2163                        requestDisallowInterceptTouchEvent(true);
2164                    }
2165
2166                    final int rawDeltaY = deltaY;
2167                    deltaY -= mMotionCorrection;
2168                    int incrementalDeltaY = mLastY != Integer.MIN_VALUE ? y - mLastY : deltaY;
2169
2170                    final int motionIndex;
2171                    if (mMotionPosition >= 0) {
2172                        motionIndex = mMotionPosition - mFirstPosition;
2173                    } else {
2174                        // If we don't have a motion position that we can reliably track,
2175                        // pick something in the middle to make a best guess at things below.
2176                        motionIndex = getChildCount() / 2;
2177                    }
2178
2179                    int motionViewPrevTop = 0;
2180                    View motionView = this.getChildAt(motionIndex);
2181                    if (motionView != null) {
2182                        motionViewPrevTop = motionView.getTop();
2183                    }
2184
2185                    // No need to do all this work if we're not going to move anyway
2186                    boolean atEdge = false;
2187                    if (incrementalDeltaY != 0) {
2188                        atEdge = trackMotionScroll(deltaY, incrementalDeltaY);
2189                    }
2190
2191                    // Check to see if we have bumped into the scroll limit
2192                    motionView = this.getChildAt(motionIndex);
2193                    if (motionView != null) {
2194                        // Check if the top of the motion view is where it is
2195                        // supposed to be
2196                        final int motionViewRealTop = motionView.getTop();
2197                        if (atEdge) {
2198                            // Apply overscroll
2199
2200                            int overscroll = -incrementalDeltaY -
2201                                    (motionViewRealTop - motionViewPrevTop);
2202                            overscrollBy(0, overscroll, 0, mScrollY, 0, 0,
2203                                    0, mOverscrollDistance, true);
2204                            if (Math.abs(mOverscrollDistance) == Math.abs(mScrollY)) {
2205                                // Don't allow overfling if we're at the edge.
2206                                mVelocityTracker.clear();
2207                            }
2208                            mTouchMode = TOUCH_MODE_OVERSCROLL;
2209                            if (mEdgeGlowTop != null) {
2210                                if (rawDeltaY > 0) {
2211                                    mEdgeGlowTop.onPull((float) overscroll / getHeight());
2212                                } else if (rawDeltaY < 0) {
2213                                    mEdgeGlowBottom.onPull((float) overscroll / getHeight());
2214                                }
2215                            }
2216                        }
2217                        mMotionY = y;
2218                        invalidate();
2219                    }
2220                    mLastY = y;
2221                }
2222                break;
2223
2224            case TOUCH_MODE_OVERSCROLL:
2225                if (y != mLastY) {
2226                    final int rawDeltaY = deltaY;
2227                    deltaY -= mMotionCorrection;
2228                    int incrementalDeltaY = mLastY != Integer.MIN_VALUE ? y - mLastY : deltaY;
2229
2230                    final int oldScroll = mScrollY;
2231                    final int newScroll = oldScroll - incrementalDeltaY;
2232
2233                    if ((oldScroll >= 0 && newScroll <= 0) ||
2234                            (oldScroll <= 0 && newScroll >= 0)) {
2235                        // Coming back to 'real' list scrolling
2236                        incrementalDeltaY = -newScroll;
2237                        mScrollY = 0;
2238
2239                        // No need to do all this work if we're not going to move anyway
2240                        if (incrementalDeltaY != 0) {
2241                            trackMotionScroll(incrementalDeltaY, incrementalDeltaY);
2242                        }
2243
2244                        // Check to see if we are back in
2245                        View motionView = this.getChildAt(mMotionPosition - mFirstPosition);
2246                        if (motionView != null) {
2247                            mTouchMode = TOUCH_MODE_SCROLL;
2248
2249                            // We did not scroll the full amount. Treat this essentially like the
2250                            // start of a new touch scroll
2251                            final int motionPosition = findClosestMotionRow(y);
2252
2253                            mMotionCorrection = 0;
2254                            motionView = getChildAt(motionPosition - mFirstPosition);
2255                            mMotionViewOriginalTop = motionView.getTop();
2256                            mMotionY = y;
2257                            mMotionPosition = motionPosition;
2258                        }
2259                    } else {
2260                        overscrollBy(0, -incrementalDeltaY, 0, mScrollY, 0, 0,
2261                                0, mOverscrollDistance, true);
2262                        if (mEdgeGlowTop != null) {
2263                            if (rawDeltaY > 0) {
2264                                mEdgeGlowTop.onPull((float) -incrementalDeltaY / getHeight());
2265                            } else if (rawDeltaY < 0) {
2266                                mEdgeGlowBottom.onPull((float) -incrementalDeltaY / getHeight());
2267                            }
2268                            invalidate();
2269                        }
2270                        if (Math.abs(mOverscrollDistance) == Math.abs(mScrollY)) {
2271                            // Don't allow overfling if we're at the edge.
2272                            mVelocityTracker.clear();
2273                        }
2274                    }
2275                    mLastY = y;
2276                }
2277                break;
2278            }
2279
2280            break;
2281        }
2282
2283        case MotionEvent.ACTION_UP: {
2284            switch (mTouchMode) {
2285            case TOUCH_MODE_DOWN:
2286            case TOUCH_MODE_TAP:
2287            case TOUCH_MODE_DONE_WAITING:
2288                final int motionPosition = mMotionPosition;
2289                final View child = getChildAt(motionPosition - mFirstPosition);
2290                if (child != null && !child.hasFocusable()) {
2291                    if (mTouchMode != TOUCH_MODE_DOWN) {
2292                        child.setPressed(false);
2293                    }
2294
2295                    if (mPerformClick == null) {
2296                        mPerformClick = new PerformClick();
2297                    }
2298
2299                    final AbsListView.PerformClick performClick = mPerformClick;
2300                    performClick.mChild = child;
2301                    performClick.mClickMotionPosition = motionPosition;
2302                    performClick.rememberWindowAttachCount();
2303
2304                    mResurrectToPosition = motionPosition;
2305
2306                    if (mTouchMode == TOUCH_MODE_DOWN || mTouchMode == TOUCH_MODE_TAP) {
2307                        final Handler handler = getHandler();
2308                        if (handler != null) {
2309                            handler.removeCallbacks(mTouchMode == TOUCH_MODE_DOWN ?
2310                                    mPendingCheckForTap : mPendingCheckForLongPress);
2311                        }
2312                        mLayoutMode = LAYOUT_NORMAL;
2313                        if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {
2314                            mTouchMode = TOUCH_MODE_TAP;
2315                            setSelectedPositionInt(mMotionPosition);
2316                            layoutChildren();
2317                            child.setPressed(true);
2318                            positionSelector(child);
2319                            setPressed(true);
2320                            if (mSelector != null) {
2321                                Drawable d = mSelector.getCurrent();
2322                                if (d != null && d instanceof TransitionDrawable) {
2323                                    ((TransitionDrawable) d).resetTransition();
2324                                }
2325                            }
2326                            postDelayed(new Runnable() {
2327                                public void run() {
2328                                    child.setPressed(false);
2329                                    setPressed(false);
2330                                    if (!mDataChanged) {
2331                                        post(performClick);
2332                                    }
2333                                    mTouchMode = TOUCH_MODE_REST;
2334                                }
2335                            }, ViewConfiguration.getPressedStateDuration());
2336                        } else {
2337                            mTouchMode = TOUCH_MODE_REST;
2338                        }
2339                        return true;
2340                    } else if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {
2341                        post(performClick);
2342                    }
2343                }
2344                mTouchMode = TOUCH_MODE_REST;
2345                break;
2346            case TOUCH_MODE_SCROLL:
2347                final int childCount = getChildCount();
2348                if (childCount > 0) {
2349                    final int firstChildTop = getChildAt(0).getTop();
2350                    final int lastChildBottom = getChildAt(childCount - 1).getBottom();
2351                    final int contentTop = mListPadding.top;
2352                    final int contentBottom = getHeight() - mListPadding.bottom;
2353                    if (mFirstPosition == 0 && firstChildTop >= contentTop &&
2354                            mFirstPosition + childCount < mItemCount &&
2355                            lastChildBottom <= getHeight() - contentBottom) {
2356                        mTouchMode = TOUCH_MODE_REST;
2357                        reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
2358                    } else {
2359                        final VelocityTracker velocityTracker = mVelocityTracker;
2360                        velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
2361                        final int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
2362
2363                        // Fling if we have enough velocity and we aren't at a boundary.
2364                        // Since we can potentially overfling more than we can overscroll, don't
2365                        // allow the weird behavior where you can scroll to a boundary then
2366                        // fling further.
2367                        if (Math.abs(initialVelocity) > mMinimumVelocity &&
2368                                !((mFirstPosition == 0 &&
2369                                        firstChildTop == contentTop - mOverscrollDistance) ||
2370                                  (mFirstPosition + childCount == mItemCount &&
2371                                        lastChildBottom == contentBottom + mOverscrollDistance))) {
2372                            if (mFlingRunnable == null) {
2373                                mFlingRunnable = new FlingRunnable();
2374                            }
2375                            reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
2376
2377                            mFlingRunnable.start(-initialVelocity);
2378                        } else {
2379                            mTouchMode = TOUCH_MODE_REST;
2380                            reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
2381                        }
2382                    }
2383                } else {
2384                    mTouchMode = TOUCH_MODE_REST;
2385                    reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
2386                }
2387                break;
2388
2389            case TOUCH_MODE_OVERSCROLL:
2390                if (mFlingRunnable == null) {
2391                    mFlingRunnable = new FlingRunnable();
2392                }
2393                final VelocityTracker velocityTracker = mVelocityTracker;
2394                velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
2395                final int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
2396
2397                reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
2398                if (Math.abs(initialVelocity) > mMinimumVelocity) {
2399                    mFlingRunnable.startOverfling(-initialVelocity);
2400                } else {
2401                    mFlingRunnable.startSpringback();
2402                }
2403
2404                break;
2405            }
2406
2407            setPressed(false);
2408
2409            if (mEdgeGlowTop != null) {
2410                mEdgeGlowTop.onRelease();
2411                mEdgeGlowBottom.onRelease();
2412            }
2413
2414            // Need to redraw since we probably aren't drawing the selector anymore
2415            invalidate();
2416
2417            final Handler handler = getHandler();
2418            if (handler != null) {
2419                handler.removeCallbacks(mPendingCheckForLongPress);
2420            }
2421
2422            if (mVelocityTracker != null) {
2423                mVelocityTracker.recycle();
2424                mVelocityTracker = null;
2425            }
2426
2427            mActivePointerId = INVALID_POINTER;
2428
2429            if (PROFILE_SCROLLING) {
2430                if (mScrollProfilingStarted) {
2431                    Debug.stopMethodTracing();
2432                    mScrollProfilingStarted = false;
2433                }
2434            }
2435            break;
2436        }
2437
2438        case MotionEvent.ACTION_CANCEL: {
2439            switch (mTouchMode) {
2440            case TOUCH_MODE_OVERSCROLL:
2441                if (mFlingRunnable == null) {
2442                    mFlingRunnable = new FlingRunnable();
2443                }
2444                mFlingRunnable.startSpringback();
2445                break;
2446
2447            case TOUCH_MODE_OVERFLING:
2448                // Do nothing - let it play out.
2449                break;
2450
2451            default:
2452                mTouchMode = TOUCH_MODE_REST;
2453                setPressed(false);
2454                View motionView = this.getChildAt(mMotionPosition - mFirstPosition);
2455                if (motionView != null) {
2456                    motionView.setPressed(false);
2457                }
2458                clearScrollingCache();
2459
2460                final Handler handler = getHandler();
2461                if (handler != null) {
2462                    handler.removeCallbacks(mPendingCheckForLongPress);
2463                }
2464
2465                if (mVelocityTracker != null) {
2466                    mVelocityTracker.recycle();
2467                    mVelocityTracker = null;
2468                }
2469            }
2470
2471            if (mEdgeGlowTop != null) {
2472                mEdgeGlowTop.onRelease();
2473                mEdgeGlowBottom.onRelease();
2474            }
2475            mActivePointerId = INVALID_POINTER;
2476            break;
2477        }
2478
2479        case MotionEvent.ACTION_POINTER_UP: {
2480            onSecondaryPointerUp(ev);
2481            final int x = mMotionX;
2482            final int y = mMotionY;
2483            final int motionPosition = pointToPosition(x, y);
2484            if (motionPosition >= 0) {
2485                // Remember where the motion event started
2486                v = getChildAt(motionPosition - mFirstPosition);
2487                mMotionViewOriginalTop = v.getTop();
2488                mMotionPosition = motionPosition;
2489            }
2490            mLastY = y;
2491            break;
2492        }
2493        }
2494
2495        return true;
2496    }
2497
2498    @Override
2499    protected void onOverscrolled(int scrollX, int scrollY,
2500            boolean clampedX, boolean clampedY) {
2501        mScrollY = scrollY;
2502
2503        if (clampedY) {
2504            // Velocity is broken by hitting the limit; don't start a fling off of this.
2505            if (mVelocityTracker != null) {
2506                mVelocityTracker.clear();
2507            }
2508        }
2509        awakenScrollBars();
2510    }
2511
2512    @Override
2513    public void draw(Canvas canvas) {
2514        super.draw(canvas);
2515        if (mEdgeGlowTop != null) {
2516            final int scrollY = mScrollY;
2517            if (!mEdgeGlowTop.isFinished()) {
2518                final int restoreCount = canvas.save();
2519                final int width = getWidth();
2520
2521                canvas.translate(-width / 2, scrollY);
2522                mEdgeGlowTop.setSize(width * 2, getHeight());
2523                if (mEdgeGlowTop.draw(canvas)) {
2524                    invalidate();
2525                }
2526                canvas.restoreToCount(restoreCount);
2527            }
2528            if (!mEdgeGlowBottom.isFinished()) {
2529                final int restoreCount = canvas.save();
2530                final int width = getWidth();
2531                final int height = getHeight();
2532
2533                canvas.translate(-width / 2, scrollY + height);
2534                canvas.rotate(180, width, 0);
2535                mEdgeGlowBottom.setSize(width * 2, height);
2536                if (mEdgeGlowBottom.draw(canvas)) {
2537                    invalidate();
2538                }
2539                canvas.restoreToCount(restoreCount);
2540            }
2541        }
2542        if (mFastScroller != null) {
2543            final int scrollY = mScrollY;
2544            if (scrollY != 0) {
2545                // Pin to the top/bottom during overscroll
2546                int restoreCount = canvas.save();
2547                canvas.translate(0, (float) scrollY);
2548                mFastScroller.draw(canvas);
2549                canvas.restoreToCount(restoreCount);
2550            } else {
2551                mFastScroller.draw(canvas);
2552            }
2553        }
2554    }
2555
2556    @Override
2557    public boolean onInterceptTouchEvent(MotionEvent ev) {
2558        int action = ev.getAction();
2559        View v;
2560
2561        if (mFastScroller != null) {
2562            boolean intercepted = mFastScroller.onInterceptTouchEvent(ev);
2563            if (intercepted) {
2564                return true;
2565            }
2566        }
2567
2568        switch (action & MotionEvent.ACTION_MASK) {
2569        case MotionEvent.ACTION_DOWN: {
2570            int touchMode = mTouchMode;
2571            if (touchMode == TOUCH_MODE_OVERFLING || touchMode == TOUCH_MODE_OVERSCROLL) {
2572                mMotionCorrection = 0;
2573                return true;
2574            }
2575
2576            final int x = (int) ev.getX();
2577            final int y = (int) ev.getY();
2578            mActivePointerId = ev.getPointerId(0);
2579
2580            int motionPosition = findMotionRow(y);
2581            if (touchMode != TOUCH_MODE_FLING && motionPosition >= 0) {
2582                // User clicked on an actual view (and was not stopping a fling).
2583                // Remember where the motion event started
2584                v = getChildAt(motionPosition - mFirstPosition);
2585                mMotionViewOriginalTop = v.getTop();
2586                mMotionX = x;
2587                mMotionY = y;
2588                mMotionPosition = motionPosition;
2589                mTouchMode = TOUCH_MODE_DOWN;
2590                clearScrollingCache();
2591            }
2592            mLastY = Integer.MIN_VALUE;
2593            if (touchMode == TOUCH_MODE_FLING) {
2594                return true;
2595            }
2596            break;
2597        }
2598
2599        case MotionEvent.ACTION_MOVE: {
2600            switch (mTouchMode) {
2601            case TOUCH_MODE_DOWN:
2602                final int pointerIndex = ev.findPointerIndex(mActivePointerId);
2603                final int y = (int) ev.getY(pointerIndex);
2604                if (startScrollIfNeeded(y - mMotionY)) {
2605                    return true;
2606                }
2607                break;
2608            }
2609            break;
2610        }
2611
2612        case MotionEvent.ACTION_UP: {
2613            mTouchMode = TOUCH_MODE_REST;
2614            mActivePointerId = INVALID_POINTER;
2615            reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
2616            break;
2617        }
2618
2619        case MotionEvent.ACTION_POINTER_UP: {
2620            onSecondaryPointerUp(ev);
2621            break;
2622        }
2623        }
2624
2625        return false;
2626    }
2627
2628    private void onSecondaryPointerUp(MotionEvent ev) {
2629        final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
2630                MotionEvent.ACTION_POINTER_INDEX_SHIFT;
2631        final int pointerId = ev.getPointerId(pointerIndex);
2632        if (pointerId == mActivePointerId) {
2633            // This was our active pointer going up. Choose a new
2634            // active pointer and adjust accordingly.
2635            // TODO: Make this decision more intelligent.
2636            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
2637            mMotionX = (int) ev.getX(newPointerIndex);
2638            mMotionY = (int) ev.getY(newPointerIndex);
2639            mMotionCorrection = 0;
2640            mActivePointerId = ev.getPointerId(newPointerIndex);
2641            if (mVelocityTracker != null) {
2642                mVelocityTracker.clear();
2643            }
2644        }
2645    }
2646
2647    /**
2648     * {@inheritDoc}
2649     */
2650    @Override
2651    public void addTouchables(ArrayList<View> views) {
2652        final int count = getChildCount();
2653        final int firstPosition = mFirstPosition;
2654        final ListAdapter adapter = mAdapter;
2655
2656        if (adapter == null) {
2657            return;
2658        }
2659
2660        for (int i = 0; i < count; i++) {
2661            final View child = getChildAt(i);
2662            if (adapter.isEnabled(firstPosition + i)) {
2663                views.add(child);
2664            }
2665            child.addTouchables(views);
2666        }
2667    }
2668
2669    /**
2670     * Fires an "on scroll state changed" event to the registered
2671     * {@link android.widget.AbsListView.OnScrollListener}, if any. The state change
2672     * is fired only if the specified state is different from the previously known state.
2673     *
2674     * @param newState The new scroll state.
2675     */
2676    void reportScrollStateChange(int newState) {
2677        if (newState != mLastScrollState) {
2678            if (mOnScrollListener != null) {
2679                mOnScrollListener.onScrollStateChanged(this, newState);
2680                mLastScrollState = newState;
2681            }
2682        }
2683    }
2684
2685    /**
2686     * Responsible for fling behavior. Use {@link #start(int)} to
2687     * initiate a fling. Each frame of the fling is handled in {@link #run()}.
2688     * A FlingRunnable will keep re-posting itself until the fling is done.
2689     *
2690     */
2691    private class FlingRunnable implements Runnable {
2692        /**
2693         * Tracks the decay of a fling scroll
2694         */
2695        private final OverScroller mScroller;
2696
2697        /**
2698         * Y value reported by mScroller on the previous fling
2699         */
2700        private int mLastFlingY;
2701
2702        FlingRunnable() {
2703            mScroller = new OverScroller(getContext());
2704        }
2705
2706        void start(int initialVelocity) {
2707            int initialY = initialVelocity < 0 ? Integer.MAX_VALUE : 0;
2708            mLastFlingY = initialY;
2709            mScroller.fling(0, initialY, 0, initialVelocity,
2710                    0, Integer.MAX_VALUE, 0, Integer.MAX_VALUE);
2711            mTouchMode = TOUCH_MODE_FLING;
2712            post(this);
2713
2714            if (PROFILE_FLINGING) {
2715                if (!mFlingProfilingStarted) {
2716                    Debug.startMethodTracing("AbsListViewFling");
2717                    mFlingProfilingStarted = true;
2718                }
2719            }
2720        }
2721
2722        void startSpringback() {
2723            if (mScroller.springback(0, mScrollY, 0, 0, 0, 0)) {
2724                mTouchMode = TOUCH_MODE_OVERFLING;
2725                invalidate();
2726                post(this);
2727            } else {
2728                mTouchMode = TOUCH_MODE_REST;
2729            }
2730        }
2731
2732        void startOverfling(int initialVelocity) {
2733            final int min = mScrollY > 0 ? Integer.MIN_VALUE : 0;
2734            final int max = mScrollY > 0 ? 0 : Integer.MAX_VALUE;
2735            mScroller.fling(0, mScrollY, 0, initialVelocity, 0, 0, min, max, 0, getHeight());
2736            mTouchMode = TOUCH_MODE_OVERFLING;
2737            invalidate();
2738            post(this);
2739        }
2740
2741        void edgeReached(int delta) {
2742            mScroller.notifyVerticalEdgeReached(mScrollY, 0, mOverflingDistance);
2743            mTouchMode = TOUCH_MODE_OVERFLING;
2744            if (mEdgeGlowTop != null) {
2745                final int vel = (int) mScroller.getCurrVelocity();
2746                if (delta > 0) {
2747                    mEdgeGlowTop.onAbsorb(vel);
2748                } else {
2749                    mEdgeGlowBottom.onAbsorb(vel);
2750                }
2751            }
2752            invalidate();
2753            post(this);
2754        }
2755
2756        void startScroll(int distance, int duration) {
2757            int initialY = distance < 0 ? Integer.MAX_VALUE : 0;
2758            mLastFlingY = initialY;
2759            mScroller.startScroll(0, initialY, 0, distance, duration);
2760            mTouchMode = TOUCH_MODE_FLING;
2761            post(this);
2762        }
2763
2764        private void endFling() {
2765            mTouchMode = TOUCH_MODE_REST;
2766
2767            reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
2768            clearScrollingCache();
2769
2770            removeCallbacks(this);
2771
2772            if (mPositionScroller != null) {
2773                removeCallbacks(mPositionScroller);
2774            }
2775        }
2776
2777        public void run() {
2778            switch (mTouchMode) {
2779            default:
2780                return;
2781
2782            case TOUCH_MODE_FLING: {
2783                if (mItemCount == 0 || getChildCount() == 0) {
2784                    endFling();
2785                    return;
2786                }
2787
2788                final OverScroller scroller = mScroller;
2789                boolean more = scroller.computeScrollOffset();
2790                final int y = scroller.getCurrY();
2791
2792                // Flip sign to convert finger direction to list items direction
2793                // (e.g. finger moving down means list is moving towards the top)
2794                int delta = mLastFlingY - y;
2795
2796                // Pretend that each frame of a fling scroll is a touch scroll
2797                if (delta > 0) {
2798                    // List is moving towards the top. Use first view as mMotionPosition
2799                    mMotionPosition = mFirstPosition;
2800                    final View firstView = getChildAt(0);
2801                    mMotionViewOriginalTop = firstView.getTop();
2802
2803                    // Don't fling more than 1 screen
2804                    delta = Math.min(getHeight() - mPaddingBottom - mPaddingTop - 1, delta);
2805                } else {
2806                    // List is moving towards the bottom. Use last view as mMotionPosition
2807                    int offsetToLast = getChildCount() - 1;
2808                    mMotionPosition = mFirstPosition + offsetToLast;
2809
2810                    final View lastView = getChildAt(offsetToLast);
2811                    mMotionViewOriginalTop = lastView.getTop();
2812
2813                    // Don't fling more than 1 screen
2814                    delta = Math.max(-(getHeight() - mPaddingBottom - mPaddingTop - 1), delta);
2815                }
2816
2817                // Check to see if we have bumped into the scroll limit
2818                View motionView = getChildAt(mMotionPosition - mFirstPosition);
2819                int oldTop = 0;
2820                if (motionView != null) {
2821                    oldTop = motionView.getTop();
2822                }
2823
2824                final boolean atEnd = trackMotionScroll(delta, delta);
2825                if (atEnd) {
2826                    if (motionView != null) {
2827                        // Tweak the scroll for how far we overshot
2828                        int overshoot = -(delta - (motionView.getTop() - oldTop));
2829                        overscrollBy(0, overshoot, 0, mScrollY, 0, 0,
2830                                0, mOverflingDistance, false);
2831                    }
2832                    edgeReached(delta);
2833                    break;
2834                }
2835
2836                if (more && !atEnd) {
2837                    invalidate();
2838                    mLastFlingY = y;
2839                    post(this);
2840                } else {
2841                    endFling();
2842
2843                    if (PROFILE_FLINGING) {
2844                        if (mFlingProfilingStarted) {
2845                            Debug.stopMethodTracing();
2846                            mFlingProfilingStarted = false;
2847                        }
2848                    }
2849                }
2850                break;
2851            }
2852
2853            case TOUCH_MODE_OVERFLING: {
2854                final OverScroller scroller = mScroller;
2855                if (scroller.computeScrollOffset()) {
2856                    final int scrollY = mScrollY;
2857                    final int deltaY = scroller.getCurrY() - scrollY;
2858                    if (overscrollBy(0, deltaY, 0, scrollY, 0, 0,
2859                            0, mOverflingDistance, false)) {
2860                        startSpringback();
2861                    } else {
2862                        invalidate();
2863                        post(this);
2864                    }
2865                } else {
2866                    endFling();
2867                }
2868                break;
2869            }
2870            }
2871
2872        }
2873    }
2874
2875
2876    class PositionScroller implements Runnable {
2877        private static final int SCROLL_DURATION = 400;
2878
2879        private static final int MOVE_DOWN_POS = 1;
2880        private static final int MOVE_UP_POS = 2;
2881        private static final int MOVE_DOWN_BOUND = 3;
2882        private static final int MOVE_UP_BOUND = 4;
2883
2884        private int mMode;
2885        private int mTargetPos;
2886        private int mBoundPos;
2887        private int mLastSeenPos;
2888        private int mScrollDuration;
2889        private final int mExtraScroll;
2890
2891        PositionScroller() {
2892            mExtraScroll = ViewConfiguration.get(mContext).getScaledFadingEdgeLength();
2893        }
2894
2895        void start(int position) {
2896            final int firstPos = mFirstPosition;
2897            final int lastPos = firstPos + getChildCount() - 1;
2898
2899            int viewTravelCount = 0;
2900            if (position <= firstPos) {
2901                viewTravelCount = firstPos - position + 1;
2902                mMode = MOVE_UP_POS;
2903            } else if (position >= lastPos) {
2904                viewTravelCount = position - lastPos + 1;
2905                mMode = MOVE_DOWN_POS;
2906            } else {
2907                // Already on screen, nothing to do
2908                return;
2909            }
2910
2911            if (viewTravelCount > 0) {
2912                mScrollDuration = SCROLL_DURATION / viewTravelCount;
2913            } else {
2914                mScrollDuration = SCROLL_DURATION;
2915            }
2916            mTargetPos = position;
2917            mBoundPos = INVALID_POSITION;
2918            mLastSeenPos = INVALID_POSITION;
2919
2920            post(this);
2921        }
2922
2923        void start(int position, int boundPosition) {
2924            if (boundPosition == INVALID_POSITION) {
2925                start(position);
2926                return;
2927            }
2928
2929            final int firstPos = mFirstPosition;
2930            final int lastPos = firstPos + getChildCount() - 1;
2931
2932            int viewTravelCount = 0;
2933            if (position <= firstPos) {
2934                final int boundPosFromLast = lastPos - boundPosition;
2935                if (boundPosFromLast < 1) {
2936                    // Moving would shift our bound position off the screen. Abort.
2937                    return;
2938                }
2939
2940                final int posTravel = firstPos - position + 1;
2941                final int boundTravel = boundPosFromLast - 1;
2942                if (boundTravel < posTravel) {
2943                    viewTravelCount = boundTravel;
2944                    mMode = MOVE_UP_BOUND;
2945                } else {
2946                    viewTravelCount = posTravel;
2947                    mMode = MOVE_UP_POS;
2948                }
2949            } else if (position >= lastPos) {
2950                final int boundPosFromFirst = boundPosition - firstPos;
2951                if (boundPosFromFirst < 1) {
2952                    // Moving would shift our bound position off the screen. Abort.
2953                    return;
2954                }
2955
2956                final int posTravel = position - lastPos + 1;
2957                final int boundTravel = boundPosFromFirst - 1;
2958                if (boundTravel < posTravel) {
2959                    viewTravelCount = boundTravel;
2960                    mMode = MOVE_DOWN_BOUND;
2961                } else {
2962                    viewTravelCount = posTravel;
2963                    mMode = MOVE_DOWN_POS;
2964                }
2965            } else {
2966                // Already on screen, nothing to do
2967                return;
2968            }
2969
2970            if (viewTravelCount > 0) {
2971                mScrollDuration = SCROLL_DURATION / viewTravelCount;
2972            } else {
2973                mScrollDuration = SCROLL_DURATION;
2974            }
2975            mTargetPos = position;
2976            mBoundPos = boundPosition;
2977            mLastSeenPos = INVALID_POSITION;
2978
2979            post(this);
2980        }
2981
2982        void stop() {
2983            removeCallbacks(this);
2984        }
2985
2986        public void run() {
2987            final int listHeight = getHeight();
2988            final int firstPos = mFirstPosition;
2989
2990            switch (mMode) {
2991            case MOVE_DOWN_POS: {
2992                final int lastViewIndex = getChildCount() - 1;
2993                final int lastPos = firstPos + lastViewIndex;
2994
2995                if (lastViewIndex < 0) {
2996                    return;
2997                }
2998
2999                if (lastPos == mLastSeenPos) {
3000                    // No new views, let things keep going.
3001                    post(this);
3002                    return;
3003                }
3004
3005                final View lastView = getChildAt(lastViewIndex);
3006                final int lastViewHeight = lastView.getHeight();
3007                final int lastViewTop = lastView.getTop();
3008                final int lastViewPixelsShowing = listHeight - lastViewTop;
3009                final int extraScroll = lastPos < mItemCount - 1 ? mExtraScroll : mListPadding.bottom;
3010
3011                smoothScrollBy(lastViewHeight - lastViewPixelsShowing + extraScroll,
3012                        mScrollDuration);
3013
3014                mLastSeenPos = lastPos;
3015                if (lastPos < mTargetPos) {
3016                    post(this);
3017                }
3018                break;
3019            }
3020
3021            case MOVE_DOWN_BOUND: {
3022                final int nextViewIndex = 1;
3023                final int childCount = getChildCount();
3024
3025                if (firstPos == mBoundPos || childCount <= nextViewIndex
3026                        || firstPos + childCount >= mItemCount) {
3027                    return;
3028                }
3029                final int nextPos = firstPos + nextViewIndex;
3030
3031                if (nextPos == mLastSeenPos) {
3032                    // No new views, let things keep going.
3033                    post(this);
3034                    return;
3035                }
3036
3037                final View nextView = getChildAt(nextViewIndex);
3038                final int nextViewHeight = nextView.getHeight();
3039                final int nextViewTop = nextView.getTop();
3040                final int extraScroll = mExtraScroll;
3041                if (nextPos < mBoundPos) {
3042                    smoothScrollBy(Math.max(0, nextViewHeight + nextViewTop - extraScroll),
3043                            mScrollDuration);
3044
3045                    mLastSeenPos = nextPos;
3046
3047                    post(this);
3048                } else  {
3049                    if (nextViewTop > extraScroll) {
3050                        smoothScrollBy(nextViewTop - extraScroll, mScrollDuration);
3051                    }
3052                }
3053                break;
3054            }
3055
3056            case MOVE_UP_POS: {
3057                if (firstPos == mLastSeenPos) {
3058                    // No new views, let things keep going.
3059                    post(this);
3060                    return;
3061                }
3062
3063                final View firstView = getChildAt(0);
3064                if (firstView == null) {
3065                    return;
3066                }
3067                final int firstViewTop = firstView.getTop();
3068                final int extraScroll = firstPos > 0 ? mExtraScroll : mListPadding.top;
3069
3070                smoothScrollBy(firstViewTop - extraScroll, mScrollDuration);
3071
3072                mLastSeenPos = firstPos;
3073
3074                if (firstPos > mTargetPos) {
3075                    post(this);
3076                }
3077                break;
3078            }
3079
3080            case MOVE_UP_BOUND: {
3081                final int lastViewIndex = getChildCount() - 2;
3082                if (lastViewIndex < 0) {
3083                    return;
3084                }
3085                final int lastPos = firstPos + lastViewIndex;
3086
3087                if (lastPos == mLastSeenPos) {
3088                    // No new views, let things keep going.
3089                    post(this);
3090                    return;
3091                }
3092
3093                final View lastView = getChildAt(lastViewIndex);
3094                final int lastViewHeight = lastView.getHeight();
3095                final int lastViewTop = lastView.getTop();
3096                final int lastViewPixelsShowing = listHeight - lastViewTop;
3097                mLastSeenPos = lastPos;
3098                if (lastPos > mBoundPos) {
3099                    smoothScrollBy(-(lastViewPixelsShowing - mExtraScroll), mScrollDuration);
3100                    post(this);
3101                } else {
3102                    final int bottom = listHeight - mExtraScroll;
3103                    final int lastViewBottom = lastViewTop + lastViewHeight;
3104                    if (bottom > lastViewBottom) {
3105                        smoothScrollBy(-(bottom - lastViewBottom), mScrollDuration);
3106                    }
3107                }
3108                break;
3109            }
3110
3111            default:
3112                break;
3113            }
3114        }
3115    }
3116
3117    /**
3118     * Smoothly scroll to the specified adapter position. The view will
3119     * scroll such that the indicated position is displayed.
3120     * @param position Scroll to this adapter position.
3121     */
3122    public void smoothScrollToPosition(int position) {
3123        if (mPositionScroller == null) {
3124            mPositionScroller = new PositionScroller();
3125        }
3126        mPositionScroller.start(position);
3127    }
3128
3129    /**
3130     * Smoothly scroll to the specified adapter position. The view will
3131     * scroll such that the indicated position is displayed, but it will
3132     * stop early if scrolling further would scroll boundPosition out of
3133     * view.
3134     * @param position Scroll to this adapter position.
3135     * @param boundPosition Do not scroll if it would move this adapter
3136     *          position out of view.
3137     */
3138    public void smoothScrollToPosition(int position, int boundPosition) {
3139        if (mPositionScroller == null) {
3140            mPositionScroller = new PositionScroller();
3141        }
3142        mPositionScroller.start(position, boundPosition);
3143    }
3144
3145    /**
3146     * Smoothly scroll by distance pixels over duration milliseconds.
3147     * @param distance Distance to scroll in pixels.
3148     * @param duration Duration of the scroll animation in milliseconds.
3149     */
3150    public void smoothScrollBy(int distance, int duration) {
3151        if (mFlingRunnable == null) {
3152            mFlingRunnable = new FlingRunnable();
3153        } else {
3154            mFlingRunnable.endFling();
3155        }
3156        mFlingRunnable.startScroll(distance, duration);
3157    }
3158
3159    private void createScrollingCache() {
3160        if (mScrollingCacheEnabled && !mCachingStarted) {
3161            setChildrenDrawnWithCacheEnabled(true);
3162            setChildrenDrawingCacheEnabled(true);
3163            mCachingStarted = true;
3164        }
3165    }
3166
3167    private void clearScrollingCache() {
3168        if (mClearScrollingCache == null) {
3169            mClearScrollingCache = new Runnable() {
3170                public void run() {
3171                    if (mCachingStarted) {
3172                        mCachingStarted = false;
3173                        setChildrenDrawnWithCacheEnabled(false);
3174                        if ((mPersistentDrawingCache & PERSISTENT_SCROLLING_CACHE) == 0) {
3175                            setChildrenDrawingCacheEnabled(false);
3176                        }
3177                        if (!isAlwaysDrawnWithCacheEnabled()) {
3178                            invalidate();
3179                        }
3180                    }
3181                }
3182            };
3183        }
3184        post(mClearScrollingCache);
3185    }
3186
3187    /**
3188     * Track a motion scroll
3189     *
3190     * @param deltaY Amount to offset mMotionView. This is the accumulated delta since the motion
3191     *        began. Positive numbers mean the user's finger is moving down the screen.
3192     * @param incrementalDeltaY Change in deltaY from the previous event.
3193     * @return true if we're already at the beginning/end of the list and have nothing to do.
3194     */
3195    boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
3196        final int childCount = getChildCount();
3197        if (childCount == 0) {
3198            return true;
3199        }
3200
3201        final int firstTop = getChildAt(0).getTop();
3202        final int lastBottom = getChildAt(childCount - 1).getBottom();
3203
3204        final Rect listPadding = mListPadding;
3205
3206         // FIXME account for grid vertical spacing too?
3207        final int spaceAbove = listPadding.top - firstTop;
3208        final int end = getHeight() - listPadding.bottom;
3209        final int spaceBelow = lastBottom - end;
3210
3211        final int height = getHeight() - mPaddingBottom - mPaddingTop;
3212        if (deltaY < 0) {
3213            deltaY = Math.max(-(height - 1), deltaY);
3214        } else {
3215            deltaY = Math.min(height - 1, deltaY);
3216        }
3217
3218        if (incrementalDeltaY < 0) {
3219            incrementalDeltaY = Math.max(-(height - 1), incrementalDeltaY);
3220        } else {
3221            incrementalDeltaY = Math.min(height - 1, incrementalDeltaY);
3222        }
3223
3224        final int firstPosition = mFirstPosition;
3225
3226        if (firstPosition == 0 && firstTop >= listPadding.top && incrementalDeltaY >= 0) {
3227            // Don't need to move views down if the top of the first position
3228            // is already visible
3229            return incrementalDeltaY != 0;
3230        }
3231
3232        if (firstPosition + childCount == mItemCount && lastBottom <= end &&
3233                incrementalDeltaY <= 0) {
3234            // Don't need to move views up if the bottom of the last position
3235            // is already visible
3236            return incrementalDeltaY != 0;
3237        }
3238
3239        final boolean down = incrementalDeltaY < 0;
3240
3241        final boolean inTouchMode = isInTouchMode();
3242        if (inTouchMode) {
3243            hideSelector();
3244        }
3245
3246        final int headerViewsCount = getHeaderViewsCount();
3247        final int footerViewsStart = mItemCount - getFooterViewsCount();
3248
3249        int start = 0;
3250        int count = 0;
3251
3252        if (down) {
3253            final int top = listPadding.top - incrementalDeltaY;
3254            for (int i = 0; i < childCount; i++) {
3255                final View child = getChildAt(i);
3256                if (child.getBottom() >= top) {
3257                    break;
3258                } else {
3259                    count++;
3260                    int position = firstPosition + i;
3261                    if (position >= headerViewsCount && position < footerViewsStart) {
3262                        mRecycler.addScrapView(child);
3263
3264                        if (ViewDebug.TRACE_RECYCLER) {
3265                            ViewDebug.trace(child,
3266                                    ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP,
3267                                    firstPosition + i, -1);
3268                        }
3269                    }
3270                }
3271            }
3272        } else {
3273            final int bottom = getHeight() - listPadding.bottom - incrementalDeltaY;
3274            for (int i = childCount - 1; i >= 0; i--) {
3275                final View child = getChildAt(i);
3276                if (child.getTop() <= bottom) {
3277                    break;
3278                } else {
3279                    start = i;
3280                    count++;
3281                    int position = firstPosition + i;
3282                    if (position >= headerViewsCount && position < footerViewsStart) {
3283                        mRecycler.addScrapView(child);
3284
3285                        if (ViewDebug.TRACE_RECYCLER) {
3286                            ViewDebug.trace(child,
3287                                    ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP,
3288                                    firstPosition + i, -1);
3289                        }
3290                    }
3291                }
3292            }
3293        }
3294
3295        mMotionViewNewTop = mMotionViewOriginalTop + deltaY;
3296
3297        mBlockLayoutRequests = true;
3298
3299        if (count > 0) {
3300            detachViewsFromParent(start, count);
3301        }
3302        offsetChildrenTopAndBottom(incrementalDeltaY);
3303
3304        if (down) {
3305            mFirstPosition += count;
3306        }
3307
3308        invalidate();
3309
3310        final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
3311        if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
3312            fillGap(down);
3313        }
3314
3315        if (!inTouchMode && mSelectedPosition != INVALID_POSITION) {
3316            final int childIndex = mSelectedPosition - mFirstPosition;
3317            if (childIndex >= 0 && childIndex < getChildCount()) {
3318                positionSelector(getChildAt(childIndex));
3319            }
3320        }
3321
3322        mBlockLayoutRequests = false;
3323
3324        invokeOnItemScrollListener();
3325        awakenScrollBars();
3326
3327        return false;
3328    }
3329
3330    /**
3331     * Returns the number of header views in the list. Header views are special views
3332     * at the top of the list that should not be recycled during a layout.
3333     *
3334     * @return The number of header views, 0 in the default implementation.
3335     */
3336    int getHeaderViewsCount() {
3337        return 0;
3338    }
3339
3340    /**
3341     * Returns the number of footer views in the list. Footer views are special views
3342     * at the bottom of the list that should not be recycled during a layout.
3343     *
3344     * @return The number of footer views, 0 in the default implementation.
3345     */
3346    int getFooterViewsCount() {
3347        return 0;
3348    }
3349
3350    /**
3351     * Fills the gap left open by a touch-scroll. During a touch scroll, children that
3352     * remain on screen are shifted and the other ones are discarded. The role of this
3353     * method is to fill the gap thus created by performing a partial layout in the
3354     * empty space.
3355     *
3356     * @param down true if the scroll is going down, false if it is going up
3357     */
3358    abstract void fillGap(boolean down);
3359
3360    void hideSelector() {
3361        if (mSelectedPosition != INVALID_POSITION) {
3362            if (mLayoutMode != LAYOUT_SPECIFIC) {
3363                mResurrectToPosition = mSelectedPosition;
3364            }
3365            if (mNextSelectedPosition >= 0 && mNextSelectedPosition != mSelectedPosition) {
3366                mResurrectToPosition = mNextSelectedPosition;
3367            }
3368            setSelectedPositionInt(INVALID_POSITION);
3369            setNextSelectedPositionInt(INVALID_POSITION);
3370            mSelectedTop = 0;
3371            mSelectorRect.setEmpty();
3372        }
3373    }
3374
3375    /**
3376     * @return A position to select. First we try mSelectedPosition. If that has been clobbered by
3377     * entering touch mode, we then try mResurrectToPosition. Values are pinned to the range
3378     * of items available in the adapter
3379     */
3380    int reconcileSelectedPosition() {
3381        int position = mSelectedPosition;
3382        if (position < 0) {
3383            position = mResurrectToPosition;
3384        }
3385        position = Math.max(0, position);
3386        position = Math.min(position, mItemCount - 1);
3387        return position;
3388    }
3389
3390    /**
3391     * Find the row closest to y. This row will be used as the motion row when scrolling
3392     *
3393     * @param y Where the user touched
3394     * @return The position of the first (or only) item in the row containing y
3395     */
3396    abstract int findMotionRow(int y);
3397
3398    /**
3399     * Find the row closest to y. This row will be used as the motion row when scrolling.
3400     *
3401     * @param y Where the user touched
3402     * @return The position of the first (or only) item in the row closest to y
3403     */
3404    int findClosestMotionRow(int y) {
3405        final int childCount = getChildCount();
3406        if (childCount == 0) {
3407            return INVALID_POSITION;
3408        }
3409
3410        final int motionRow = findMotionRow(y);
3411        return motionRow != INVALID_POSITION ? motionRow : mFirstPosition + childCount - 1;
3412    }
3413
3414    /**
3415     * Causes all the views to be rebuilt and redrawn.
3416     */
3417    public void invalidateViews() {
3418        mDataChanged = true;
3419        rememberSyncState();
3420        requestLayout();
3421        invalidate();
3422    }
3423
3424    /**
3425     * Makes the item at the supplied position selected.
3426     *
3427     * @param position the position of the new selection
3428     */
3429    abstract void setSelectionInt(int position);
3430
3431    /**
3432     * Attempt to bring the selection back if the user is switching from touch
3433     * to trackball mode
3434     * @return Whether selection was set to something.
3435     */
3436    boolean resurrectSelection() {
3437        final int childCount = getChildCount();
3438
3439        if (childCount <= 0) {
3440            return false;
3441        }
3442
3443        int selectedTop = 0;
3444        int selectedPos;
3445        int childrenTop = mListPadding.top;
3446        int childrenBottom = mBottom - mTop - mListPadding.bottom;
3447        final int firstPosition = mFirstPosition;
3448        final int toPosition = mResurrectToPosition;
3449        boolean down = true;
3450
3451        if (toPosition >= firstPosition && toPosition < firstPosition + childCount) {
3452            selectedPos = toPosition;
3453
3454            final View selected = getChildAt(selectedPos - mFirstPosition);
3455            selectedTop = selected.getTop();
3456            int selectedBottom = selected.getBottom();
3457
3458            // We are scrolled, don't get in the fade
3459            if (selectedTop < childrenTop) {
3460                selectedTop = childrenTop + getVerticalFadingEdgeLength();
3461            } else if (selectedBottom > childrenBottom) {
3462                selectedTop = childrenBottom - selected.getMeasuredHeight()
3463                        - getVerticalFadingEdgeLength();
3464            }
3465        } else {
3466            if (toPosition < firstPosition) {
3467                // Default to selecting whatever is first
3468                selectedPos = firstPosition;
3469                for (int i = 0; i < childCount; i++) {
3470                    final View v = getChildAt(i);
3471                    final int top = v.getTop();
3472
3473                    if (i == 0) {
3474                        // Remember the position of the first item
3475                        selectedTop = top;
3476                        // See if we are scrolled at all
3477                        if (firstPosition > 0 || top < childrenTop) {
3478                            // If we are scrolled, don't select anything that is
3479                            // in the fade region
3480                            childrenTop += getVerticalFadingEdgeLength();
3481                        }
3482                    }
3483                    if (top >= childrenTop) {
3484                        // Found a view whose top is fully visisble
3485                        selectedPos = firstPosition + i;
3486                        selectedTop = top;
3487                        break;
3488                    }
3489                }
3490            } else {
3491                final int itemCount = mItemCount;
3492                down = false;
3493                selectedPos = firstPosition + childCount - 1;
3494
3495                for (int i = childCount - 1; i >= 0; i--) {
3496                    final View v = getChildAt(i);
3497                    final int top = v.getTop();
3498                    final int bottom = v.getBottom();
3499
3500                    if (i == childCount - 1) {
3501                        selectedTop = top;
3502                        if (firstPosition + childCount < itemCount || bottom > childrenBottom) {
3503                            childrenBottom -= getVerticalFadingEdgeLength();
3504                        }
3505                    }
3506
3507                    if (bottom <= childrenBottom) {
3508                        selectedPos = firstPosition + i;
3509                        selectedTop = top;
3510                        break;
3511                    }
3512                }
3513            }
3514        }
3515
3516        mResurrectToPosition = INVALID_POSITION;
3517        removeCallbacks(mFlingRunnable);
3518        mTouchMode = TOUCH_MODE_REST;
3519        clearScrollingCache();
3520        mSpecificTop = selectedTop;
3521        selectedPos = lookForSelectablePosition(selectedPos, down);
3522        if (selectedPos >= firstPosition && selectedPos <= getLastVisiblePosition()) {
3523            mLayoutMode = LAYOUT_SPECIFIC;
3524            setSelectionInt(selectedPos);
3525            invokeOnItemScrollListener();
3526        } else {
3527            selectedPos = INVALID_POSITION;
3528        }
3529        reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
3530
3531        return selectedPos >= 0;
3532    }
3533
3534    @Override
3535    protected void handleDataChanged() {
3536        int count = mItemCount;
3537        if (count > 0) {
3538
3539            int newPos;
3540
3541            int selectablePos;
3542
3543            // Find the row we are supposed to sync to
3544            if (mNeedSync) {
3545                // Update this first, since setNextSelectedPositionInt inspects it
3546                mNeedSync = false;
3547
3548                if (mTranscriptMode == TRANSCRIPT_MODE_ALWAYS_SCROLL ||
3549                        (mTranscriptMode == TRANSCRIPT_MODE_NORMAL &&
3550                                mFirstPosition + getChildCount() >= mOldItemCount)) {
3551                    mLayoutMode = LAYOUT_FORCE_BOTTOM;
3552                    return;
3553                }
3554
3555                switch (mSyncMode) {
3556                case SYNC_SELECTED_POSITION:
3557                    if (isInTouchMode()) {
3558                        // We saved our state when not in touch mode. (We know this because
3559                        // mSyncMode is SYNC_SELECTED_POSITION.) Now we are trying to
3560                        // restore in touch mode. Just leave mSyncPosition as it is (possibly
3561                        // adjusting if the available range changed) and return.
3562                        mLayoutMode = LAYOUT_SYNC;
3563                        mSyncPosition = Math.min(Math.max(0, mSyncPosition), count - 1);
3564
3565                        return;
3566                    } else {
3567                        // See if we can find a position in the new data with the same
3568                        // id as the old selection. This will change mSyncPosition.
3569                        newPos = findSyncPosition();
3570                        if (newPos >= 0) {
3571                            // Found it. Now verify that new selection is still selectable
3572                            selectablePos = lookForSelectablePosition(newPos, true);
3573                            if (selectablePos == newPos) {
3574                                // Same row id is selected
3575                                mSyncPosition = newPos;
3576
3577                                if (mSyncHeight == getHeight()) {
3578                                    // If we are at the same height as when we saved state, try
3579                                    // to restore the scroll position too.
3580                                    mLayoutMode = LAYOUT_SYNC;
3581                                } else {
3582                                    // We are not the same height as when the selection was saved, so
3583                                    // don't try to restore the exact position
3584                                    mLayoutMode = LAYOUT_SET_SELECTION;
3585                                }
3586
3587                                // Restore selection
3588                                setNextSelectedPositionInt(newPos);
3589                                return;
3590                            }
3591                        }
3592                    }
3593                    break;
3594                case SYNC_FIRST_POSITION:
3595                    // Leave mSyncPosition as it is -- just pin to available range
3596                    mLayoutMode = LAYOUT_SYNC;
3597                    mSyncPosition = Math.min(Math.max(0, mSyncPosition), count - 1);
3598
3599                    return;
3600                }
3601            }
3602
3603            if (!isInTouchMode()) {
3604                // We couldn't find matching data -- try to use the same position
3605                newPos = getSelectedItemPosition();
3606
3607                // Pin position to the available range
3608                if (newPos >= count) {
3609                    newPos = count - 1;
3610                }
3611                if (newPos < 0) {
3612                    newPos = 0;
3613                }
3614
3615                // Make sure we select something selectable -- first look down
3616                selectablePos = lookForSelectablePosition(newPos, true);
3617
3618                if (selectablePos >= 0) {
3619                    setNextSelectedPositionInt(selectablePos);
3620                    return;
3621                } else {
3622                    // Looking down didn't work -- try looking up
3623                    selectablePos = lookForSelectablePosition(newPos, false);
3624                    if (selectablePos >= 0) {
3625                        setNextSelectedPositionInt(selectablePos);
3626                        return;
3627                    }
3628                }
3629            } else {
3630
3631                // We already know where we want to resurrect the selection
3632                if (mResurrectToPosition >= 0) {
3633                    return;
3634                }
3635            }
3636
3637        }
3638
3639        // Nothing is selected. Give up and reset everything.
3640        mLayoutMode = mStackFromBottom ? LAYOUT_FORCE_BOTTOM : LAYOUT_FORCE_TOP;
3641        mSelectedPosition = INVALID_POSITION;
3642        mSelectedRowId = INVALID_ROW_ID;
3643        mNextSelectedPosition = INVALID_POSITION;
3644        mNextSelectedRowId = INVALID_ROW_ID;
3645        mNeedSync = false;
3646        checkSelectionChanged();
3647    }
3648
3649    @Override
3650    protected void onDisplayHint(int hint) {
3651        super.onDisplayHint(hint);
3652        switch (hint) {
3653            case INVISIBLE:
3654                if (mPopup != null && mPopup.isShowing()) {
3655                    dismissPopup();
3656                }
3657                break;
3658            case VISIBLE:
3659                if (mFiltered && mPopup != null && !mPopup.isShowing()) {
3660                    showPopup();
3661                }
3662                break;
3663        }
3664        mPopupHidden = hint == INVISIBLE;
3665    }
3666
3667    /**
3668     * Removes the filter window
3669     */
3670    private void dismissPopup() {
3671        if (mPopup != null) {
3672            mPopup.dismiss();
3673        }
3674    }
3675
3676    /**
3677     * Shows the filter window
3678     */
3679    private void showPopup() {
3680        // Make sure we have a window before showing the popup
3681        if (getWindowVisibility() == View.VISIBLE) {
3682            createTextFilter(true);
3683            positionPopup();
3684            // Make sure we get focus if we are showing the popup
3685            checkFocus();
3686        }
3687    }
3688
3689    private void positionPopup() {
3690        int screenHeight = getResources().getDisplayMetrics().heightPixels;
3691        final int[] xy = new int[2];
3692        getLocationOnScreen(xy);
3693        // TODO: The 20 below should come from the theme
3694        // TODO: And the gravity should be defined in the theme as well
3695        final int bottomGap = screenHeight - xy[1] - getHeight() + (int) (mDensityScale * 20);
3696        if (!mPopup.isShowing()) {
3697            mPopup.showAtLocation(this, Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL,
3698                    xy[0], bottomGap);
3699        } else {
3700            mPopup.update(xy[0], bottomGap, -1, -1);
3701        }
3702    }
3703
3704    /**
3705     * What is the distance between the source and destination rectangles given the direction of
3706     * focus navigation between them? The direction basically helps figure out more quickly what is
3707     * self evident by the relationship between the rects...
3708     *
3709     * @param source the source rectangle
3710     * @param dest the destination rectangle
3711     * @param direction the direction
3712     * @return the distance between the rectangles
3713     */
3714    static int getDistance(Rect source, Rect dest, int direction) {
3715        int sX, sY; // source x, y
3716        int dX, dY; // dest x, y
3717        switch (direction) {
3718        case View.FOCUS_RIGHT:
3719            sX = source.right;
3720            sY = source.top + source.height() / 2;
3721            dX = dest.left;
3722            dY = dest.top + dest.height() / 2;
3723            break;
3724        case View.FOCUS_DOWN:
3725            sX = source.left + source.width() / 2;
3726            sY = source.bottom;
3727            dX = dest.left + dest.width() / 2;
3728            dY = dest.top;
3729            break;
3730        case View.FOCUS_LEFT:
3731            sX = source.left;
3732            sY = source.top + source.height() / 2;
3733            dX = dest.right;
3734            dY = dest.top + dest.height() / 2;
3735            break;
3736        case View.FOCUS_UP:
3737            sX = source.left + source.width() / 2;
3738            sY = source.top;
3739            dX = dest.left + dest.width() / 2;
3740            dY = dest.bottom;
3741            break;
3742        default:
3743            throw new IllegalArgumentException("direction must be one of "
3744                    + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
3745        }
3746        int deltaX = dX - sX;
3747        int deltaY = dY - sY;
3748        return deltaY * deltaY + deltaX * deltaX;
3749    }
3750
3751    @Override
3752    protected boolean isInFilterMode() {
3753        return mFiltered;
3754    }
3755
3756    /**
3757     * Sends a key to the text filter window
3758     *
3759     * @param keyCode The keycode for the event
3760     * @param event The actual key event
3761     *
3762     * @return True if the text filter handled the event, false otherwise.
3763     */
3764    boolean sendToTextFilter(int keyCode, int count, KeyEvent event) {
3765        if (!acceptFilter()) {
3766            return false;
3767        }
3768
3769        boolean handled = false;
3770        boolean okToSend = true;
3771        switch (keyCode) {
3772        case KeyEvent.KEYCODE_DPAD_UP:
3773        case KeyEvent.KEYCODE_DPAD_DOWN:
3774        case KeyEvent.KEYCODE_DPAD_LEFT:
3775        case KeyEvent.KEYCODE_DPAD_RIGHT:
3776        case KeyEvent.KEYCODE_DPAD_CENTER:
3777        case KeyEvent.KEYCODE_ENTER:
3778            okToSend = false;
3779            break;
3780        case KeyEvent.KEYCODE_BACK:
3781            if (mFiltered && mPopup != null && mPopup.isShowing()) {
3782                if (event.getAction() == KeyEvent.ACTION_DOWN
3783                        && event.getRepeatCount() == 0) {
3784                    getKeyDispatcherState().startTracking(event, this);
3785                    handled = true;
3786                } else if (event.getAction() == KeyEvent.ACTION_UP
3787                        && event.isTracking() && !event.isCanceled()) {
3788                    handled = true;
3789                    mTextFilter.setText("");
3790                }
3791            }
3792            okToSend = false;
3793            break;
3794        case KeyEvent.KEYCODE_SPACE:
3795            // Only send spaces once we are filtered
3796            okToSend = mFiltered;
3797            break;
3798        }
3799
3800        if (okToSend) {
3801            createTextFilter(true);
3802
3803            KeyEvent forwardEvent = event;
3804            if (forwardEvent.getRepeatCount() > 0) {
3805                forwardEvent = KeyEvent.changeTimeRepeat(event, event.getEventTime(), 0);
3806            }
3807
3808            int action = event.getAction();
3809            switch (action) {
3810                case KeyEvent.ACTION_DOWN:
3811                    handled = mTextFilter.onKeyDown(keyCode, forwardEvent);
3812                    break;
3813
3814                case KeyEvent.ACTION_UP:
3815                    handled = mTextFilter.onKeyUp(keyCode, forwardEvent);
3816                    break;
3817
3818                case KeyEvent.ACTION_MULTIPLE:
3819                    handled = mTextFilter.onKeyMultiple(keyCode, count, event);
3820                    break;
3821            }
3822        }
3823        return handled;
3824    }
3825
3826    /**
3827     * Return an InputConnection for editing of the filter text.
3828     */
3829    @Override
3830    public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
3831        if (isTextFilterEnabled()) {
3832            // XXX we need to have the text filter created, so we can get an
3833            // InputConnection to proxy to.  Unfortunately this means we pretty
3834            // much need to make it as soon as a list view gets focus.
3835            createTextFilter(false);
3836            if (mPublicInputConnection == null) {
3837                mDefInputConnection = new BaseInputConnection(this, false);
3838                mPublicInputConnection = new InputConnectionWrapper(
3839                        mTextFilter.onCreateInputConnection(outAttrs), true) {
3840                    @Override
3841                    public boolean reportFullscreenMode(boolean enabled) {
3842                        // Use our own input connection, since it is
3843                        // the "real" one the IME is talking with.
3844                        return mDefInputConnection.reportFullscreenMode(enabled);
3845                    }
3846
3847                    @Override
3848                    public boolean performEditorAction(int editorAction) {
3849                        // The editor is off in its own window; we need to be
3850                        // the one that does this.
3851                        if (editorAction == EditorInfo.IME_ACTION_DONE) {
3852                            InputMethodManager imm = (InputMethodManager)
3853                                    getContext().getSystemService(
3854                                            Context.INPUT_METHOD_SERVICE);
3855                            if (imm != null) {
3856                                imm.hideSoftInputFromWindow(getWindowToken(), 0);
3857                            }
3858                            return true;
3859                        }
3860                        return false;
3861                    }
3862
3863                    @Override
3864                    public boolean sendKeyEvent(KeyEvent event) {
3865                        // Use our own input connection, since the filter
3866                        // text view may not be shown in a window so has
3867                        // no ViewRoot to dispatch events with.
3868                        return mDefInputConnection.sendKeyEvent(event);
3869                    }
3870                };
3871            }
3872            outAttrs.inputType = EditorInfo.TYPE_CLASS_TEXT
3873                    | EditorInfo.TYPE_TEXT_VARIATION_FILTER;
3874            outAttrs.imeOptions = EditorInfo.IME_ACTION_DONE;
3875            return mPublicInputConnection;
3876        }
3877        return null;
3878    }
3879
3880    /**
3881     * For filtering we proxy an input connection to an internal text editor,
3882     * and this allows the proxying to happen.
3883     */
3884    @Override
3885    public boolean checkInputConnectionProxy(View view) {
3886        return view == mTextFilter;
3887    }
3888
3889    /**
3890     * Creates the window for the text filter and populates it with an EditText field;
3891     *
3892     * @param animateEntrance true if the window should appear with an animation
3893     */
3894    private void createTextFilter(boolean animateEntrance) {
3895        if (mPopup == null) {
3896            Context c = getContext();
3897            PopupWindow p = new PopupWindow(c);
3898            LayoutInflater layoutInflater = (LayoutInflater)
3899                    c.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
3900            mTextFilter = (EditText) layoutInflater.inflate(
3901                    com.android.internal.R.layout.typing_filter, null);
3902            // For some reason setting this as the "real" input type changes
3903            // the text view in some way that it doesn't work, and I don't
3904            // want to figure out why this is.
3905            mTextFilter.setRawInputType(EditorInfo.TYPE_CLASS_TEXT
3906                    | EditorInfo.TYPE_TEXT_VARIATION_FILTER);
3907            mTextFilter.setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI);
3908            mTextFilter.addTextChangedListener(this);
3909            p.setFocusable(false);
3910            p.setTouchable(false);
3911            p.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
3912            p.setContentView(mTextFilter);
3913            p.setWidth(LayoutParams.WRAP_CONTENT);
3914            p.setHeight(LayoutParams.WRAP_CONTENT);
3915            p.setBackgroundDrawable(null);
3916            mPopup = p;
3917            getViewTreeObserver().addOnGlobalLayoutListener(this);
3918            mGlobalLayoutListenerAddedFilter = true;
3919        }
3920        if (animateEntrance) {
3921            mPopup.setAnimationStyle(com.android.internal.R.style.Animation_TypingFilter);
3922        } else {
3923            mPopup.setAnimationStyle(com.android.internal.R.style.Animation_TypingFilterRestore);
3924        }
3925    }
3926
3927    /**
3928     * Clear the text filter.
3929     */
3930    public void clearTextFilter() {
3931        if (mFiltered) {
3932            mTextFilter.setText("");
3933            mFiltered = false;
3934            if (mPopup != null && mPopup.isShowing()) {
3935                dismissPopup();
3936            }
3937        }
3938    }
3939
3940    /**
3941     * Returns if the ListView currently has a text filter.
3942     */
3943    public boolean hasTextFilter() {
3944        return mFiltered;
3945    }
3946
3947    public void onGlobalLayout() {
3948        if (isShown()) {
3949            // Show the popup if we are filtered
3950            if (mFiltered && mPopup != null && !mPopup.isShowing() && !mPopupHidden) {
3951                showPopup();
3952            }
3953        } else {
3954            // Hide the popup when we are no longer visible
3955            if (mPopup != null && mPopup.isShowing()) {
3956                dismissPopup();
3957            }
3958        }
3959
3960    }
3961
3962    /**
3963     * For our text watcher that is associated with the text filter.  Does
3964     * nothing.
3965     */
3966    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
3967    }
3968
3969    /**
3970     * For our text watcher that is associated with the text filter. Performs
3971     * the actual filtering as the text changes, and takes care of hiding and
3972     * showing the popup displaying the currently entered filter text.
3973     */
3974    public void onTextChanged(CharSequence s, int start, int before, int count) {
3975        if (mPopup != null && isTextFilterEnabled()) {
3976            int length = s.length();
3977            boolean showing = mPopup.isShowing();
3978            if (!showing && length > 0) {
3979                // Show the filter popup if necessary
3980                showPopup();
3981                mFiltered = true;
3982            } else if (showing && length == 0) {
3983                // Remove the filter popup if the user has cleared all text
3984                dismissPopup();
3985                mFiltered = false;
3986            }
3987            if (mAdapter instanceof Filterable) {
3988                Filter f = ((Filterable) mAdapter).getFilter();
3989                // Filter should not be null when we reach this part
3990                if (f != null) {
3991                    f.filter(s, this);
3992                } else {
3993                    throw new IllegalStateException("You cannot call onTextChanged with a non "
3994                            + "filterable adapter");
3995                }
3996            }
3997        }
3998    }
3999
4000    /**
4001     * For our text watcher that is associated with the text filter.  Does
4002     * nothing.
4003     */
4004    public void afterTextChanged(Editable s) {
4005    }
4006
4007    public void onFilterComplete(int count) {
4008        if (mSelectedPosition < 0 && count > 0) {
4009            mResurrectToPosition = INVALID_POSITION;
4010            resurrectSelection();
4011        }
4012    }
4013
4014    @Override
4015    protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
4016        return new LayoutParams(p);
4017    }
4018
4019    @Override
4020    public LayoutParams generateLayoutParams(AttributeSet attrs) {
4021        return new AbsListView.LayoutParams(getContext(), attrs);
4022    }
4023
4024    @Override
4025    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
4026        return p instanceof AbsListView.LayoutParams;
4027    }
4028
4029    /**
4030     * Puts the list or grid into transcript mode. In this mode the list or grid will always scroll
4031     * to the bottom to show new items.
4032     *
4033     * @param mode the transcript mode to set
4034     *
4035     * @see #TRANSCRIPT_MODE_DISABLED
4036     * @see #TRANSCRIPT_MODE_NORMAL
4037     * @see #TRANSCRIPT_MODE_ALWAYS_SCROLL
4038     */
4039    public void setTranscriptMode(int mode) {
4040        mTranscriptMode = mode;
4041    }
4042
4043    /**
4044     * Returns the current transcript mode.
4045     *
4046     * @return {@link #TRANSCRIPT_MODE_DISABLED}, {@link #TRANSCRIPT_MODE_NORMAL} or
4047     *         {@link #TRANSCRIPT_MODE_ALWAYS_SCROLL}
4048     */
4049    public int getTranscriptMode() {
4050        return mTranscriptMode;
4051    }
4052
4053    @Override
4054    public int getSolidColor() {
4055        return mCacheColorHint;
4056    }
4057
4058    /**
4059     * When set to a non-zero value, the cache color hint indicates that this list is always drawn
4060     * on top of a solid, single-color, opaque background
4061     *
4062     * @param color The background color
4063     */
4064    public void setCacheColorHint(int color) {
4065        if (color != mCacheColorHint) {
4066            mCacheColorHint = color;
4067            int count = getChildCount();
4068            for (int i = 0; i < count; i++) {
4069                getChildAt(i).setDrawingCacheBackgroundColor(color);
4070            }
4071            mRecycler.setCacheColorHint(color);
4072        }
4073    }
4074
4075    /**
4076     * When set to a non-zero value, the cache color hint indicates that this list is always drawn
4077     * on top of a solid, single-color, opaque background
4078     *
4079     * @return The cache color hint
4080     */
4081    public int getCacheColorHint() {
4082        return mCacheColorHint;
4083    }
4084
4085    /**
4086     * Move all views (excluding headers and footers) held by this AbsListView into the supplied
4087     * List. This includes views displayed on the screen as well as views stored in AbsListView's
4088     * internal view recycler.
4089     *
4090     * @param views A list into which to put the reclaimed views
4091     */
4092    public void reclaimViews(List<View> views) {
4093        int childCount = getChildCount();
4094        RecyclerListener listener = mRecycler.mRecyclerListener;
4095
4096        // Reclaim views on screen
4097        for (int i = 0; i < childCount; i++) {
4098            View child = getChildAt(i);
4099            AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams();
4100            // Don't reclaim header or footer views, or views that should be ignored
4101            if (lp != null && mRecycler.shouldRecycleViewType(lp.viewType)) {
4102                views.add(child);
4103                if (listener != null) {
4104                    // Pretend they went through the scrap heap
4105                    listener.onMovedToScrapHeap(child);
4106                }
4107            }
4108        }
4109        mRecycler.reclaimScrapViews(views);
4110        removeAllViewsInLayout();
4111    }
4112
4113    /**
4114     * @hide
4115     */
4116    @Override
4117    protected boolean onConsistencyCheck(int consistency) {
4118        boolean result = super.onConsistencyCheck(consistency);
4119
4120        final boolean checkLayout = (consistency & ViewDebug.CONSISTENCY_LAYOUT) != 0;
4121
4122        if (checkLayout) {
4123            // The active recycler must be empty
4124            final View[] activeViews = mRecycler.mActiveViews;
4125            int count = activeViews.length;
4126            for (int i = 0; i < count; i++) {
4127                if (activeViews[i] != null) {
4128                    result = false;
4129                    Log.d(ViewDebug.CONSISTENCY_LOG_TAG,
4130                            "AbsListView " + this + " has a view in its active recycler: " +
4131                                    activeViews[i]);
4132                }
4133            }
4134
4135            // All views in the recycler must NOT be on screen and must NOT have a parent
4136            final ArrayList<View> scrap = mRecycler.mCurrentScrap;
4137            if (!checkScrap(scrap)) result = false;
4138            final ArrayList<View>[] scraps = mRecycler.mScrapViews;
4139            count = scraps.length;
4140            for (int i = 0; i < count; i++) {
4141                if (!checkScrap(scraps[i])) result = false;
4142            }
4143        }
4144
4145        return result;
4146    }
4147
4148    private boolean checkScrap(ArrayList<View> scrap) {
4149        if (scrap == null) return true;
4150        boolean result = true;
4151
4152        final int count = scrap.size();
4153        for (int i = 0; i < count; i++) {
4154            final View view = scrap.get(i);
4155            if (view.getParent() != null) {
4156                result = false;
4157                Log.d(ViewDebug.CONSISTENCY_LOG_TAG, "AbsListView " + this +
4158                        " has a view in its scrap heap still attached to a parent: " + view);
4159            }
4160            if (indexOfChild(view) >= 0) {
4161                result = false;
4162                Log.d(ViewDebug.CONSISTENCY_LOG_TAG, "AbsListView " + this +
4163                        " has a view in its scrap heap that is also a direct child: " + view);
4164            }
4165        }
4166
4167        return result;
4168    }
4169
4170    /**
4171     * Sets the recycler listener to be notified whenever a View is set aside in
4172     * the recycler for later reuse. This listener can be used to free resources
4173     * associated to the View.
4174     *
4175     * @param listener The recycler listener to be notified of views set aside
4176     *        in the recycler.
4177     *
4178     * @see android.widget.AbsListView.RecycleBin
4179     * @see android.widget.AbsListView.RecyclerListener
4180     */
4181    public void setRecyclerListener(RecyclerListener listener) {
4182        mRecycler.mRecyclerListener = listener;
4183    }
4184
4185    /**
4186     * AbsListView extends LayoutParams to provide a place to hold the view type.
4187     */
4188    public static class LayoutParams extends ViewGroup.LayoutParams {
4189        /**
4190         * View type for this view, as returned by
4191         * {@link android.widget.Adapter#getItemViewType(int) }
4192         */
4193        @ViewDebug.ExportedProperty(category = "list", mapping = {
4194            @ViewDebug.IntToString(from = ITEM_VIEW_TYPE_IGNORE, to = "ITEM_VIEW_TYPE_IGNORE"),
4195            @ViewDebug.IntToString(from = ITEM_VIEW_TYPE_HEADER_OR_FOOTER, to = "ITEM_VIEW_TYPE_HEADER_OR_FOOTER")
4196        })
4197        int viewType;
4198
4199        /**
4200         * When this boolean is set, the view has been added to the AbsListView
4201         * at least once. It is used to know whether headers/footers have already
4202         * been added to the list view and whether they should be treated as
4203         * recycled views or not.
4204         */
4205        @ViewDebug.ExportedProperty(category = "list")
4206        boolean recycledHeaderFooter;
4207
4208        /**
4209         * When an AbsListView is measured with an AT_MOST measure spec, it needs
4210         * to obtain children views to measure itself. When doing so, the children
4211         * are not attached to the window, but put in the recycler which assumes
4212         * they've been attached before. Setting this flag will force the reused
4213         * view to be attached to the window rather than just attached to the
4214         * parent.
4215         */
4216        @ViewDebug.ExportedProperty(category = "list")
4217        boolean forceAdd;
4218
4219        public LayoutParams(Context c, AttributeSet attrs) {
4220            super(c, attrs);
4221        }
4222
4223        public LayoutParams(int w, int h) {
4224            super(w, h);
4225        }
4226
4227        public LayoutParams(int w, int h, int viewType) {
4228            super(w, h);
4229            this.viewType = viewType;
4230        }
4231
4232        public LayoutParams(ViewGroup.LayoutParams source) {
4233            super(source);
4234        }
4235    }
4236
4237    /**
4238     * A RecyclerListener is used to receive a notification whenever a View is placed
4239     * inside the RecycleBin's scrap heap. This listener is used to free resources
4240     * associated to Views placed in the RecycleBin.
4241     *
4242     * @see android.widget.AbsListView.RecycleBin
4243     * @see android.widget.AbsListView#setRecyclerListener(android.widget.AbsListView.RecyclerListener)
4244     */
4245    public static interface RecyclerListener {
4246        /**
4247         * Indicates that the specified View was moved into the recycler's scrap heap.
4248         * The view is not displayed on screen any more and any expensive resource
4249         * associated with the view should be discarded.
4250         *
4251         * @param view
4252         */
4253        void onMovedToScrapHeap(View view);
4254    }
4255
4256    /**
4257     * The RecycleBin facilitates reuse of views across layouts. The RecycleBin has two levels of
4258     * storage: ActiveViews and ScrapViews. ActiveViews are those views which were onscreen at the
4259     * start of a layout. By construction, they are displaying current information. At the end of
4260     * layout, all views in ActiveViews are demoted to ScrapViews. ScrapViews are old views that
4261     * could potentially be used by the adapter to avoid allocating views unnecessarily.
4262     *
4263     * @see android.widget.AbsListView#setRecyclerListener(android.widget.AbsListView.RecyclerListener)
4264     * @see android.widget.AbsListView.RecyclerListener
4265     */
4266    class RecycleBin {
4267        private RecyclerListener mRecyclerListener;
4268
4269        /**
4270         * The position of the first view stored in mActiveViews.
4271         */
4272        private int mFirstActivePosition;
4273
4274        /**
4275         * Views that were on screen at the start of layout. This array is populated at the start of
4276         * layout, and at the end of layout all view in mActiveViews are moved to mScrapViews.
4277         * Views in mActiveViews represent a contiguous range of Views, with position of the first
4278         * view store in mFirstActivePosition.
4279         */
4280        private View[] mActiveViews = new View[0];
4281
4282        /**
4283         * Unsorted views that can be used by the adapter as a convert view.
4284         */
4285        private ArrayList<View>[] mScrapViews;
4286
4287        private int mViewTypeCount;
4288
4289        private ArrayList<View> mCurrentScrap;
4290
4291        public void setViewTypeCount(int viewTypeCount) {
4292            if (viewTypeCount < 1) {
4293                throw new IllegalArgumentException("Can't have a viewTypeCount < 1");
4294            }
4295            //noinspection unchecked
4296            ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount];
4297            for (int i = 0; i < viewTypeCount; i++) {
4298                scrapViews[i] = new ArrayList<View>();
4299            }
4300            mViewTypeCount = viewTypeCount;
4301            mCurrentScrap = scrapViews[0];
4302            mScrapViews = scrapViews;
4303        }
4304
4305        public void markChildrenDirty() {
4306            if (mViewTypeCount == 1) {
4307                final ArrayList<View> scrap = mCurrentScrap;
4308                final int scrapCount = scrap.size();
4309                for (int i = 0; i < scrapCount; i++) {
4310                    scrap.get(i).forceLayout();
4311                }
4312            } else {
4313                final int typeCount = mViewTypeCount;
4314                for (int i = 0; i < typeCount; i++) {
4315                    final ArrayList<View> scrap = mScrapViews[i];
4316                    final int scrapCount = scrap.size();
4317                    for (int j = 0; j < scrapCount; j++) {
4318                        scrap.get(j).forceLayout();
4319                    }
4320                }
4321            }
4322        }
4323
4324        public boolean shouldRecycleViewType(int viewType) {
4325            return viewType >= 0;
4326        }
4327
4328        /**
4329         * Clears the scrap heap.
4330         */
4331        void clear() {
4332            if (mViewTypeCount == 1) {
4333                final ArrayList<View> scrap = mCurrentScrap;
4334                final int scrapCount = scrap.size();
4335                for (int i = 0; i < scrapCount; i++) {
4336                    removeDetachedView(scrap.remove(scrapCount - 1 - i), false);
4337                }
4338            } else {
4339                final int typeCount = mViewTypeCount;
4340                for (int i = 0; i < typeCount; i++) {
4341                    final ArrayList<View> scrap = mScrapViews[i];
4342                    final int scrapCount = scrap.size();
4343                    for (int j = 0; j < scrapCount; j++) {
4344                        removeDetachedView(scrap.remove(scrapCount - 1 - j), false);
4345                    }
4346                }
4347            }
4348        }
4349
4350        /**
4351         * Fill ActiveViews with all of the children of the AbsListView.
4352         *
4353         * @param childCount The minimum number of views mActiveViews should hold
4354         * @param firstActivePosition The position of the first view that will be stored in
4355         *        mActiveViews
4356         */
4357        void fillActiveViews(int childCount, int firstActivePosition) {
4358            if (mActiveViews.length < childCount) {
4359                mActiveViews = new View[childCount];
4360            }
4361            mFirstActivePosition = firstActivePosition;
4362
4363            final View[] activeViews = mActiveViews;
4364            for (int i = 0; i < childCount; i++) {
4365                View child = getChildAt(i);
4366                AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams();
4367                // Don't put header or footer views into the scrap heap
4368                if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
4369                    // Note:  We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in active views.
4370                    //        However, we will NOT place them into scrap views.
4371                    activeViews[i] = child;
4372                }
4373            }
4374        }
4375
4376        /**
4377         * Get the view corresponding to the specified position. The view will be removed from
4378         * mActiveViews if it is found.
4379         *
4380         * @param position The position to look up in mActiveViews
4381         * @return The view if it is found, null otherwise
4382         */
4383        View getActiveView(int position) {
4384            int index = position - mFirstActivePosition;
4385            final View[] activeViews = mActiveViews;
4386            if (index >=0 && index < activeViews.length) {
4387                final View match = activeViews[index];
4388                activeViews[index] = null;
4389                return match;
4390            }
4391            return null;
4392        }
4393
4394        /**
4395         * @return A view from the ScrapViews collection. These are unordered.
4396         */
4397        View getScrapView(int position) {
4398            ArrayList<View> scrapViews;
4399            if (mViewTypeCount == 1) {
4400                scrapViews = mCurrentScrap;
4401                int size = scrapViews.size();
4402                if (size > 0) {
4403                    return scrapViews.remove(size - 1);
4404                } else {
4405                    return null;
4406                }
4407            } else {
4408                int whichScrap = mAdapter.getItemViewType(position);
4409                if (whichScrap >= 0 && whichScrap < mScrapViews.length) {
4410                    scrapViews = mScrapViews[whichScrap];
4411                    int size = scrapViews.size();
4412                    if (size > 0) {
4413                        return scrapViews.remove(size - 1);
4414                    }
4415                }
4416            }
4417            return null;
4418        }
4419
4420        /**
4421         * Put a view into the ScapViews list. These views are unordered.
4422         *
4423         * @param scrap The view to add
4424         */
4425        void addScrapView(View scrap) {
4426            AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
4427            if (lp == null) {
4428                return;
4429            }
4430
4431            // Don't put header or footer views or views that should be ignored
4432            // into the scrap heap
4433            int viewType = lp.viewType;
4434            if (!shouldRecycleViewType(viewType)) {
4435                if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
4436                    removeDetachedView(scrap, false);
4437                }
4438                return;
4439            }
4440
4441            if (mViewTypeCount == 1) {
4442                scrap.dispatchStartTemporaryDetach();
4443                mCurrentScrap.add(scrap);
4444            } else {
4445                scrap.dispatchStartTemporaryDetach();
4446                mScrapViews[viewType].add(scrap);
4447            }
4448
4449            if (mRecyclerListener != null) {
4450                mRecyclerListener.onMovedToScrapHeap(scrap);
4451            }
4452        }
4453
4454        /**
4455         * Move all views remaining in mActiveViews to mScrapViews.
4456         */
4457        void scrapActiveViews() {
4458            final View[] activeViews = mActiveViews;
4459            final boolean hasListener = mRecyclerListener != null;
4460            final boolean multipleScraps = mViewTypeCount > 1;
4461
4462            ArrayList<View> scrapViews = mCurrentScrap;
4463            final int count = activeViews.length;
4464            for (int i = count - 1; i >= 0; i--) {
4465                final View victim = activeViews[i];
4466                if (victim != null) {
4467                    int whichScrap = ((AbsListView.LayoutParams) victim.getLayoutParams()).viewType;
4468
4469                    activeViews[i] = null;
4470
4471                    if (!shouldRecycleViewType(whichScrap)) {
4472                        // Do not move views that should be ignored
4473                        if (whichScrap != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
4474                            removeDetachedView(victim, false);
4475                        }
4476                        continue;
4477                    }
4478
4479                    if (multipleScraps) {
4480                        scrapViews = mScrapViews[whichScrap];
4481                    }
4482                    victim.dispatchStartTemporaryDetach();
4483                    scrapViews.add(victim);
4484
4485                    if (hasListener) {
4486                        mRecyclerListener.onMovedToScrapHeap(victim);
4487                    }
4488
4489                    if (ViewDebug.TRACE_RECYCLER) {
4490                        ViewDebug.trace(victim,
4491                                ViewDebug.RecyclerTraceType.MOVE_FROM_ACTIVE_TO_SCRAP_HEAP,
4492                                mFirstActivePosition + i, -1);
4493                    }
4494                }
4495            }
4496
4497            pruneScrapViews();
4498        }
4499
4500        /**
4501         * Makes sure that the size of mScrapViews does not exceed the size of mActiveViews.
4502         * (This can happen if an adapter does not recycle its views).
4503         */
4504        private void pruneScrapViews() {
4505            final int maxViews = mActiveViews.length;
4506            final int viewTypeCount = mViewTypeCount;
4507            final ArrayList<View>[] scrapViews = mScrapViews;
4508            for (int i = 0; i < viewTypeCount; ++i) {
4509                final ArrayList<View> scrapPile = scrapViews[i];
4510                int size = scrapPile.size();
4511                final int extras = size - maxViews;
4512                size--;
4513                for (int j = 0; j < extras; j++) {
4514                    removeDetachedView(scrapPile.remove(size--), false);
4515                }
4516            }
4517        }
4518
4519        /**
4520         * Puts all views in the scrap heap into the supplied list.
4521         */
4522        void reclaimScrapViews(List<View> views) {
4523            if (mViewTypeCount == 1) {
4524                views.addAll(mCurrentScrap);
4525            } else {
4526                final int viewTypeCount = mViewTypeCount;
4527                final ArrayList<View>[] scrapViews = mScrapViews;
4528                for (int i = 0; i < viewTypeCount; ++i) {
4529                    final ArrayList<View> scrapPile = scrapViews[i];
4530                    views.addAll(scrapPile);
4531                }
4532            }
4533        }
4534
4535        /**
4536         * Updates the cache color hint of all known views.
4537         *
4538         * @param color The new cache color hint.
4539         */
4540        void setCacheColorHint(int color) {
4541            if (mViewTypeCount == 1) {
4542                final ArrayList<View> scrap = mCurrentScrap;
4543                final int scrapCount = scrap.size();
4544                for (int i = 0; i < scrapCount; i++) {
4545                    scrap.get(i).setDrawingCacheBackgroundColor(color);
4546                }
4547            } else {
4548                final int typeCount = mViewTypeCount;
4549                for (int i = 0; i < typeCount; i++) {
4550                    final ArrayList<View> scrap = mScrapViews[i];
4551                    final int scrapCount = scrap.size();
4552                    for (int j = 0; j < scrapCount; j++) {
4553                        scrap.get(i).setDrawingCacheBackgroundColor(color);
4554                    }
4555                }
4556            }
4557            // Just in case this is called during a layout pass
4558            final View[] activeViews = mActiveViews;
4559            final int count = activeViews.length;
4560            for (int i = 0; i < count; ++i) {
4561                final View victim = activeViews[i];
4562                if (victim != null) {
4563                    victim.setDrawingCacheBackgroundColor(color);
4564                }
4565            }
4566        }
4567    }
4568}
4569