ListView.java revision 70c9ffbc838271f0ea27a4780eb146287de53ef6
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;
20import com.google.android.collect.Lists;
21
22import android.content.Context;
23import android.content.res.TypedArray;
24import android.graphics.Canvas;
25import android.graphics.Paint;
26import android.graphics.PixelFormat;
27import android.graphics.Rect;
28import android.graphics.drawable.ColorDrawable;
29import android.graphics.drawable.Drawable;
30import android.os.Parcel;
31import android.os.Parcelable;
32import android.util.AttributeSet;
33import android.util.LongSparseArray;
34import android.util.SparseBooleanArray;
35import android.view.FocusFinder;
36import android.view.KeyEvent;
37import android.view.MotionEvent;
38import android.view.SoundEffectConstants;
39import android.view.View;
40import android.view.ViewDebug;
41import android.view.ViewGroup;
42import android.view.ViewParent;
43import android.view.accessibility.AccessibilityEvent;
44
45import java.util.ArrayList;
46
47/*
48 * Implementation Notes:
49 *
50 * Some terminology:
51 *
52 *     index    - index of the items that are currently visible
53 *     position - index of the items in the cursor
54 */
55
56
57/**
58 * A view that shows items in a vertically scrolling list. The items
59 * come from the {@link ListAdapter} associated with this view.
60 *
61 * <p>See the <a href="{@docRoot}resources/tutorials/views/hello-listview.html">List View
62 * tutorial</a>.</p>
63 *
64 * @attr ref android.R.styleable#ListView_entries
65 * @attr ref android.R.styleable#ListView_divider
66 * @attr ref android.R.styleable#ListView_dividerHeight
67 * @attr ref android.R.styleable#ListView_choiceMode
68 * @attr ref android.R.styleable#ListView_headerDividersEnabled
69 * @attr ref android.R.styleable#ListView_footerDividersEnabled
70 */
71public class ListView extends AbsListView {
72    /**
73     * Used to indicate a no preference for a position type.
74     */
75    static final int NO_POSITION = -1;
76
77    /**
78     * Normal list that does not indicate choices
79     */
80    public static final int CHOICE_MODE_NONE = 0;
81
82    /**
83     * The list allows up to one choice
84     */
85    public static final int CHOICE_MODE_SINGLE = 1;
86
87    /**
88     * The list allows multiple choices
89     */
90    public static final int CHOICE_MODE_MULTIPLE = 2;
91
92    /**
93     * When arrow scrolling, ListView will never scroll more than this factor
94     * times the height of the list.
95     */
96    private static final float MAX_SCROLL_FACTOR = 0.33f;
97
98    /**
99     * When arrow scrolling, need a certain amount of pixels to preview next
100     * items.  This is usually the fading edge, but if that is small enough,
101     * we want to make sure we preview at least this many pixels.
102     */
103    private static final int MIN_SCROLL_PREVIEW_PIXELS = 2;
104
105    /**
106     * A class that represents a fixed view in a list, for example a header at the top
107     * or a footer at the bottom.
108     */
109    public class FixedViewInfo {
110        /** The view to add to the list */
111        public View view;
112        /** The data backing the view. This is returned from {@link ListAdapter#getItem(int)}. */
113        public Object data;
114        /** <code>true</code> if the fixed view should be selectable in the list */
115        public boolean isSelectable;
116    }
117
118    private ArrayList<FixedViewInfo> mHeaderViewInfos = Lists.newArrayList();
119    private ArrayList<FixedViewInfo> mFooterViewInfos = Lists.newArrayList();
120
121    Drawable mDivider;
122    int mDividerHeight;
123
124    private boolean mIsCacheColorOpaque;
125    private boolean mDividerIsOpaque;
126    private boolean mClipDivider;
127
128    private boolean mHeaderDividersEnabled;
129    private boolean mFooterDividersEnabled;
130
131    private boolean mAreAllItemsSelectable = true;
132
133    private boolean mItemsCanFocus = false;
134
135    private int mChoiceMode = CHOICE_MODE_NONE;
136
137    private SparseBooleanArray mCheckStates;
138    private LongSparseArray<Boolean> mCheckedIdStates;
139
140    // used for temporary calculations.
141    private final Rect mTempRect = new Rect();
142    private Paint mDividerPaint;
143
144    // the single allocated result per list view; kinda cheesey but avoids
145    // allocating these thingies too often.
146    private final ArrowScrollFocusResult mArrowScrollFocusResult = new ArrowScrollFocusResult();
147
148    // Keeps focused children visible through resizes
149    private FocusSelector mFocusSelector;
150
151    public ListView(Context context) {
152        this(context, null);
153    }
154
155    public ListView(Context context, AttributeSet attrs) {
156        this(context, attrs, com.android.internal.R.attr.listViewStyle);
157    }
158
159    public ListView(Context context, AttributeSet attrs, int defStyle) {
160        super(context, attrs, defStyle);
161
162        TypedArray a = context.obtainStyledAttributes(attrs,
163                com.android.internal.R.styleable.ListView, defStyle, 0);
164
165        CharSequence[] entries = a.getTextArray(
166                com.android.internal.R.styleable.ListView_entries);
167        if (entries != null) {
168            setAdapter(new ArrayAdapter<CharSequence>(context,
169                    com.android.internal.R.layout.simple_list_item_1, entries));
170        }
171
172        final Drawable d = a.getDrawable(com.android.internal.R.styleable.ListView_divider);
173        if (d != null) {
174            // If a divider is specified use its intrinsic height for divider height
175            setDivider(d);
176        }
177
178        // Use the height specified, zero being the default
179        final int dividerHeight = a.getDimensionPixelSize(
180                com.android.internal.R.styleable.ListView_dividerHeight, 0);
181        if (dividerHeight != 0) {
182            setDividerHeight(dividerHeight);
183        }
184
185        setChoiceMode(a.getInt(R.styleable.ListView_choiceMode, CHOICE_MODE_NONE));
186
187        mHeaderDividersEnabled = a.getBoolean(R.styleable.ListView_headerDividersEnabled, true);
188        mFooterDividersEnabled = a.getBoolean(R.styleable.ListView_footerDividersEnabled, true);
189
190        a.recycle();
191    }
192
193    /**
194     * @return The maximum amount a list view will scroll in response to
195     *   an arrow event.
196     */
197    public int getMaxScrollAmount() {
198        return (int) (MAX_SCROLL_FACTOR * (mBottom - mTop));
199    }
200
201    /**
202     * Make sure views are touching the top or bottom edge, as appropriate for
203     * our gravity
204     */
205    private void adjustViewsUpOrDown() {
206        final int childCount = getChildCount();
207        int delta;
208
209        if (childCount > 0) {
210            View child;
211
212            if (!mStackFromBottom) {
213                // Uh-oh -- we came up short. Slide all views up to make them
214                // align with the top
215                child = getChildAt(0);
216                delta = child.getTop() - mListPadding.top;
217                if (mFirstPosition != 0) {
218                    // It's OK to have some space above the first item if it is
219                    // part of the vertical spacing
220                    delta -= mDividerHeight;
221                }
222                if (delta < 0) {
223                    // We only are looking to see if we are too low, not too high
224                    delta = 0;
225                }
226            } else {
227                // we are too high, slide all views down to align with bottom
228                child = getChildAt(childCount - 1);
229                delta = child.getBottom() - (getHeight() - mListPadding.bottom);
230
231                if (mFirstPosition + childCount < mItemCount) {
232                    // It's OK to have some space below the last item if it is
233                    // part of the vertical spacing
234                    delta += mDividerHeight;
235                }
236
237                if (delta > 0) {
238                    delta = 0;
239                }
240            }
241
242            if (delta != 0) {
243                offsetChildrenTopAndBottom(-delta);
244            }
245        }
246    }
247
248    /**
249     * Add a fixed view to appear at the top of the list. If addHeaderView is
250     * called more than once, the views will appear in the order they were
251     * added. Views added using this call can take focus if they want.
252     * <p>
253     * NOTE: Call this before calling setAdapter. This is so ListView can wrap
254     * the supplied cursor with one that will also account for header and footer
255     * views.
256     *
257     * @param v The view to add.
258     * @param data Data to associate with this view
259     * @param isSelectable whether the item is selectable
260     */
261    public void addHeaderView(View v, Object data, boolean isSelectable) {
262
263        if (mAdapter != null) {
264            throw new IllegalStateException(
265                    "Cannot add header view to list -- setAdapter has already been called.");
266        }
267
268        FixedViewInfo info = new FixedViewInfo();
269        info.view = v;
270        info.data = data;
271        info.isSelectable = isSelectable;
272        mHeaderViewInfos.add(info);
273    }
274
275    /**
276     * Add a fixed view to appear at the top of the list. If addHeaderView is
277     * called more than once, the views will appear in the order they were
278     * added. Views added using this call can take focus if they want.
279     * <p>
280     * NOTE: Call this before calling setAdapter. This is so ListView can wrap
281     * the supplied cursor with one that will also account for header and footer
282     * views.
283     *
284     * @param v The view to add.
285     */
286    public void addHeaderView(View v) {
287        addHeaderView(v, null, true);
288    }
289
290    @Override
291    public int getHeaderViewsCount() {
292        return mHeaderViewInfos.size();
293    }
294
295    /**
296     * Removes a previously-added header view.
297     *
298     * @param v The view to remove
299     * @return true if the view was removed, false if the view was not a header
300     *         view
301     */
302    public boolean removeHeaderView(View v) {
303        if (mHeaderViewInfos.size() > 0) {
304            boolean result = false;
305            if (((HeaderViewListAdapter) mAdapter).removeHeader(v)) {
306                mDataSetObserver.onChanged();
307                result = true;
308            }
309            removeFixedViewInfo(v, mHeaderViewInfos);
310            return result;
311        }
312        return false;
313    }
314
315    private void removeFixedViewInfo(View v, ArrayList<FixedViewInfo> where) {
316        int len = where.size();
317        for (int i = 0; i < len; ++i) {
318            FixedViewInfo info = where.get(i);
319            if (info.view == v) {
320                where.remove(i);
321                break;
322            }
323        }
324    }
325
326    /**
327     * Add a fixed view to appear at the bottom of the list. If addFooterView is
328     * called more than once, the views will appear in the order they were
329     * added. Views added using this call can take focus if they want.
330     * <p>
331     * NOTE: Call this before calling setAdapter. This is so ListView can wrap
332     * the supplied cursor with one that will also account for header and footer
333     * views.
334     *
335     * @param v The view to add.
336     * @param data Data to associate with this view
337     * @param isSelectable true if the footer view can be selected
338     */
339    public void addFooterView(View v, Object data, boolean isSelectable) {
340        FixedViewInfo info = new FixedViewInfo();
341        info.view = v;
342        info.data = data;
343        info.isSelectable = isSelectable;
344        mFooterViewInfos.add(info);
345
346        // in the case of re-adding a footer view, or adding one later on,
347        // we need to notify the observer
348        if (mDataSetObserver != null) {
349            mDataSetObserver.onChanged();
350        }
351    }
352
353    /**
354     * Add a fixed view to appear at the bottom of the list. If addFooterView is called more
355     * than once, the views will appear in the order they were added. Views added using
356     * this call can take focus if they want.
357     * <p>NOTE: Call this before calling setAdapter. This is so ListView can wrap the supplied
358     * cursor with one that will also account for header and footer views.
359     *
360     *
361     * @param v The view to add.
362     */
363    public void addFooterView(View v) {
364        addFooterView(v, null, true);
365    }
366
367    @Override
368    public int getFooterViewsCount() {
369        return mFooterViewInfos.size();
370    }
371
372    /**
373     * Removes a previously-added footer view.
374     *
375     * @param v The view to remove
376     * @return
377     * true if the view was removed, false if the view was not a footer view
378     */
379    public boolean removeFooterView(View v) {
380        if (mFooterViewInfos.size() > 0) {
381            boolean result = false;
382            if (((HeaderViewListAdapter) mAdapter).removeFooter(v)) {
383                mDataSetObserver.onChanged();
384                result = true;
385            }
386            removeFixedViewInfo(v, mFooterViewInfos);
387            return result;
388        }
389        return false;
390    }
391
392    /**
393     * Returns the adapter currently in use in this ListView. The returned adapter
394     * might not be the same adapter passed to {@link #setAdapter(ListAdapter)} but
395     * might be a {@link WrapperListAdapter}.
396     *
397     * @return The adapter currently used to display data in this ListView.
398     *
399     * @see #setAdapter(ListAdapter)
400     */
401    @Override
402    public ListAdapter getAdapter() {
403        return mAdapter;
404    }
405
406    /**
407     * Sets the data behind this ListView.
408     *
409     * The adapter passed to this method may be wrapped by a {@link WrapperListAdapter},
410     * depending on the ListView features currently in use. For instance, adding
411     * headers and/or footers will cause the adapter to be wrapped.
412     *
413     * @param adapter The ListAdapter which is responsible for maintaining the
414     *        data backing this list and for producing a view to represent an
415     *        item in that data set.
416     *
417     * @see #getAdapter()
418     */
419    @Override
420    public void setAdapter(ListAdapter adapter) {
421        if (null != mAdapter) {
422            mAdapter.unregisterDataSetObserver(mDataSetObserver);
423        }
424
425        resetList();
426        mRecycler.clear();
427
428        if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) {
429            mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, adapter);
430        } else {
431            mAdapter = adapter;
432        }
433
434        mOldSelectedPosition = INVALID_POSITION;
435        mOldSelectedRowId = INVALID_ROW_ID;
436        if (mAdapter != null) {
437            mAreAllItemsSelectable = mAdapter.areAllItemsEnabled();
438            mOldItemCount = mItemCount;
439            mItemCount = mAdapter.getCount();
440            checkFocus();
441
442            mDataSetObserver = new AdapterDataSetObserver();
443            mAdapter.registerDataSetObserver(mDataSetObserver);
444
445            mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());
446
447            int position;
448            if (mStackFromBottom) {
449                position = lookForSelectablePosition(mItemCount - 1, false);
450            } else {
451                position = lookForSelectablePosition(0, true);
452            }
453            setSelectedPositionInt(position);
454            setNextSelectedPositionInt(position);
455
456            if (mItemCount == 0) {
457                // Nothing selected
458                checkSelectionChanged();
459            }
460
461            if (mChoiceMode != CHOICE_MODE_NONE &&
462                    mAdapter.hasStableIds() &&
463                    mCheckedIdStates == null) {
464                mCheckedIdStates = new LongSparseArray<Boolean>();
465            }
466
467        } else {
468            mAreAllItemsSelectable = true;
469            checkFocus();
470            // Nothing selected
471            checkSelectionChanged();
472        }
473
474        if (mCheckStates != null) {
475            mCheckStates.clear();
476        }
477
478        if (mCheckedIdStates != null) {
479            mCheckedIdStates.clear();
480        }
481
482        requestLayout();
483    }
484
485
486    /**
487     * The list is empty. Clear everything out.
488     */
489    @Override
490    void resetList() {
491        // The parent's resetList() will remove all views from the layout so we need to
492        // cleanup the state of our footers and headers
493        clearRecycledState(mHeaderViewInfos);
494        clearRecycledState(mFooterViewInfos);
495
496        super.resetList();
497
498        mLayoutMode = LAYOUT_NORMAL;
499    }
500
501    private void clearRecycledState(ArrayList<FixedViewInfo> infos) {
502        if (infos != null) {
503            final int count = infos.size();
504
505            for (int i = 0; i < count; i++) {
506                final View child = infos.get(i).view;
507                final LayoutParams p = (LayoutParams) child.getLayoutParams();
508                if (p != null) {
509                    p.recycledHeaderFooter = false;
510                }
511            }
512        }
513    }
514
515    /**
516     * @return Whether the list needs to show the top fading edge
517     */
518    private boolean showingTopFadingEdge() {
519        final int listTop = mScrollY + mListPadding.top;
520        return (mFirstPosition > 0) || (getChildAt(0).getTop() > listTop);
521    }
522
523    /**
524     * @return Whether the list needs to show the bottom fading edge
525     */
526    private boolean showingBottomFadingEdge() {
527        final int childCount = getChildCount();
528        final int bottomOfBottomChild = getChildAt(childCount - 1).getBottom();
529        final int lastVisiblePosition = mFirstPosition + childCount - 1;
530
531        final int listBottom = mScrollY + getHeight() - mListPadding.bottom;
532
533        return (lastVisiblePosition < mItemCount - 1)
534                         || (bottomOfBottomChild < listBottom);
535    }
536
537
538    @Override
539    public boolean requestChildRectangleOnScreen(View child, Rect rect, boolean immediate) {
540
541        int rectTopWithinChild = rect.top;
542
543        // offset so rect is in coordinates of the this view
544        rect.offset(child.getLeft(), child.getTop());
545        rect.offset(-child.getScrollX(), -child.getScrollY());
546
547        final int height = getHeight();
548        int listUnfadedTop = getScrollY();
549        int listUnfadedBottom = listUnfadedTop + height;
550        final int fadingEdge = getVerticalFadingEdgeLength();
551
552        if (showingTopFadingEdge()) {
553            // leave room for top fading edge as long as rect isn't at very top
554            if ((mSelectedPosition > 0) || (rectTopWithinChild > fadingEdge)) {
555                listUnfadedTop += fadingEdge;
556            }
557        }
558
559        int childCount = getChildCount();
560        int bottomOfBottomChild = getChildAt(childCount - 1).getBottom();
561
562        if (showingBottomFadingEdge()) {
563            // leave room for bottom fading edge as long as rect isn't at very bottom
564            if ((mSelectedPosition < mItemCount - 1)
565                    || (rect.bottom < (bottomOfBottomChild - fadingEdge))) {
566                listUnfadedBottom -= fadingEdge;
567            }
568        }
569
570        int scrollYDelta = 0;
571
572        if (rect.bottom > listUnfadedBottom && rect.top > listUnfadedTop) {
573            // need to MOVE DOWN to get it in view: move down just enough so
574            // that the entire rectangle is in view (or at least the first
575            // screen size chunk).
576
577            if (rect.height() > height) {
578                // just enough to get screen size chunk on
579                scrollYDelta += (rect.top - listUnfadedTop);
580            } else {
581                // get entire rect at bottom of screen
582                scrollYDelta += (rect.bottom - listUnfadedBottom);
583            }
584
585            // make sure we aren't scrolling beyond the end of our children
586            int distanceToBottom = bottomOfBottomChild - listUnfadedBottom;
587            scrollYDelta = Math.min(scrollYDelta, distanceToBottom);
588        } else if (rect.top < listUnfadedTop && rect.bottom < listUnfadedBottom) {
589            // need to MOVE UP to get it in view: move up just enough so that
590            // entire rectangle is in view (or at least the first screen
591            // size chunk of it).
592
593            if (rect.height() > height) {
594                // screen size chunk
595                scrollYDelta -= (listUnfadedBottom - rect.bottom);
596            } else {
597                // entire rect at top
598                scrollYDelta -= (listUnfadedTop - rect.top);
599            }
600
601            // make sure we aren't scrolling any further than the top our children
602            int top = getChildAt(0).getTop();
603            int deltaToTop = top - listUnfadedTop;
604            scrollYDelta = Math.max(scrollYDelta, deltaToTop);
605        }
606
607        final boolean scroll = scrollYDelta != 0;
608        if (scroll) {
609            scrollListItemsBy(-scrollYDelta);
610            positionSelector(child);
611            mSelectedTop = child.getTop();
612            invalidate();
613        }
614        return scroll;
615    }
616
617    /**
618     * {@inheritDoc}
619     */
620    @Override
621    void fillGap(boolean down) {
622        final int count = getChildCount();
623        if (down) {
624            final int startOffset = count > 0 ? getChildAt(count - 1).getBottom() + mDividerHeight :
625                    getListPaddingTop();
626            fillDown(mFirstPosition + count, startOffset);
627            correctTooHigh(getChildCount());
628        } else {
629            final int startOffset = count > 0 ? getChildAt(0).getTop() - mDividerHeight :
630                    getHeight() - getListPaddingBottom();
631            fillUp(mFirstPosition - 1, startOffset);
632            correctTooLow(getChildCount());
633        }
634    }
635
636    /**
637     * Fills the list from pos down to the end of the list view.
638     *
639     * @param pos The first position to put in the list
640     *
641     * @param nextTop The location where the top of the item associated with pos
642     *        should be drawn
643     *
644     * @return The view that is currently selected, if it happens to be in the
645     *         range that we draw.
646     */
647    private View fillDown(int pos, int nextTop) {
648        View selectedView = null;
649
650        int end = (mBottom - mTop) - mListPadding.bottom;
651
652        while (nextTop < end && pos < mItemCount) {
653            // is this the selected item?
654            boolean selected = pos == mSelectedPosition;
655            View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);
656
657            nextTop = child.getBottom() + mDividerHeight;
658            if (selected) {
659                selectedView = child;
660            }
661            pos++;
662        }
663
664        return selectedView;
665    }
666
667    /**
668     * Fills the list from pos up to the top of the list view.
669     *
670     * @param pos The first position to put in the list
671     *
672     * @param nextBottom The location where the bottom of the item associated
673     *        with pos should be drawn
674     *
675     * @return The view that is currently selected
676     */
677    private View fillUp(int pos, int nextBottom) {
678        View selectedView = null;
679
680        int end = mListPadding.top;
681
682        while (nextBottom > end && pos >= 0) {
683            // is this the selected item?
684            boolean selected = pos == mSelectedPosition;
685            View child = makeAndAddView(pos, nextBottom, false, mListPadding.left, selected);
686            nextBottom = child.getTop() - mDividerHeight;
687            if (selected) {
688                selectedView = child;
689            }
690            pos--;
691        }
692
693        mFirstPosition = pos + 1;
694
695        return selectedView;
696    }
697
698    /**
699     * Fills the list from top to bottom, starting with mFirstPosition
700     *
701     * @param nextTop The location where the top of the first item should be
702     *        drawn
703     *
704     * @return The view that is currently selected
705     */
706    private View fillFromTop(int nextTop) {
707        mFirstPosition = Math.min(mFirstPosition, mSelectedPosition);
708        mFirstPosition = Math.min(mFirstPosition, mItemCount - 1);
709        if (mFirstPosition < 0) {
710            mFirstPosition = 0;
711        }
712        return fillDown(mFirstPosition, nextTop);
713    }
714
715
716    /**
717     * Put mSelectedPosition in the middle of the screen and then build up and
718     * down from there. This method forces mSelectedPosition to the center.
719     *
720     * @param childrenTop Top of the area in which children can be drawn, as
721     *        measured in pixels
722     * @param childrenBottom Bottom of the area in which children can be drawn,
723     *        as measured in pixels
724     * @return Currently selected view
725     */
726    private View fillFromMiddle(int childrenTop, int childrenBottom) {
727        int height = childrenBottom - childrenTop;
728
729        int position = reconcileSelectedPosition();
730
731        View sel = makeAndAddView(position, childrenTop, true,
732                mListPadding.left, true);
733        mFirstPosition = position;
734
735        int selHeight = sel.getMeasuredHeight();
736        if (selHeight <= height) {
737            sel.offsetTopAndBottom((height - selHeight) / 2);
738        }
739
740        fillAboveAndBelow(sel, position);
741
742        if (!mStackFromBottom) {
743            correctTooHigh(getChildCount());
744        } else {
745            correctTooLow(getChildCount());
746        }
747
748        return sel;
749    }
750
751    /**
752     * Once the selected view as been placed, fill up the visible area above and
753     * below it.
754     *
755     * @param sel The selected view
756     * @param position The position corresponding to sel
757     */
758    private void fillAboveAndBelow(View sel, int position) {
759        final int dividerHeight = mDividerHeight;
760        if (!mStackFromBottom) {
761            fillUp(position - 1, sel.getTop() - dividerHeight);
762            adjustViewsUpOrDown();
763            fillDown(position + 1, sel.getBottom() + dividerHeight);
764        } else {
765            fillDown(position + 1, sel.getBottom() + dividerHeight);
766            adjustViewsUpOrDown();
767            fillUp(position - 1, sel.getTop() - dividerHeight);
768        }
769    }
770
771
772    /**
773     * Fills the grid based on positioning the new selection at a specific
774     * location. The selection may be moved so that it does not intersect the
775     * faded edges. The grid is then filled upwards and downwards from there.
776     *
777     * @param selectedTop Where the selected item should be
778     * @param childrenTop Where to start drawing children
779     * @param childrenBottom Last pixel where children can be drawn
780     * @return The view that currently has selection
781     */
782    private View fillFromSelection(int selectedTop, int childrenTop, int childrenBottom) {
783        int fadingEdgeLength = getVerticalFadingEdgeLength();
784        final int selectedPosition = mSelectedPosition;
785
786        View sel;
787
788        final int topSelectionPixel = getTopSelectionPixel(childrenTop, fadingEdgeLength,
789                selectedPosition);
790        final int bottomSelectionPixel = getBottomSelectionPixel(childrenBottom, fadingEdgeLength,
791                selectedPosition);
792
793        sel = makeAndAddView(selectedPosition, selectedTop, true, mListPadding.left, true);
794
795
796        // Some of the newly selected item extends below the bottom of the list
797        if (sel.getBottom() > bottomSelectionPixel) {
798            // Find space available above the selection into which we can scroll
799            // upwards
800            final int spaceAbove = sel.getTop() - topSelectionPixel;
801
802            // Find space required to bring the bottom of the selected item
803            // fully into view
804            final int spaceBelow = sel.getBottom() - bottomSelectionPixel;
805            final int offset = Math.min(spaceAbove, spaceBelow);
806
807            // Now offset the selected item to get it into view
808            sel.offsetTopAndBottom(-offset);
809        } else if (sel.getTop() < topSelectionPixel) {
810            // Find space required to bring the top of the selected item fully
811            // into view
812            final int spaceAbove = topSelectionPixel - sel.getTop();
813
814            // Find space available below the selection into which we can scroll
815            // downwards
816            final int spaceBelow = bottomSelectionPixel - sel.getBottom();
817            final int offset = Math.min(spaceAbove, spaceBelow);
818
819            // Offset the selected item to get it into view
820            sel.offsetTopAndBottom(offset);
821        }
822
823        // Fill in views above and below
824        fillAboveAndBelow(sel, selectedPosition);
825
826        if (!mStackFromBottom) {
827            correctTooHigh(getChildCount());
828        } else {
829            correctTooLow(getChildCount());
830        }
831
832        return sel;
833    }
834
835    /**
836     * Calculate the bottom-most pixel we can draw the selection into
837     *
838     * @param childrenBottom Bottom pixel were children can be drawn
839     * @param fadingEdgeLength Length of the fading edge in pixels, if present
840     * @param selectedPosition The position that will be selected
841     * @return The bottom-most pixel we can draw the selection into
842     */
843    private int getBottomSelectionPixel(int childrenBottom, int fadingEdgeLength,
844            int selectedPosition) {
845        int bottomSelectionPixel = childrenBottom;
846        if (selectedPosition != mItemCount - 1) {
847            bottomSelectionPixel -= fadingEdgeLength;
848        }
849        return bottomSelectionPixel;
850    }
851
852    /**
853     * Calculate the top-most pixel we can draw the selection into
854     *
855     * @param childrenTop Top pixel were children can be drawn
856     * @param fadingEdgeLength Length of the fading edge in pixels, if present
857     * @param selectedPosition The position that will be selected
858     * @return The top-most pixel we can draw the selection into
859     */
860    private int getTopSelectionPixel(int childrenTop, int fadingEdgeLength, int selectedPosition) {
861        // first pixel we can draw the selection into
862        int topSelectionPixel = childrenTop;
863        if (selectedPosition > 0) {
864            topSelectionPixel += fadingEdgeLength;
865        }
866        return topSelectionPixel;
867    }
868
869
870    /**
871     * Fills the list based on positioning the new selection relative to the old
872     * selection. The new selection will be placed at, above, or below the
873     * location of the new selection depending on how the selection is moving.
874     * The selection will then be pinned to the visible part of the screen,
875     * excluding the edges that are faded. The list is then filled upwards and
876     * downwards from there.
877     *
878     * @param oldSel The old selected view. Useful for trying to put the new
879     *        selection in the same place
880     * @param newSel The view that is to become selected. Useful for trying to
881     *        put the new selection in the same place
882     * @param delta Which way we are moving
883     * @param childrenTop Where to start drawing children
884     * @param childrenBottom Last pixel where children can be drawn
885     * @return The view that currently has selection
886     */
887    private View moveSelection(View oldSel, View newSel, int delta, int childrenTop,
888            int childrenBottom) {
889        int fadingEdgeLength = getVerticalFadingEdgeLength();
890        final int selectedPosition = mSelectedPosition;
891
892        View sel;
893
894        final int topSelectionPixel = getTopSelectionPixel(childrenTop, fadingEdgeLength,
895                selectedPosition);
896        final int bottomSelectionPixel = getBottomSelectionPixel(childrenTop, fadingEdgeLength,
897                selectedPosition);
898
899        if (delta > 0) {
900            /*
901             * Case 1: Scrolling down.
902             */
903
904            /*
905             *     Before           After
906             *    |       |        |       |
907             *    +-------+        +-------+
908             *    |   A   |        |   A   |
909             *    |   1   |   =>   +-------+
910             *    +-------+        |   B   |
911             *    |   B   |        |   2   |
912             *    +-------+        +-------+
913             *    |       |        |       |
914             *
915             *    Try to keep the top of the previously selected item where it was.
916             *    oldSel = A
917             *    sel = B
918             */
919
920            // Put oldSel (A) where it belongs
921            oldSel = makeAndAddView(selectedPosition - 1, oldSel.getTop(), true,
922                    mListPadding.left, false);
923
924            final int dividerHeight = mDividerHeight;
925
926            // Now put the new selection (B) below that
927            sel = makeAndAddView(selectedPosition, oldSel.getBottom() + dividerHeight, true,
928                    mListPadding.left, true);
929
930            // Some of the newly selected item extends below the bottom of the list
931            if (sel.getBottom() > bottomSelectionPixel) {
932
933                // Find space available above the selection into which we can scroll upwards
934                int spaceAbove = sel.getTop() - topSelectionPixel;
935
936                // Find space required to bring the bottom of the selected item fully into view
937                int spaceBelow = sel.getBottom() - bottomSelectionPixel;
938
939                // Don't scroll more than half the height of the list
940                int halfVerticalSpace = (childrenBottom - childrenTop) / 2;
941                int offset = Math.min(spaceAbove, spaceBelow);
942                offset = Math.min(offset, halfVerticalSpace);
943
944                // We placed oldSel, so offset that item
945                oldSel.offsetTopAndBottom(-offset);
946                // Now offset the selected item to get it into view
947                sel.offsetTopAndBottom(-offset);
948            }
949
950            // Fill in views above and below
951            if (!mStackFromBottom) {
952                fillUp(mSelectedPosition - 2, sel.getTop() - dividerHeight);
953                adjustViewsUpOrDown();
954                fillDown(mSelectedPosition + 1, sel.getBottom() + dividerHeight);
955            } else {
956                fillDown(mSelectedPosition + 1, sel.getBottom() + dividerHeight);
957                adjustViewsUpOrDown();
958                fillUp(mSelectedPosition - 2, sel.getTop() - dividerHeight);
959            }
960        } else if (delta < 0) {
961            /*
962             * Case 2: Scrolling up.
963             */
964
965            /*
966             *     Before           After
967             *    |       |        |       |
968             *    +-------+        +-------+
969             *    |   A   |        |   A   |
970             *    +-------+   =>   |   1   |
971             *    |   B   |        +-------+
972             *    |   2   |        |   B   |
973             *    +-------+        +-------+
974             *    |       |        |       |
975             *
976             *    Try to keep the top of the item about to become selected where it was.
977             *    newSel = A
978             *    olSel = B
979             */
980
981            if (newSel != null) {
982                // Try to position the top of newSel (A) where it was before it was selected
983                sel = makeAndAddView(selectedPosition, newSel.getTop(), true, mListPadding.left,
984                        true);
985            } else {
986                // If (A) was not on screen and so did not have a view, position
987                // it above the oldSel (B)
988                sel = makeAndAddView(selectedPosition, oldSel.getTop(), false, mListPadding.left,
989                        true);
990            }
991
992            // Some of the newly selected item extends above the top of the list
993            if (sel.getTop() < topSelectionPixel) {
994                // Find space required to bring the top of the selected item fully into view
995                int spaceAbove = topSelectionPixel - sel.getTop();
996
997               // Find space available below the selection into which we can scroll downwards
998                int spaceBelow = bottomSelectionPixel - sel.getBottom();
999
1000                // Don't scroll more than half the height of the list
1001                int halfVerticalSpace = (childrenBottom - childrenTop) / 2;
1002                int offset = Math.min(spaceAbove, spaceBelow);
1003                offset = Math.min(offset, halfVerticalSpace);
1004
1005                // Offset the selected item to get it into view
1006                sel.offsetTopAndBottom(offset);
1007            }
1008
1009            // Fill in views above and below
1010            fillAboveAndBelow(sel, selectedPosition);
1011        } else {
1012
1013            int oldTop = oldSel.getTop();
1014
1015            /*
1016             * Case 3: Staying still
1017             */
1018            sel = makeAndAddView(selectedPosition, oldTop, true, mListPadding.left, true);
1019
1020            // We're staying still...
1021            if (oldTop < childrenTop) {
1022                // ... but the top of the old selection was off screen.
1023                // (This can happen if the data changes size out from under us)
1024                int newBottom = sel.getBottom();
1025                if (newBottom < childrenTop + 20) {
1026                    // Not enough visible -- bring it onscreen
1027                    sel.offsetTopAndBottom(childrenTop - sel.getTop());
1028                }
1029            }
1030
1031            // Fill in views above and below
1032            fillAboveAndBelow(sel, selectedPosition);
1033        }
1034
1035        return sel;
1036    }
1037
1038    private class FocusSelector implements Runnable {
1039        private int mPosition;
1040        private int mPositionTop;
1041
1042        public FocusSelector setup(int position, int top) {
1043            mPosition = position;
1044            mPositionTop = top;
1045            return this;
1046        }
1047
1048        public void run() {
1049            setSelectionFromTop(mPosition, mPositionTop);
1050        }
1051    }
1052
1053    @Override
1054    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
1055        if (getChildCount() > 0) {
1056            View focusedChild = getFocusedChild();
1057            if (focusedChild != null) {
1058                final int childPosition = mFirstPosition + indexOfChild(focusedChild);
1059                final int childBottom = focusedChild.getBottom();
1060                final int offset = Math.max(0, childBottom - (h - mPaddingTop));
1061                final int top = focusedChild.getTop() - offset;
1062                if (mFocusSelector == null) {
1063                    mFocusSelector = new FocusSelector();
1064                }
1065                post(mFocusSelector.setup(childPosition, top));
1066            }
1067        }
1068        super.onSizeChanged(w, h, oldw, oldh);
1069    }
1070
1071    @Override
1072    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1073        // Sets up mListPadding
1074        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
1075
1076        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
1077        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
1078        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
1079        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
1080
1081        int childWidth = 0;
1082        int childHeight = 0;
1083
1084        mItemCount = mAdapter == null ? 0 : mAdapter.getCount();
1085        if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED ||
1086                heightMode == MeasureSpec.UNSPECIFIED)) {
1087            final View child = obtainView(0, mIsScrap);
1088
1089            measureScrapChild(child, 0, widthMeasureSpec);
1090
1091            childWidth = child.getMeasuredWidth();
1092            childHeight = child.getMeasuredHeight();
1093
1094            if (recycleOnMeasure() && mRecycler.shouldRecycleViewType(
1095                    ((LayoutParams) child.getLayoutParams()).viewType)) {
1096                mRecycler.addScrapView(child);
1097            }
1098        }
1099
1100        if (widthMode == MeasureSpec.UNSPECIFIED) {
1101            widthSize = mListPadding.left + mListPadding.right + childWidth +
1102                    getVerticalScrollbarWidth();
1103        }
1104
1105        if (heightMode == MeasureSpec.UNSPECIFIED) {
1106            heightSize = mListPadding.top + mListPadding.bottom + childHeight +
1107                    getVerticalFadingEdgeLength() * 2;
1108        }
1109
1110        if (heightMode == MeasureSpec.AT_MOST) {
1111            // TODO: after first layout we should maybe start at the first visible position, not 0
1112            heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
1113        }
1114
1115        setMeasuredDimension(widthSize, heightSize);
1116        mWidthMeasureSpec = widthMeasureSpec;
1117    }
1118
1119    private void measureScrapChild(View child, int position, int widthMeasureSpec) {
1120        LayoutParams p = (LayoutParams) child.getLayoutParams();
1121        if (p == null) {
1122            p = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
1123                    ViewGroup.LayoutParams.WRAP_CONTENT, 0);
1124            child.setLayoutParams(p);
1125        }
1126        p.viewType = mAdapter.getItemViewType(position);
1127        p.forceAdd = true;
1128
1129        int childWidthSpec = ViewGroup.getChildMeasureSpec(widthMeasureSpec,
1130                mListPadding.left + mListPadding.right, p.width);
1131        int lpHeight = p.height;
1132        int childHeightSpec;
1133        if (lpHeight > 0) {
1134            childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
1135        } else {
1136            childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
1137        }
1138        child.measure(childWidthSpec, childHeightSpec);
1139    }
1140
1141    /**
1142     * @return True to recycle the views used to measure this ListView in
1143     *         UNSPECIFIED/AT_MOST modes, false otherwise.
1144     * @hide
1145     */
1146    @ViewDebug.ExportedProperty(category = "list")
1147    protected boolean recycleOnMeasure() {
1148        return true;
1149    }
1150
1151    /**
1152     * Measures the height of the given range of children (inclusive) and
1153     * returns the height with this ListView's padding and divider heights
1154     * included. If maxHeight is provided, the measuring will stop when the
1155     * current height reaches maxHeight.
1156     *
1157     * @param widthMeasureSpec The width measure spec to be given to a child's
1158     *            {@link View#measure(int, int)}.
1159     * @param startPosition The position of the first child to be shown.
1160     * @param endPosition The (inclusive) position of the last child to be
1161     *            shown. Specify {@link #NO_POSITION} if the last child should be
1162     *            the last available child from the adapter.
1163     * @param maxHeight The maximum height that will be returned (if all the
1164     *            children don't fit in this value, this value will be
1165     *            returned).
1166     * @param disallowPartialChildPosition In general, whether the returned
1167     *            height should only contain entire children. This is more
1168     *            powerful--it is the first inclusive position at which partial
1169     *            children will not be allowed. Example: it looks nice to have
1170     *            at least 3 completely visible children, and in portrait this
1171     *            will most likely fit; but in landscape there could be times
1172     *            when even 2 children can not be completely shown, so a value
1173     *            of 2 (remember, inclusive) would be good (assuming
1174     *            startPosition is 0).
1175     * @return The height of this ListView with the given children.
1176     */
1177    final int measureHeightOfChildren(int widthMeasureSpec, int startPosition, int endPosition,
1178            final int maxHeight, int disallowPartialChildPosition) {
1179
1180        final ListAdapter adapter = mAdapter;
1181        if (adapter == null) {
1182            return mListPadding.top + mListPadding.bottom;
1183        }
1184
1185        // Include the padding of the list
1186        int returnedHeight = mListPadding.top + mListPadding.bottom;
1187        final int dividerHeight = ((mDividerHeight > 0) && mDivider != null) ? mDividerHeight : 0;
1188        // The previous height value that was less than maxHeight and contained
1189        // no partial children
1190        int prevHeightWithoutPartialChild = 0;
1191        int i;
1192        View child;
1193
1194        // mItemCount - 1 since endPosition parameter is inclusive
1195        endPosition = (endPosition == NO_POSITION) ? adapter.getCount() - 1 : endPosition;
1196        final AbsListView.RecycleBin recycleBin = mRecycler;
1197        final boolean recyle = recycleOnMeasure();
1198        final boolean[] isScrap = mIsScrap;
1199
1200        for (i = startPosition; i <= endPosition; ++i) {
1201            child = obtainView(i, isScrap);
1202
1203            measureScrapChild(child, i, widthMeasureSpec);
1204
1205            if (i > 0) {
1206                // Count the divider for all but one child
1207                returnedHeight += dividerHeight;
1208            }
1209
1210            // Recycle the view before we possibly return from the method
1211            if (recyle && recycleBin.shouldRecycleViewType(
1212                    ((LayoutParams) child.getLayoutParams()).viewType)) {
1213                recycleBin.addScrapView(child);
1214            }
1215
1216            returnedHeight += child.getMeasuredHeight();
1217
1218            if (returnedHeight >= maxHeight) {
1219                // We went over, figure out which height to return.  If returnedHeight > maxHeight,
1220                // then the i'th position did not fit completely.
1221                return (disallowPartialChildPosition >= 0) // Disallowing is enabled (> -1)
1222                            && (i > disallowPartialChildPosition) // We've past the min pos
1223                            && (prevHeightWithoutPartialChild > 0) // We have a prev height
1224                            && (returnedHeight != maxHeight) // i'th child did not fit completely
1225                        ? prevHeightWithoutPartialChild
1226                        : maxHeight;
1227            }
1228
1229            if ((disallowPartialChildPosition >= 0) && (i >= disallowPartialChildPosition)) {
1230                prevHeightWithoutPartialChild = returnedHeight;
1231            }
1232        }
1233
1234        // At this point, we went through the range of children, and they each
1235        // completely fit, so return the returnedHeight
1236        return returnedHeight;
1237    }
1238
1239    @Override
1240    int findMotionRow(int y) {
1241        int childCount = getChildCount();
1242        if (childCount > 0) {
1243            if (!mStackFromBottom) {
1244                for (int i = 0; i < childCount; i++) {
1245                    View v = getChildAt(i);
1246                    if (y <= v.getBottom()) {
1247                        return mFirstPosition + i;
1248                    }
1249                }
1250            } else {
1251                for (int i = childCount - 1; i >= 0; i--) {
1252                    View v = getChildAt(i);
1253                    if (y >= v.getTop()) {
1254                        return mFirstPosition + i;
1255                    }
1256                }
1257            }
1258        }
1259        return INVALID_POSITION;
1260    }
1261
1262    /**
1263     * Put a specific item at a specific location on the screen and then build
1264     * up and down from there.
1265     *
1266     * @param position The reference view to use as the starting point
1267     * @param top Pixel offset from the top of this view to the top of the
1268     *        reference view.
1269     *
1270     * @return The selected view, or null if the selected view is outside the
1271     *         visible area.
1272     */
1273    private View fillSpecific(int position, int top) {
1274        boolean tempIsSelected = position == mSelectedPosition;
1275        View temp = makeAndAddView(position, top, true, mListPadding.left, tempIsSelected);
1276        // Possibly changed again in fillUp if we add rows above this one.
1277        mFirstPosition = position;
1278
1279        View above;
1280        View below;
1281
1282        final int dividerHeight = mDividerHeight;
1283        if (!mStackFromBottom) {
1284            above = fillUp(position - 1, temp.getTop() - dividerHeight);
1285            // This will correct for the top of the first view not touching the top of the list
1286            adjustViewsUpOrDown();
1287            below = fillDown(position + 1, temp.getBottom() + dividerHeight);
1288            int childCount = getChildCount();
1289            if (childCount > 0) {
1290                correctTooHigh(childCount);
1291            }
1292        } else {
1293            below = fillDown(position + 1, temp.getBottom() + dividerHeight);
1294            // This will correct for the bottom of the last view not touching the bottom of the list
1295            adjustViewsUpOrDown();
1296            above = fillUp(position - 1, temp.getTop() - dividerHeight);
1297            int childCount = getChildCount();
1298            if (childCount > 0) {
1299                 correctTooLow(childCount);
1300            }
1301        }
1302
1303        if (tempIsSelected) {
1304            return temp;
1305        } else if (above != null) {
1306            return above;
1307        } else {
1308            return below;
1309        }
1310    }
1311
1312    /**
1313     * Check if we have dragged the bottom of the list too high (we have pushed the
1314     * top element off the top of the screen when we did not need to). Correct by sliding
1315     * everything back down.
1316     *
1317     * @param childCount Number of children
1318     */
1319    private void correctTooHigh(int childCount) {
1320        // First see if the last item is visible. If it is not, it is OK for the
1321        // top of the list to be pushed up.
1322        int lastPosition = mFirstPosition + childCount - 1;
1323        if (lastPosition == mItemCount - 1 && childCount > 0) {
1324
1325            // Get the last child ...
1326            final View lastChild = getChildAt(childCount - 1);
1327
1328            // ... and its bottom edge
1329            final int lastBottom = lastChild.getBottom();
1330
1331            // This is bottom of our drawable area
1332            final int end = (mBottom - mTop) - mListPadding.bottom;
1333
1334            // This is how far the bottom edge of the last view is from the bottom of the
1335            // drawable area
1336            int bottomOffset = end - lastBottom;
1337            View firstChild = getChildAt(0);
1338            final int firstTop = firstChild.getTop();
1339
1340            // Make sure we are 1) Too high, and 2) Either there are more rows above the
1341            // first row or the first row is scrolled off the top of the drawable area
1342            if (bottomOffset > 0 && (mFirstPosition > 0 || firstTop < mListPadding.top))  {
1343                if (mFirstPosition == 0) {
1344                    // Don't pull the top too far down
1345                    bottomOffset = Math.min(bottomOffset, mListPadding.top - firstTop);
1346                }
1347                // Move everything down
1348                offsetChildrenTopAndBottom(bottomOffset);
1349                if (mFirstPosition > 0) {
1350                    // Fill the gap that was opened above mFirstPosition with more rows, if
1351                    // possible
1352                    fillUp(mFirstPosition - 1, firstChild.getTop() - mDividerHeight);
1353                    // Close up the remaining gap
1354                    adjustViewsUpOrDown();
1355                }
1356
1357            }
1358        }
1359    }
1360
1361    /**
1362     * Check if we have dragged the bottom of the list too low (we have pushed the
1363     * bottom element off the bottom of the screen when we did not need to). Correct by sliding
1364     * everything back up.
1365     *
1366     * @param childCount Number of children
1367     */
1368    private void correctTooLow(int childCount) {
1369        // First see if the first item is visible. If it is not, it is OK for the
1370        // bottom of the list to be pushed down.
1371        if (mFirstPosition == 0 && childCount > 0) {
1372
1373            // Get the first child ...
1374            final View firstChild = getChildAt(0);
1375
1376            // ... and its top edge
1377            final int firstTop = firstChild.getTop();
1378
1379            // This is top of our drawable area
1380            final int start = mListPadding.top;
1381
1382            // This is bottom of our drawable area
1383            final int end = (mBottom - mTop) - mListPadding.bottom;
1384
1385            // This is how far the top edge of the first view is from the top of the
1386            // drawable area
1387            int topOffset = firstTop - start;
1388            View lastChild = getChildAt(childCount - 1);
1389            final int lastBottom = lastChild.getBottom();
1390            int lastPosition = mFirstPosition + childCount - 1;
1391
1392            // Make sure we are 1) Too low, and 2) Either there are more rows below the
1393            // last row or the last row is scrolled off the bottom of the drawable area
1394            if (topOffset > 0) {
1395                if (lastPosition < mItemCount - 1 || lastBottom > end)  {
1396                    if (lastPosition == mItemCount - 1) {
1397                        // Don't pull the bottom too far up
1398                        topOffset = Math.min(topOffset, lastBottom - end);
1399                    }
1400                    // Move everything up
1401                    offsetChildrenTopAndBottom(-topOffset);
1402                    if (lastPosition < mItemCount - 1) {
1403                        // Fill the gap that was opened below the last position with more rows, if
1404                        // possible
1405                        fillDown(lastPosition + 1, lastChild.getBottom() + mDividerHeight);
1406                        // Close up the remaining gap
1407                        adjustViewsUpOrDown();
1408                    }
1409                } else if (lastPosition == mItemCount - 1) {
1410                    adjustViewsUpOrDown();
1411                }
1412            }
1413        }
1414    }
1415
1416    @Override
1417    protected void layoutChildren() {
1418        final boolean blockLayoutRequests = mBlockLayoutRequests;
1419        if (!blockLayoutRequests) {
1420            mBlockLayoutRequests = true;
1421        } else {
1422            return;
1423        }
1424
1425        try {
1426            super.layoutChildren();
1427
1428            invalidate();
1429
1430            if (mAdapter == null) {
1431                resetList();
1432                invokeOnItemScrollListener();
1433                return;
1434            }
1435
1436            int childrenTop = mListPadding.top;
1437            int childrenBottom = mBottom - mTop - mListPadding.bottom;
1438
1439            int childCount = getChildCount();
1440            int index = 0;
1441            int delta = 0;
1442
1443            View sel;
1444            View oldSel = null;
1445            View oldFirst = null;
1446            View newSel = null;
1447
1448            View focusLayoutRestoreView = null;
1449
1450            // Remember stuff we will need down below
1451            switch (mLayoutMode) {
1452            case LAYOUT_SET_SELECTION:
1453                index = mNextSelectedPosition - mFirstPosition;
1454                if (index >= 0 && index < childCount) {
1455                    newSel = getChildAt(index);
1456                }
1457                break;
1458            case LAYOUT_FORCE_TOP:
1459            case LAYOUT_FORCE_BOTTOM:
1460            case LAYOUT_SPECIFIC:
1461            case LAYOUT_SYNC:
1462                break;
1463            case LAYOUT_MOVE_SELECTION:
1464            default:
1465                // Remember the previously selected view
1466                index = mSelectedPosition - mFirstPosition;
1467                if (index >= 0 && index < childCount) {
1468                    oldSel = getChildAt(index);
1469                }
1470
1471                // Remember the previous first child
1472                oldFirst = getChildAt(0);
1473
1474                if (mNextSelectedPosition >= 0) {
1475                    delta = mNextSelectedPosition - mSelectedPosition;
1476                }
1477
1478                // Caution: newSel might be null
1479                newSel = getChildAt(index + delta);
1480            }
1481
1482
1483            boolean dataChanged = mDataChanged;
1484            if (dataChanged) {
1485                handleDataChanged();
1486            }
1487
1488            // Handle the empty set by removing all views that are visible
1489            // and calling it a day
1490            if (mItemCount == 0) {
1491                resetList();
1492                invokeOnItemScrollListener();
1493                return;
1494            } else if (mItemCount != mAdapter.getCount()) {
1495                throw new IllegalStateException("The content of the adapter has changed but "
1496                        + "ListView did not receive a notification. Make sure the content of "
1497                        + "your adapter is not modified from a background thread, but only "
1498                        + "from the UI thread. [in ListView(" + getId() + ", " + getClass()
1499                        + ") with Adapter(" + mAdapter.getClass() + ")]");
1500            }
1501
1502            setSelectedPositionInt(mNextSelectedPosition);
1503
1504            // Pull all children into the RecycleBin.
1505            // These views will be reused if possible
1506            final int firstPosition = mFirstPosition;
1507            final RecycleBin recycleBin = mRecycler;
1508
1509            // reset the focus restoration
1510            View focusLayoutRestoreDirectChild = null;
1511
1512
1513            // Don't put header or footer views into the Recycler. Those are
1514            // already cached in mHeaderViews;
1515            if (dataChanged) {
1516                for (int i = 0; i < childCount; i++) {
1517                    recycleBin.addScrapView(getChildAt(i));
1518                    if (ViewDebug.TRACE_RECYCLER) {
1519                        ViewDebug.trace(getChildAt(i),
1520                                ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP, index, i);
1521                    }
1522                }
1523            } else {
1524                recycleBin.fillActiveViews(childCount, firstPosition);
1525            }
1526
1527            // take focus back to us temporarily to avoid the eventual
1528            // call to clear focus when removing the focused child below
1529            // from messing things up when ViewRoot assigns focus back
1530            // to someone else
1531            final View focusedChild = getFocusedChild();
1532            if (focusedChild != null) {
1533                // TODO: in some cases focusedChild.getParent() == null
1534
1535                // we can remember the focused view to restore after relayout if the
1536                // data hasn't changed, or if the focused position is a header or footer
1537                if (!dataChanged || isDirectChildHeaderOrFooter(focusedChild)) {
1538                    focusLayoutRestoreDirectChild = focusedChild;
1539                    // remember the specific view that had focus
1540                    focusLayoutRestoreView = findFocus();
1541                    if (focusLayoutRestoreView != null) {
1542                        // tell it we are going to mess with it
1543                        focusLayoutRestoreView.onStartTemporaryDetach();
1544                    }
1545                }
1546                requestFocus();
1547            }
1548
1549            // Clear out old views
1550            detachAllViewsFromParent();
1551
1552            switch (mLayoutMode) {
1553            case LAYOUT_SET_SELECTION:
1554                if (newSel != null) {
1555                    sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom);
1556                } else {
1557                    sel = fillFromMiddle(childrenTop, childrenBottom);
1558                }
1559                break;
1560            case LAYOUT_SYNC:
1561                sel = fillSpecific(mSyncPosition, mSpecificTop);
1562                break;
1563            case LAYOUT_FORCE_BOTTOM:
1564                sel = fillUp(mItemCount - 1, childrenBottom);
1565                adjustViewsUpOrDown();
1566                break;
1567            case LAYOUT_FORCE_TOP:
1568                mFirstPosition = 0;
1569                sel = fillFromTop(childrenTop);
1570                adjustViewsUpOrDown();
1571                break;
1572            case LAYOUT_SPECIFIC:
1573                sel = fillSpecific(reconcileSelectedPosition(), mSpecificTop);
1574                break;
1575            case LAYOUT_MOVE_SELECTION:
1576                sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom);
1577                break;
1578            default:
1579                if (childCount == 0) {
1580                    if (!mStackFromBottom) {
1581                        final int position = lookForSelectablePosition(0, true);
1582                        setSelectedPositionInt(position);
1583                        sel = fillFromTop(childrenTop);
1584                    } else {
1585                        final int position = lookForSelectablePosition(mItemCount - 1, false);
1586                        setSelectedPositionInt(position);
1587                        sel = fillUp(mItemCount - 1, childrenBottom);
1588                    }
1589                } else {
1590                    if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) {
1591                        sel = fillSpecific(mSelectedPosition,
1592                                oldSel == null ? childrenTop : oldSel.getTop());
1593                    } else if (mFirstPosition < mItemCount) {
1594                        sel = fillSpecific(mFirstPosition,
1595                                oldFirst == null ? childrenTop : oldFirst.getTop());
1596                    } else {
1597                        sel = fillSpecific(0, childrenTop);
1598                    }
1599                }
1600                break;
1601            }
1602
1603            // Flush any cached views that did not get reused above
1604            recycleBin.scrapActiveViews();
1605
1606            if (sel != null) {
1607                // the current selected item should get focus if items
1608                // are focusable
1609                if (mItemsCanFocus && hasFocus() && !sel.hasFocus()) {
1610                    final boolean focusWasTaken = (sel == focusLayoutRestoreDirectChild &&
1611                            focusLayoutRestoreView.requestFocus()) || sel.requestFocus();
1612                    if (!focusWasTaken) {
1613                        // selected item didn't take focus, fine, but still want
1614                        // to make sure something else outside of the selected view
1615                        // has focus
1616                        final View focused = getFocusedChild();
1617                        if (focused != null) {
1618                            focused.clearFocus();
1619                        }
1620                        positionSelector(sel);
1621                    } else {
1622                        sel.setSelected(false);
1623                        mSelectorRect.setEmpty();
1624                    }
1625                } else {
1626                    positionSelector(sel);
1627                }
1628                mSelectedTop = sel.getTop();
1629            } else {
1630                if (mTouchMode > TOUCH_MODE_DOWN && mTouchMode < TOUCH_MODE_SCROLL) {
1631                    View child = getChildAt(mMotionPosition - mFirstPosition);
1632                    if (child != null) positionSelector(child);
1633                } else {
1634                    mSelectedTop = 0;
1635                    mSelectorRect.setEmpty();
1636                }
1637
1638                // even if there is not selected position, we may need to restore
1639                // focus (i.e. something focusable in touch mode)
1640                if (hasFocus() && focusLayoutRestoreView != null) {
1641                    focusLayoutRestoreView.requestFocus();
1642                }
1643            }
1644
1645            // tell focus view we are done mucking with it, if it is still in
1646            // our view hierarchy.
1647            if (focusLayoutRestoreView != null
1648                    && focusLayoutRestoreView.getWindowToken() != null) {
1649                focusLayoutRestoreView.onFinishTemporaryDetach();
1650            }
1651
1652            mLayoutMode = LAYOUT_NORMAL;
1653            mDataChanged = false;
1654            mNeedSync = false;
1655            setNextSelectedPositionInt(mSelectedPosition);
1656
1657            updateScrollIndicators();
1658
1659            if (mItemCount > 0) {
1660                checkSelectionChanged();
1661            }
1662
1663            invokeOnItemScrollListener();
1664        } finally {
1665            if (!blockLayoutRequests) {
1666                mBlockLayoutRequests = false;
1667            }
1668        }
1669    }
1670
1671    /**
1672     * @param child a direct child of this list.
1673     * @return Whether child is a header or footer view.
1674     */
1675    private boolean isDirectChildHeaderOrFooter(View child) {
1676
1677        final ArrayList<FixedViewInfo> headers = mHeaderViewInfos;
1678        final int numHeaders = headers.size();
1679        for (int i = 0; i < numHeaders; i++) {
1680            if (child == headers.get(i).view) {
1681                return true;
1682            }
1683        }
1684        final ArrayList<FixedViewInfo> footers = mFooterViewInfos;
1685        final int numFooters = footers.size();
1686        for (int i = 0; i < numFooters; i++) {
1687            if (child == footers.get(i).view) {
1688                return true;
1689            }
1690        }
1691        return false;
1692    }
1693
1694    /**
1695     * Obtain the view and add it to our list of children. The view can be made
1696     * fresh, converted from an unused view, or used as is if it was in the
1697     * recycle bin.
1698     *
1699     * @param position Logical position in the list
1700     * @param y Top or bottom edge of the view to add
1701     * @param flow If flow is true, align top edge to y. If false, align bottom
1702     *        edge to y.
1703     * @param childrenLeft Left edge where children should be positioned
1704     * @param selected Is this position selected?
1705     * @return View that was added
1706     */
1707    private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
1708            boolean selected) {
1709        View child;
1710
1711
1712        if (!mDataChanged) {
1713            // Try to use an exsiting view for this position
1714            child = mRecycler.getActiveView(position);
1715            if (child != null) {
1716                if (ViewDebug.TRACE_RECYCLER) {
1717                    ViewDebug.trace(child, ViewDebug.RecyclerTraceType.RECYCLE_FROM_ACTIVE_HEAP,
1718                            position, getChildCount());
1719                }
1720
1721                // Found it -- we're using an existing child
1722                // This just needs to be positioned
1723                setupChild(child, position, y, flow, childrenLeft, selected, true);
1724
1725                return child;
1726            }
1727        }
1728
1729        // Make a new view for this position, or convert an unused view if possible
1730        child = obtainView(position, mIsScrap);
1731
1732        // This needs to be positioned and measured
1733        setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
1734
1735        return child;
1736    }
1737
1738    /**
1739     * Add a view as a child and make sure it is measured (if necessary) and
1740     * positioned properly.
1741     *
1742     * @param child The view to add
1743     * @param position The position of this child
1744     * @param y The y position relative to which this view will be positioned
1745     * @param flowDown If true, align top edge to y. If false, align bottom
1746     *        edge to y.
1747     * @param childrenLeft Left edge where children should be positioned
1748     * @param selected Is this position selected?
1749     * @param recycled Has this view been pulled from the recycle bin? If so it
1750     *        does not need to be remeasured.
1751     */
1752    private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
1753            boolean selected, boolean recycled) {
1754        final boolean isSelected = selected && shouldShowSelector();
1755        final boolean updateChildSelected = isSelected != child.isSelected();
1756        final int mode = mTouchMode;
1757        final boolean isPressed = mode > TOUCH_MODE_DOWN && mode < TOUCH_MODE_SCROLL &&
1758                mMotionPosition == position;
1759        final boolean updateChildPressed = isPressed != child.isPressed();
1760        final boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested();
1761
1762        // Respect layout params that are already in the view. Otherwise make some up...
1763        // noinspection unchecked
1764        AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
1765        if (p == null) {
1766            p = new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
1767                    ViewGroup.LayoutParams.WRAP_CONTENT, 0);
1768        }
1769        p.viewType = mAdapter.getItemViewType(position);
1770
1771        if ((recycled && !p.forceAdd) || (p.recycledHeaderFooter &&
1772                p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
1773            attachViewToParent(child, flowDown ? -1 : 0, p);
1774        } else {
1775            p.forceAdd = false;
1776            if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
1777                p.recycledHeaderFooter = true;
1778            }
1779            addViewInLayout(child, flowDown ? -1 : 0, p, true);
1780        }
1781
1782        if (updateChildSelected) {
1783            child.setSelected(isSelected);
1784        }
1785
1786        if (updateChildPressed) {
1787            child.setPressed(isPressed);
1788        }
1789
1790        if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null) {
1791            if (child instanceof Checkable) {
1792                ((Checkable) child).setChecked(mCheckStates.get(position));
1793            }
1794        }
1795
1796        if (needToMeasure) {
1797            int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
1798                    mListPadding.left + mListPadding.right, p.width);
1799            int lpHeight = p.height;
1800            int childHeightSpec;
1801            if (lpHeight > 0) {
1802                childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
1803            } else {
1804                childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
1805            }
1806            child.measure(childWidthSpec, childHeightSpec);
1807        } else {
1808            cleanupLayoutState(child);
1809        }
1810
1811        final int w = child.getMeasuredWidth();
1812        final int h = child.getMeasuredHeight();
1813        final int childTop = flowDown ? y : y - h;
1814
1815        if (needToMeasure) {
1816            final int childRight = childrenLeft + w;
1817            final int childBottom = childTop + h;
1818            child.layout(childrenLeft, childTop, childRight, childBottom);
1819        } else {
1820            child.offsetLeftAndRight(childrenLeft - child.getLeft());
1821            child.offsetTopAndBottom(childTop - child.getTop());
1822        }
1823
1824        if (mCachingStarted && !child.isDrawingCacheEnabled()) {
1825            child.setDrawingCacheEnabled(true);
1826        }
1827    }
1828
1829    @Override
1830    protected boolean canAnimate() {
1831        return super.canAnimate() && mItemCount > 0;
1832    }
1833
1834    /**
1835     * Sets the currently selected item. If in touch mode, the item will not be selected
1836     * but it will still be positioned appropriately. If the specified selection position
1837     * is less than 0, then the item at position 0 will be selected.
1838     *
1839     * @param position Index (starting at 0) of the data item to be selected.
1840     */
1841    @Override
1842    public void setSelection(int position) {
1843        setSelectionFromTop(position, 0);
1844    }
1845
1846    /**
1847     * Sets the selected item and positions the selection y pixels from the top edge
1848     * of the ListView. (If in touch mode, the item will not be selected but it will
1849     * still be positioned appropriately.)
1850     *
1851     * @param position Index (starting at 0) of the data item to be selected.
1852     * @param y The distance from the top edge of the ListView (plus padding) that the
1853     *        item will be positioned.
1854     */
1855    public void setSelectionFromTop(int position, int y) {
1856        if (mAdapter == null) {
1857            return;
1858        }
1859
1860        if (!isInTouchMode()) {
1861            position = lookForSelectablePosition(position, true);
1862            if (position >= 0) {
1863                setNextSelectedPositionInt(position);
1864            }
1865        } else {
1866            mResurrectToPosition = position;
1867        }
1868
1869        if (position >= 0) {
1870            mLayoutMode = LAYOUT_SPECIFIC;
1871            mSpecificTop = mListPadding.top + y;
1872
1873            if (mNeedSync) {
1874                mSyncPosition = position;
1875                mSyncRowId = mAdapter.getItemId(position);
1876            }
1877
1878            requestLayout();
1879        }
1880    }
1881
1882    /**
1883     * Makes the item at the supplied position selected.
1884     *
1885     * @param position the position of the item to select
1886     */
1887    @Override
1888    void setSelectionInt(int position) {
1889        setNextSelectedPositionInt(position);
1890        boolean awakeScrollbars = false;
1891
1892        final int selectedPosition = mSelectedPosition;
1893
1894        if (selectedPosition >= 0) {
1895            if (position == selectedPosition - 1) {
1896                awakeScrollbars = true;
1897            } else if (position == selectedPosition + 1) {
1898                awakeScrollbars = true;
1899            }
1900        }
1901
1902        layoutChildren();
1903
1904        if (awakeScrollbars) {
1905            awakenScrollBars();
1906        }
1907    }
1908
1909    /**
1910     * Find a position that can be selected (i.e., is not a separator).
1911     *
1912     * @param position The starting position to look at.
1913     * @param lookDown Whether to look down for other positions.
1914     * @return The next selectable position starting at position and then searching either up or
1915     *         down. Returns {@link #INVALID_POSITION} if nothing can be found.
1916     */
1917    @Override
1918    int lookForSelectablePosition(int position, boolean lookDown) {
1919        final ListAdapter adapter = mAdapter;
1920        if (adapter == null || isInTouchMode()) {
1921            return INVALID_POSITION;
1922        }
1923
1924        final int count = adapter.getCount();
1925        if (!mAreAllItemsSelectable) {
1926            if (lookDown) {
1927                position = Math.max(0, position);
1928                while (position < count && !adapter.isEnabled(position)) {
1929                    position++;
1930                }
1931            } else {
1932                position = Math.min(position, count - 1);
1933                while (position >= 0 && !adapter.isEnabled(position)) {
1934                    position--;
1935                }
1936            }
1937
1938            if (position < 0 || position >= count) {
1939                return INVALID_POSITION;
1940            }
1941            return position;
1942        } else {
1943            if (position < 0 || position >= count) {
1944                return INVALID_POSITION;
1945            }
1946            return position;
1947        }
1948    }
1949
1950    @Override
1951    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
1952        boolean populated = super.dispatchPopulateAccessibilityEvent(event);
1953
1954        // If the item count is less than 15 then subtract disabled items from the count and
1955        // position. Otherwise ignore disabled items.
1956        if (!populated) {
1957            int itemCount = 0;
1958            int currentItemIndex = getSelectedItemPosition();
1959
1960            ListAdapter adapter = getAdapter();
1961            if (adapter != null) {
1962                final int count = adapter.getCount();
1963                if (count < 15) {
1964                    for (int i = 0; i < count; i++) {
1965                        if (adapter.isEnabled(i)) {
1966                            itemCount++;
1967                        } else if (i <= currentItemIndex) {
1968                            currentItemIndex--;
1969                        }
1970                    }
1971                } else {
1972                    itemCount = count;
1973                }
1974            }
1975
1976            event.setItemCount(itemCount);
1977            event.setCurrentItemIndex(currentItemIndex);
1978        }
1979
1980        return populated;
1981    }
1982
1983    /**
1984     * setSelectionAfterHeaderView set the selection to be the first list item
1985     * after the header views.
1986     */
1987    public void setSelectionAfterHeaderView() {
1988        final int count = mHeaderViewInfos.size();
1989        if (count > 0) {
1990            mNextSelectedPosition = 0;
1991            return;
1992        }
1993
1994        if (mAdapter != null) {
1995            setSelection(count);
1996        } else {
1997            mNextSelectedPosition = count;
1998            mLayoutMode = LAYOUT_SET_SELECTION;
1999        }
2000
2001    }
2002
2003    @Override
2004    public boolean dispatchKeyEvent(KeyEvent event) {
2005        // Dispatch in the normal way
2006        boolean handled = super.dispatchKeyEvent(event);
2007        if (!handled) {
2008            // If we didn't handle it...
2009            View focused = getFocusedChild();
2010            if (focused != null && event.getAction() == KeyEvent.ACTION_DOWN) {
2011                // ... and our focused child didn't handle it
2012                // ... give it to ourselves so we can scroll if necessary
2013                handled = onKeyDown(event.getKeyCode(), event);
2014            }
2015        }
2016        return handled;
2017    }
2018
2019    @Override
2020    public boolean onKeyDown(int keyCode, KeyEvent event) {
2021        return commonKey(keyCode, 1, event);
2022    }
2023
2024    @Override
2025    public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
2026        return commonKey(keyCode, repeatCount, event);
2027    }
2028
2029    @Override
2030    public boolean onKeyUp(int keyCode, KeyEvent event) {
2031        return commonKey(keyCode, 1, event);
2032    }
2033
2034    private boolean commonKey(int keyCode, int count, KeyEvent event) {
2035        if (mAdapter == null) {
2036            return false;
2037        }
2038
2039        if (mDataChanged) {
2040            layoutChildren();
2041        }
2042
2043        boolean handled = false;
2044        int action = event.getAction();
2045
2046        if (action != KeyEvent.ACTION_UP) {
2047            if (mSelectedPosition < 0) {
2048                switch (keyCode) {
2049                case KeyEvent.KEYCODE_DPAD_UP:
2050                case KeyEvent.KEYCODE_DPAD_DOWN:
2051                case KeyEvent.KEYCODE_DPAD_CENTER:
2052                case KeyEvent.KEYCODE_ENTER:
2053                case KeyEvent.KEYCODE_SPACE:
2054                    if (resurrectSelection()) {
2055                        return true;
2056                    }
2057                }
2058            }
2059            switch (keyCode) {
2060            case KeyEvent.KEYCODE_DPAD_UP:
2061                if (!event.isAltPressed()) {
2062                    while (count > 0) {
2063                        handled = arrowScroll(FOCUS_UP);
2064                        count--;
2065                    }
2066                } else {
2067                    handled = fullScroll(FOCUS_UP);
2068                }
2069                break;
2070
2071            case KeyEvent.KEYCODE_DPAD_DOWN:
2072                if (!event.isAltPressed()) {
2073                    while (count > 0) {
2074                        handled = arrowScroll(FOCUS_DOWN);
2075                        count--;
2076                    }
2077                } else {
2078                    handled = fullScroll(FOCUS_DOWN);
2079                }
2080                break;
2081
2082            case KeyEvent.KEYCODE_DPAD_LEFT:
2083                handled = handleHorizontalFocusWithinListItem(View.FOCUS_LEFT);
2084                break;
2085            case KeyEvent.KEYCODE_DPAD_RIGHT:
2086                handled = handleHorizontalFocusWithinListItem(View.FOCUS_RIGHT);
2087                break;
2088
2089            case KeyEvent.KEYCODE_DPAD_CENTER:
2090            case KeyEvent.KEYCODE_ENTER:
2091                if (mItemCount > 0 && event.getRepeatCount() == 0) {
2092                    keyPressed();
2093                }
2094                handled = true;
2095                break;
2096
2097            case KeyEvent.KEYCODE_SPACE:
2098                if (mPopup == null || !mPopup.isShowing()) {
2099                    if (!event.isShiftPressed()) {
2100                        pageScroll(FOCUS_DOWN);
2101                    } else {
2102                        pageScroll(FOCUS_UP);
2103                    }
2104                    handled = true;
2105                }
2106                break;
2107            }
2108        }
2109
2110        if (!handled) {
2111            handled = sendToTextFilter(keyCode, count, event);
2112        }
2113
2114        if (handled) {
2115            return true;
2116        } else {
2117            switch (action) {
2118                case KeyEvent.ACTION_DOWN:
2119                    return super.onKeyDown(keyCode, event);
2120
2121                case KeyEvent.ACTION_UP:
2122                    return super.onKeyUp(keyCode, event);
2123
2124                case KeyEvent.ACTION_MULTIPLE:
2125                    return super.onKeyMultiple(keyCode, count, event);
2126
2127                default: // shouldn't happen
2128                    return false;
2129            }
2130        }
2131    }
2132
2133    /**
2134     * Scrolls up or down by the number of items currently present on screen.
2135     *
2136     * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN}
2137     * @return whether selection was moved
2138     */
2139    boolean pageScroll(int direction) {
2140        int nextPage = -1;
2141        boolean down = false;
2142
2143        if (direction == FOCUS_UP) {
2144            nextPage = Math.max(0, mSelectedPosition - getChildCount() - 1);
2145        } else if (direction == FOCUS_DOWN) {
2146            nextPage = Math.min(mItemCount - 1, mSelectedPosition + getChildCount() - 1);
2147            down = true;
2148        }
2149
2150        if (nextPage >= 0) {
2151            int position = lookForSelectablePosition(nextPage, down);
2152            if (position >= 0) {
2153                mLayoutMode = LAYOUT_SPECIFIC;
2154                mSpecificTop = mPaddingTop + getVerticalFadingEdgeLength();
2155
2156                if (down && position > mItemCount - getChildCount()) {
2157                    mLayoutMode = LAYOUT_FORCE_BOTTOM;
2158                }
2159
2160                if (!down && position < getChildCount()) {
2161                    mLayoutMode = LAYOUT_FORCE_TOP;
2162                }
2163
2164                setSelectionInt(position);
2165                invokeOnItemScrollListener();
2166                if (!awakenScrollBars()) {
2167                    invalidate();
2168                }
2169
2170                return true;
2171            }
2172        }
2173
2174        return false;
2175    }
2176
2177    /**
2178     * Go to the last or first item if possible (not worrying about panning across or navigating
2179     * within the internal focus of the currently selected item.)
2180     *
2181     * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN}
2182     *
2183     * @return whether selection was moved
2184     */
2185    boolean fullScroll(int direction) {
2186        boolean moved = false;
2187        if (direction == FOCUS_UP) {
2188            if (mSelectedPosition != 0) {
2189                int position = lookForSelectablePosition(0, true);
2190                if (position >= 0) {
2191                    mLayoutMode = LAYOUT_FORCE_TOP;
2192                    setSelectionInt(position);
2193                    invokeOnItemScrollListener();
2194                }
2195                moved = true;
2196            }
2197        } else if (direction == FOCUS_DOWN) {
2198            if (mSelectedPosition < mItemCount - 1) {
2199                int position = lookForSelectablePosition(mItemCount - 1, true);
2200                if (position >= 0) {
2201                    mLayoutMode = LAYOUT_FORCE_BOTTOM;
2202                    setSelectionInt(position);
2203                    invokeOnItemScrollListener();
2204                }
2205                moved = true;
2206            }
2207        }
2208
2209        if (moved && !awakenScrollBars()) {
2210            awakenScrollBars();
2211            invalidate();
2212        }
2213
2214        return moved;
2215    }
2216
2217    /**
2218     * To avoid horizontal focus searches changing the selected item, we
2219     * manually focus search within the selected item (as applicable), and
2220     * prevent focus from jumping to something within another item.
2221     * @param direction one of {View.FOCUS_LEFT, View.FOCUS_RIGHT}
2222     * @return Whether this consumes the key event.
2223     */
2224    private boolean handleHorizontalFocusWithinListItem(int direction) {
2225        if (direction != View.FOCUS_LEFT && direction != View.FOCUS_RIGHT)  {
2226            throw new IllegalArgumentException("direction must be one of"
2227                    + " {View.FOCUS_LEFT, View.FOCUS_RIGHT}");
2228        }
2229
2230        final int numChildren = getChildCount();
2231        if (mItemsCanFocus && numChildren > 0 && mSelectedPosition != INVALID_POSITION) {
2232            final View selectedView = getSelectedView();
2233            if (selectedView != null && selectedView.hasFocus() &&
2234                    selectedView instanceof ViewGroup) {
2235
2236                final View currentFocus = selectedView.findFocus();
2237                final View nextFocus = FocusFinder.getInstance().findNextFocus(
2238                        (ViewGroup) selectedView, currentFocus, direction);
2239                if (nextFocus != null) {
2240                    // do the math to get interesting rect in next focus' coordinates
2241                    currentFocus.getFocusedRect(mTempRect);
2242                    offsetDescendantRectToMyCoords(currentFocus, mTempRect);
2243                    offsetRectIntoDescendantCoords(nextFocus, mTempRect);
2244                    if (nextFocus.requestFocus(direction, mTempRect)) {
2245                        return true;
2246                    }
2247                }
2248                // we are blocking the key from being handled (by returning true)
2249                // if the global result is going to be some other view within this
2250                // list.  this is to acheive the overall goal of having
2251                // horizontal d-pad navigation remain in the current item.
2252                final View globalNextFocus = FocusFinder.getInstance().findNextFocus(
2253                        (ViewGroup) getRootView(), currentFocus, direction);
2254                if (globalNextFocus != null) {
2255                    return isViewAncestorOf(globalNextFocus, this);
2256                }
2257            }
2258        }
2259        return false;
2260    }
2261
2262    /**
2263     * Scrolls to the next or previous item if possible.
2264     *
2265     * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN}
2266     *
2267     * @return whether selection was moved
2268     */
2269    boolean arrowScroll(int direction) {
2270        try {
2271            mInLayout = true;
2272            final boolean handled = arrowScrollImpl(direction);
2273            if (handled) {
2274                playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction));
2275            }
2276            return handled;
2277        } finally {
2278            mInLayout = false;
2279        }
2280    }
2281
2282    /**
2283     * Handle an arrow scroll going up or down.  Take into account whether items are selectable,
2284     * whether there are focusable items etc.
2285     *
2286     * @param direction Either {@link android.view.View#FOCUS_UP} or {@link android.view.View#FOCUS_DOWN}.
2287     * @return Whether any scrolling, selection or focus change occured.
2288     */
2289    private boolean arrowScrollImpl(int direction) {
2290        if (getChildCount() <= 0) {
2291            return false;
2292        }
2293
2294        View selectedView = getSelectedView();
2295
2296        int nextSelectedPosition = lookForSelectablePositionOnScreen(direction);
2297        int amountToScroll = amountToScroll(direction, nextSelectedPosition);
2298
2299        // if we are moving focus, we may OVERRIDE the default behavior
2300        final ArrowScrollFocusResult focusResult = mItemsCanFocus ? arrowScrollFocused(direction) : null;
2301        if (focusResult != null) {
2302            nextSelectedPosition = focusResult.getSelectedPosition();
2303            amountToScroll = focusResult.getAmountToScroll();
2304        }
2305
2306        boolean needToRedraw = focusResult != null;
2307        if (nextSelectedPosition != INVALID_POSITION) {
2308            handleNewSelectionChange(selectedView, direction, nextSelectedPosition, focusResult != null);
2309            setSelectedPositionInt(nextSelectedPosition);
2310            setNextSelectedPositionInt(nextSelectedPosition);
2311            selectedView = getSelectedView();
2312            if (mItemsCanFocus && focusResult == null) {
2313                // there was no new view found to take focus, make sure we
2314                // don't leave focus with the old selection
2315                final View focused = getFocusedChild();
2316                if (focused != null) {
2317                    focused.clearFocus();
2318                }
2319            }
2320            needToRedraw = true;
2321            checkSelectionChanged();
2322        }
2323
2324        if (amountToScroll > 0) {
2325            scrollListItemsBy((direction == View.FOCUS_UP) ? amountToScroll : -amountToScroll);
2326            needToRedraw = true;
2327        }
2328
2329        // if we didn't find a new focusable, make sure any existing focused
2330        // item that was panned off screen gives up focus.
2331        if (mItemsCanFocus && (focusResult == null)
2332                && selectedView != null && selectedView.hasFocus()) {
2333            final View focused = selectedView.findFocus();
2334            if (distanceToView(focused) > 0) {
2335                focused.clearFocus();
2336            }
2337        }
2338
2339        // if  the current selection is panned off, we need to remove the selection
2340        if (nextSelectedPosition == INVALID_POSITION && selectedView != null
2341                && !isViewAncestorOf(selectedView, this)) {
2342            selectedView = null;
2343            hideSelector();
2344
2345            // but we don't want to set the ressurect position (that would make subsequent
2346            // unhandled key events bring back the item we just scrolled off!)
2347            mResurrectToPosition = INVALID_POSITION;
2348        }
2349
2350        if (needToRedraw) {
2351            if (selectedView != null) {
2352                positionSelector(selectedView);
2353                mSelectedTop = selectedView.getTop();
2354            }
2355            if (!awakenScrollBars()) {
2356                invalidate();
2357            }
2358            invokeOnItemScrollListener();
2359            return true;
2360        }
2361
2362        return false;
2363    }
2364
2365    /**
2366     * When selection changes, it is possible that the previously selected or the
2367     * next selected item will change its size.  If so, we need to offset some folks,
2368     * and re-layout the items as appropriate.
2369     *
2370     * @param selectedView The currently selected view (before changing selection).
2371     *   should be <code>null</code> if there was no previous selection.
2372     * @param direction Either {@link android.view.View#FOCUS_UP} or
2373     *        {@link android.view.View#FOCUS_DOWN}.
2374     * @param newSelectedPosition The position of the next selection.
2375     * @param newFocusAssigned whether new focus was assigned.  This matters because
2376     *        when something has focus, we don't want to show selection (ugh).
2377     */
2378    private void handleNewSelectionChange(View selectedView, int direction, int newSelectedPosition,
2379            boolean newFocusAssigned) {
2380        if (newSelectedPosition == INVALID_POSITION) {
2381            throw new IllegalArgumentException("newSelectedPosition needs to be valid");
2382        }
2383
2384        // whether or not we are moving down or up, we want to preserve the
2385        // top of whatever view is on top:
2386        // - moving down: the view that had selection
2387        // - moving up: the view that is getting selection
2388        View topView;
2389        View bottomView;
2390        int topViewIndex, bottomViewIndex;
2391        boolean topSelected = false;
2392        final int selectedIndex = mSelectedPosition - mFirstPosition;
2393        final int nextSelectedIndex = newSelectedPosition - mFirstPosition;
2394        if (direction == View.FOCUS_UP) {
2395            topViewIndex = nextSelectedIndex;
2396            bottomViewIndex = selectedIndex;
2397            topView = getChildAt(topViewIndex);
2398            bottomView = selectedView;
2399            topSelected = true;
2400        } else {
2401            topViewIndex = selectedIndex;
2402            bottomViewIndex = nextSelectedIndex;
2403            topView = selectedView;
2404            bottomView = getChildAt(bottomViewIndex);
2405        }
2406
2407        final int numChildren = getChildCount();
2408
2409        // start with top view: is it changing size?
2410        if (topView != null) {
2411            topView.setSelected(!newFocusAssigned && topSelected);
2412            measureAndAdjustDown(topView, topViewIndex, numChildren);
2413        }
2414
2415        // is the bottom view changing size?
2416        if (bottomView != null) {
2417            bottomView.setSelected(!newFocusAssigned && !topSelected);
2418            measureAndAdjustDown(bottomView, bottomViewIndex, numChildren);
2419        }
2420    }
2421
2422    /**
2423     * Re-measure a child, and if its height changes, lay it out preserving its
2424     * top, and adjust the children below it appropriately.
2425     * @param child The child
2426     * @param childIndex The view group index of the child.
2427     * @param numChildren The number of children in the view group.
2428     */
2429    private void measureAndAdjustDown(View child, int childIndex, int numChildren) {
2430        int oldHeight = child.getHeight();
2431        measureItem(child);
2432        if (child.getMeasuredHeight() != oldHeight) {
2433            // lay out the view, preserving its top
2434            relayoutMeasuredItem(child);
2435
2436            // adjust views below appropriately
2437            final int heightDelta = child.getMeasuredHeight() - oldHeight;
2438            for (int i = childIndex + 1; i < numChildren; i++) {
2439                getChildAt(i).offsetTopAndBottom(heightDelta);
2440            }
2441        }
2442    }
2443
2444    /**
2445     * Measure a particular list child.
2446     * TODO: unify with setUpChild.
2447     * @param child The child.
2448     */
2449    private void measureItem(View child) {
2450        ViewGroup.LayoutParams p = child.getLayoutParams();
2451        if (p == null) {
2452            p = new ViewGroup.LayoutParams(
2453                    ViewGroup.LayoutParams.MATCH_PARENT,
2454                    ViewGroup.LayoutParams.WRAP_CONTENT);
2455        }
2456
2457        int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
2458                mListPadding.left + mListPadding.right, p.width);
2459        int lpHeight = p.height;
2460        int childHeightSpec;
2461        if (lpHeight > 0) {
2462            childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
2463        } else {
2464            childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
2465        }
2466        child.measure(childWidthSpec, childHeightSpec);
2467    }
2468
2469    /**
2470     * Layout a child that has been measured, preserving its top position.
2471     * TODO: unify with setUpChild.
2472     * @param child The child.
2473     */
2474    private void relayoutMeasuredItem(View child) {
2475        final int w = child.getMeasuredWidth();
2476        final int h = child.getMeasuredHeight();
2477        final int childLeft = mListPadding.left;
2478        final int childRight = childLeft + w;
2479        final int childTop = child.getTop();
2480        final int childBottom = childTop + h;
2481        child.layout(childLeft, childTop, childRight, childBottom);
2482    }
2483
2484    /**
2485     * @return The amount to preview next items when arrow srolling.
2486     */
2487    private int getArrowScrollPreviewLength() {
2488        return Math.max(MIN_SCROLL_PREVIEW_PIXELS, getVerticalFadingEdgeLength());
2489    }
2490
2491    /**
2492     * Determine how much we need to scroll in order to get the next selected view
2493     * visible, with a fading edge showing below as applicable.  The amount is
2494     * capped at {@link #getMaxScrollAmount()} .
2495     *
2496     * @param direction either {@link android.view.View#FOCUS_UP} or
2497     *        {@link android.view.View#FOCUS_DOWN}.
2498     * @param nextSelectedPosition The position of the next selection, or
2499     *        {@link #INVALID_POSITION} if there is no next selectable position
2500     * @return The amount to scroll. Note: this is always positive!  Direction
2501     *         needs to be taken into account when actually scrolling.
2502     */
2503    private int amountToScroll(int direction, int nextSelectedPosition) {
2504        final int listBottom = getHeight() - mListPadding.bottom;
2505        final int listTop = mListPadding.top;
2506
2507        final int numChildren = getChildCount();
2508
2509        if (direction == View.FOCUS_DOWN) {
2510            int indexToMakeVisible = numChildren - 1;
2511            if (nextSelectedPosition != INVALID_POSITION) {
2512                indexToMakeVisible = nextSelectedPosition - mFirstPosition;
2513            }
2514
2515            final int positionToMakeVisible = mFirstPosition + indexToMakeVisible;
2516            final View viewToMakeVisible = getChildAt(indexToMakeVisible);
2517
2518            int goalBottom = listBottom;
2519            if (positionToMakeVisible < mItemCount - 1) {
2520                goalBottom -= getArrowScrollPreviewLength();
2521            }
2522
2523            if (viewToMakeVisible.getBottom() <= goalBottom) {
2524                // item is fully visible.
2525                return 0;
2526            }
2527
2528            if (nextSelectedPosition != INVALID_POSITION
2529                    && (goalBottom - viewToMakeVisible.getTop()) >= getMaxScrollAmount()) {
2530                // item already has enough of it visible, changing selection is good enough
2531                return 0;
2532            }
2533
2534            int amountToScroll = (viewToMakeVisible.getBottom() - goalBottom);
2535
2536            if ((mFirstPosition + numChildren) == mItemCount) {
2537                // last is last in list -> make sure we don't scroll past it
2538                final int max = getChildAt(numChildren - 1).getBottom() - listBottom;
2539                amountToScroll = Math.min(amountToScroll, max);
2540            }
2541
2542            return Math.min(amountToScroll, getMaxScrollAmount());
2543        } else {
2544            int indexToMakeVisible = 0;
2545            if (nextSelectedPosition != INVALID_POSITION) {
2546                indexToMakeVisible = nextSelectedPosition - mFirstPosition;
2547            }
2548            final int positionToMakeVisible = mFirstPosition + indexToMakeVisible;
2549            final View viewToMakeVisible = getChildAt(indexToMakeVisible);
2550            int goalTop = listTop;
2551            if (positionToMakeVisible > 0) {
2552                goalTop += getArrowScrollPreviewLength();
2553            }
2554            if (viewToMakeVisible.getTop() >= goalTop) {
2555                // item is fully visible.
2556                return 0;
2557            }
2558
2559            if (nextSelectedPosition != INVALID_POSITION &&
2560                    (viewToMakeVisible.getBottom() - goalTop) >= getMaxScrollAmount()) {
2561                // item already has enough of it visible, changing selection is good enough
2562                return 0;
2563            }
2564
2565            int amountToScroll = (goalTop - viewToMakeVisible.getTop());
2566            if (mFirstPosition == 0) {
2567                // first is first in list -> make sure we don't scroll past it
2568                final int max = listTop - getChildAt(0).getTop();
2569                amountToScroll = Math.min(amountToScroll,  max);
2570            }
2571            return Math.min(amountToScroll, getMaxScrollAmount());
2572        }
2573    }
2574
2575    /**
2576     * Holds results of focus aware arrow scrolling.
2577     */
2578    static private class ArrowScrollFocusResult {
2579        private int mSelectedPosition;
2580        private int mAmountToScroll;
2581
2582        /**
2583         * How {@link android.widget.ListView#arrowScrollFocused} returns its values.
2584         */
2585        void populate(int selectedPosition, int amountToScroll) {
2586            mSelectedPosition = selectedPosition;
2587            mAmountToScroll = amountToScroll;
2588        }
2589
2590        public int getSelectedPosition() {
2591            return mSelectedPosition;
2592        }
2593
2594        public int getAmountToScroll() {
2595            return mAmountToScroll;
2596        }
2597    }
2598
2599    /**
2600     * @param direction either {@link android.view.View#FOCUS_UP} or
2601     *        {@link android.view.View#FOCUS_DOWN}.
2602     * @return The position of the next selectable position of the views that
2603     *         are currently visible, taking into account the fact that there might
2604     *         be no selection.  Returns {@link #INVALID_POSITION} if there is no
2605     *         selectable view on screen in the given direction.
2606     */
2607    private int lookForSelectablePositionOnScreen(int direction) {
2608        final int firstPosition = mFirstPosition;
2609        if (direction == View.FOCUS_DOWN) {
2610            int startPos = (mSelectedPosition != INVALID_POSITION) ?
2611                    mSelectedPosition + 1 :
2612                    firstPosition;
2613            if (startPos >= mAdapter.getCount()) {
2614                return INVALID_POSITION;
2615            }
2616            if (startPos < firstPosition) {
2617                startPos = firstPosition;
2618            }
2619
2620            final int lastVisiblePos = getLastVisiblePosition();
2621            final ListAdapter adapter = getAdapter();
2622            for (int pos = startPos; pos <= lastVisiblePos; pos++) {
2623                if (adapter.isEnabled(pos)
2624                        && getChildAt(pos - firstPosition).getVisibility() == View.VISIBLE) {
2625                    return pos;
2626                }
2627            }
2628        } else {
2629            int last = firstPosition + getChildCount() - 1;
2630            int startPos = (mSelectedPosition != INVALID_POSITION) ?
2631                    mSelectedPosition - 1 :
2632                    firstPosition + getChildCount() - 1;
2633            if (startPos < 0) {
2634                return INVALID_POSITION;
2635            }
2636            if (startPos > last) {
2637                startPos = last;
2638            }
2639
2640            final ListAdapter adapter = getAdapter();
2641            for (int pos = startPos; pos >= firstPosition; pos--) {
2642                if (adapter.isEnabled(pos)
2643                        && getChildAt(pos - firstPosition).getVisibility() == View.VISIBLE) {
2644                    return pos;
2645                }
2646            }
2647        }
2648        return INVALID_POSITION;
2649    }
2650
2651    /**
2652     * Do an arrow scroll based on focus searching.  If a new view is
2653     * given focus, return the selection delta and amount to scroll via
2654     * an {@link ArrowScrollFocusResult}, otherwise, return null.
2655     *
2656     * @param direction either {@link android.view.View#FOCUS_UP} or
2657     *        {@link android.view.View#FOCUS_DOWN}.
2658     * @return The result if focus has changed, or <code>null</code>.
2659     */
2660    private ArrowScrollFocusResult arrowScrollFocused(final int direction) {
2661        final View selectedView = getSelectedView();
2662        View newFocus;
2663        if (selectedView != null && selectedView.hasFocus()) {
2664            View oldFocus = selectedView.findFocus();
2665            newFocus = FocusFinder.getInstance().findNextFocus(this, oldFocus, direction);
2666        } else {
2667            if (direction == View.FOCUS_DOWN) {
2668                final boolean topFadingEdgeShowing = (mFirstPosition > 0);
2669                final int listTop = mListPadding.top +
2670                        (topFadingEdgeShowing ? getArrowScrollPreviewLength() : 0);
2671                final int ySearchPoint =
2672                        (selectedView != null && selectedView.getTop() > listTop) ?
2673                                selectedView.getTop() :
2674                                listTop;
2675                mTempRect.set(0, ySearchPoint, 0, ySearchPoint);
2676            } else {
2677                final boolean bottomFadingEdgeShowing =
2678                        (mFirstPosition + getChildCount() - 1) < mItemCount;
2679                final int listBottom = getHeight() - mListPadding.bottom -
2680                        (bottomFadingEdgeShowing ? getArrowScrollPreviewLength() : 0);
2681                final int ySearchPoint =
2682                        (selectedView != null && selectedView.getBottom() < listBottom) ?
2683                                selectedView.getBottom() :
2684                                listBottom;
2685                mTempRect.set(0, ySearchPoint, 0, ySearchPoint);
2686            }
2687            newFocus = FocusFinder.getInstance().findNextFocusFromRect(this, mTempRect, direction);
2688        }
2689
2690        if (newFocus != null) {
2691            final int positionOfNewFocus = positionOfNewFocus(newFocus);
2692
2693            // if the focus change is in a different new position, make sure
2694            // we aren't jumping over another selectable position
2695            if (mSelectedPosition != INVALID_POSITION && positionOfNewFocus != mSelectedPosition) {
2696                final int selectablePosition = lookForSelectablePositionOnScreen(direction);
2697                if (selectablePosition != INVALID_POSITION &&
2698                        ((direction == View.FOCUS_DOWN && selectablePosition < positionOfNewFocus) ||
2699                        (direction == View.FOCUS_UP && selectablePosition > positionOfNewFocus))) {
2700                    return null;
2701                }
2702            }
2703
2704            int focusScroll = amountToScrollToNewFocus(direction, newFocus, positionOfNewFocus);
2705
2706            final int maxScrollAmount = getMaxScrollAmount();
2707            if (focusScroll < maxScrollAmount) {
2708                // not moving too far, safe to give next view focus
2709                newFocus.requestFocus(direction);
2710                mArrowScrollFocusResult.populate(positionOfNewFocus, focusScroll);
2711                return mArrowScrollFocusResult;
2712            } else if (distanceToView(newFocus) < maxScrollAmount){
2713                // Case to consider:
2714                // too far to get entire next focusable on screen, but by going
2715                // max scroll amount, we are getting it at least partially in view,
2716                // so give it focus and scroll the max ammount.
2717                newFocus.requestFocus(direction);
2718                mArrowScrollFocusResult.populate(positionOfNewFocus, maxScrollAmount);
2719                return mArrowScrollFocusResult;
2720            }
2721        }
2722        return null;
2723    }
2724
2725    /**
2726     * @param newFocus The view that would have focus.
2727     * @return the position that contains newFocus
2728     */
2729    private int positionOfNewFocus(View newFocus) {
2730        final int numChildren = getChildCount();
2731        for (int i = 0; i < numChildren; i++) {
2732            final View child = getChildAt(i);
2733            if (isViewAncestorOf(newFocus, child)) {
2734                return mFirstPosition + i;
2735            }
2736        }
2737        throw new IllegalArgumentException("newFocus is not a child of any of the"
2738                + " children of the list!");
2739    }
2740
2741    /**
2742     * Return true if child is an ancestor of parent, (or equal to the parent).
2743     */
2744    private boolean isViewAncestorOf(View child, View parent) {
2745        if (child == parent) {
2746            return true;
2747        }
2748
2749        final ViewParent theParent = child.getParent();
2750        return (theParent instanceof ViewGroup) && isViewAncestorOf((View) theParent, parent);
2751    }
2752
2753    /**
2754     * Determine how much we need to scroll in order to get newFocus in view.
2755     * @param direction either {@link android.view.View#FOCUS_UP} or
2756     *        {@link android.view.View#FOCUS_DOWN}.
2757     * @param newFocus The view that would take focus.
2758     * @param positionOfNewFocus The position of the list item containing newFocus
2759     * @return The amount to scroll.  Note: this is always positive!  Direction
2760     *   needs to be taken into account when actually scrolling.
2761     */
2762    private int amountToScrollToNewFocus(int direction, View newFocus, int positionOfNewFocus) {
2763        int amountToScroll = 0;
2764        newFocus.getDrawingRect(mTempRect);
2765        offsetDescendantRectToMyCoords(newFocus, mTempRect);
2766        if (direction == View.FOCUS_UP) {
2767            if (mTempRect.top < mListPadding.top) {
2768                amountToScroll = mListPadding.top - mTempRect.top;
2769                if (positionOfNewFocus > 0) {
2770                    amountToScroll += getArrowScrollPreviewLength();
2771                }
2772            }
2773        } else {
2774            final int listBottom = getHeight() - mListPadding.bottom;
2775            if (mTempRect.bottom > listBottom) {
2776                amountToScroll = mTempRect.bottom - listBottom;
2777                if (positionOfNewFocus < mItemCount - 1) {
2778                    amountToScroll += getArrowScrollPreviewLength();
2779                }
2780            }
2781        }
2782        return amountToScroll;
2783    }
2784
2785    /**
2786     * Determine the distance to the nearest edge of a view in a particular
2787     * direction.
2788     *
2789     * @param descendant A descendant of this list.
2790     * @return The distance, or 0 if the nearest edge is already on screen.
2791     */
2792    private int distanceToView(View descendant) {
2793        int distance = 0;
2794        descendant.getDrawingRect(mTempRect);
2795        offsetDescendantRectToMyCoords(descendant, mTempRect);
2796        final int listBottom = mBottom - mTop - mListPadding.bottom;
2797        if (mTempRect.bottom < mListPadding.top) {
2798            distance = mListPadding.top - mTempRect.bottom;
2799        } else if (mTempRect.top > listBottom) {
2800            distance = mTempRect.top - listBottom;
2801        }
2802        return distance;
2803    }
2804
2805
2806    /**
2807     * Scroll the children by amount, adding a view at the end and removing
2808     * views that fall off as necessary.
2809     *
2810     * @param amount The amount (positive or negative) to scroll.
2811     */
2812    private void scrollListItemsBy(int amount) {
2813        offsetChildrenTopAndBottom(amount);
2814
2815        final int listBottom = getHeight() - mListPadding.bottom;
2816        final int listTop = mListPadding.top;
2817        final AbsListView.RecycleBin recycleBin = mRecycler;
2818
2819        if (amount < 0) {
2820            // shifted items up
2821
2822            // may need to pan views into the bottom space
2823            int numChildren = getChildCount();
2824            View last = getChildAt(numChildren - 1);
2825            while (last.getBottom() < listBottom) {
2826                final int lastVisiblePosition = mFirstPosition + numChildren - 1;
2827                if (lastVisiblePosition < mItemCount - 1) {
2828                    last = addViewBelow(last, lastVisiblePosition);
2829                    numChildren++;
2830                } else {
2831                    break;
2832                }
2833            }
2834
2835            // may have brought in the last child of the list that is skinnier
2836            // than the fading edge, thereby leaving space at the end.  need
2837            // to shift back
2838            if (last.getBottom() < listBottom) {
2839                offsetChildrenTopAndBottom(listBottom - last.getBottom());
2840            }
2841
2842            // top views may be panned off screen
2843            View first = getChildAt(0);
2844            while (first.getBottom() < listTop) {
2845                AbsListView.LayoutParams layoutParams = (LayoutParams) first.getLayoutParams();
2846                if (recycleBin.shouldRecycleViewType(layoutParams.viewType)) {
2847                    detachViewFromParent(first);
2848                    recycleBin.addScrapView(first);
2849                } else {
2850                    removeViewInLayout(first);
2851                }
2852                first = getChildAt(0);
2853                mFirstPosition++;
2854            }
2855        } else {
2856            // shifted items down
2857            View first = getChildAt(0);
2858
2859            // may need to pan views into top
2860            while ((first.getTop() > listTop) && (mFirstPosition > 0)) {
2861                first = addViewAbove(first, mFirstPosition);
2862                mFirstPosition--;
2863            }
2864
2865            // may have brought the very first child of the list in too far and
2866            // need to shift it back
2867            if (first.getTop() > listTop) {
2868                offsetChildrenTopAndBottom(listTop - first.getTop());
2869            }
2870
2871            int lastIndex = getChildCount() - 1;
2872            View last = getChildAt(lastIndex);
2873
2874            // bottom view may be panned off screen
2875            while (last.getTop() > listBottom) {
2876                AbsListView.LayoutParams layoutParams = (LayoutParams) last.getLayoutParams();
2877                if (recycleBin.shouldRecycleViewType(layoutParams.viewType)) {
2878                    detachViewFromParent(last);
2879                    recycleBin.addScrapView(last);
2880                } else {
2881                    removeViewInLayout(last);
2882                }
2883                last = getChildAt(--lastIndex);
2884            }
2885        }
2886    }
2887
2888    private View addViewAbove(View theView, int position) {
2889        int abovePosition = position - 1;
2890        View view = obtainView(abovePosition, mIsScrap);
2891        int edgeOfNewChild = theView.getTop() - mDividerHeight;
2892        setupChild(view, abovePosition, edgeOfNewChild, false, mListPadding.left,
2893                false, mIsScrap[0]);
2894        return view;
2895    }
2896
2897    private View addViewBelow(View theView, int position) {
2898        int belowPosition = position + 1;
2899        View view = obtainView(belowPosition, mIsScrap);
2900        int edgeOfNewChild = theView.getBottom() + mDividerHeight;
2901        setupChild(view, belowPosition, edgeOfNewChild, true, mListPadding.left,
2902                false, mIsScrap[0]);
2903        return view;
2904    }
2905
2906    /**
2907     * Indicates that the views created by the ListAdapter can contain focusable
2908     * items.
2909     *
2910     * @param itemsCanFocus true if items can get focus, false otherwise
2911     */
2912    public void setItemsCanFocus(boolean itemsCanFocus) {
2913        mItemsCanFocus = itemsCanFocus;
2914        if (!itemsCanFocus) {
2915            setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
2916        }
2917    }
2918
2919    /**
2920     * @return Whether the views created by the ListAdapter can contain focusable
2921     * items.
2922     */
2923    public boolean getItemsCanFocus() {
2924        return mItemsCanFocus;
2925    }
2926
2927    /**
2928     * @hide Pending API council approval.
2929     */
2930    @Override
2931    public boolean isOpaque() {
2932        return (mCachingStarted && mIsCacheColorOpaque && mDividerIsOpaque &&
2933                hasOpaqueScrollbars()) || super.isOpaque();
2934    }
2935
2936    @Override
2937    public void setCacheColorHint(int color) {
2938        final boolean opaque = (color >>> 24) == 0xFF;
2939        mIsCacheColorOpaque = opaque;
2940        if (opaque) {
2941            if (mDividerPaint == null) {
2942                mDividerPaint = new Paint();
2943            }
2944            mDividerPaint.setColor(color);
2945        }
2946        super.setCacheColorHint(color);
2947    }
2948
2949    @Override
2950    protected void dispatchDraw(Canvas canvas) {
2951        // Draw the dividers
2952        final int dividerHeight = mDividerHeight;
2953        final boolean drawDividers = dividerHeight > 0 && mDivider != null;
2954
2955        if (drawDividers) {
2956            // Only modify the top and bottom in the loop, we set the left and right here
2957            final Rect bounds = mTempRect;
2958            bounds.left = mPaddingLeft;
2959            bounds.right = mRight - mLeft - mPaddingRight;
2960
2961            final int count = getChildCount();
2962            final int headerCount = mHeaderViewInfos.size();
2963            final int itemCount = mItemCount;
2964            final int footerLimit = itemCount - mFooterViewInfos.size() - 1;
2965            final boolean headerDividers = mHeaderDividersEnabled;
2966            final boolean footerDividers = mFooterDividersEnabled;
2967            final int first = mFirstPosition;
2968            final boolean areAllItemsSelectable = mAreAllItemsSelectable;
2969            final ListAdapter adapter = mAdapter;
2970            // If the list is opaque *and* the background is not, we want to
2971            // fill a rect where the dividers would be for non-selectable items
2972            // If the list is opaque and the background is also opaque, we don't
2973            // need to draw anything since the background will do it for us
2974            final boolean fillForMissingDividers = drawDividers && isOpaque() && !super.isOpaque();
2975
2976            if (fillForMissingDividers && mDividerPaint == null && mIsCacheColorOpaque) {
2977                mDividerPaint = new Paint();
2978                mDividerPaint.setColor(getCacheColorHint());
2979            }
2980            final Paint paint = mDividerPaint;
2981
2982            final int listBottom = mBottom - mTop - mListPadding.bottom + mScrollY;
2983            if (!mStackFromBottom) {
2984                int bottom = 0;
2985
2986                final int scrollY = mScrollY;
2987                for (int i = 0; i < count; i++) {
2988                    if ((headerDividers || first + i >= headerCount) &&
2989                            (footerDividers || first + i < footerLimit)) {
2990                        View child = getChildAt(i);
2991                        bottom = child.getBottom();
2992                        // Don't draw dividers next to items that are not enabled
2993                        if (drawDividers) {
2994                            if ((areAllItemsSelectable ||
2995                                    (adapter.isEnabled(first + i) && (i == count - 1 ||
2996                                            adapter.isEnabled(first + i + 1))))) {
2997                                bounds.top = bottom;
2998                                bounds.bottom = bottom + dividerHeight;
2999                                drawDivider(canvas, bounds, i);
3000                            } else if (fillForMissingDividers) {
3001                                bounds.top = bottom;
3002                                bounds.bottom = bottom + dividerHeight;
3003                                canvas.drawRect(bounds, paint);
3004                            }
3005                        }
3006                    }
3007                }
3008            } else {
3009                int top;
3010                int listTop = mListPadding.top;
3011
3012                final int scrollY = mScrollY;
3013
3014                for (int i = 0; i < count; i++) {
3015                    if ((headerDividers || first + i >= headerCount) &&
3016                            (footerDividers || first + i < footerLimit)) {
3017                        View child = getChildAt(i);
3018                        top = child.getTop();
3019                        // Don't draw dividers next to items that are not enabled
3020                        if (drawDividers && top > listTop) {
3021                            if ((areAllItemsSelectable ||
3022                                    (adapter.isEnabled(first + i) && (i == count - 1 ||
3023                                            adapter.isEnabled(first + i + 1))))) {
3024                                bounds.top = top - dividerHeight;
3025                                bounds.bottom = top;
3026                                // Give the method the child ABOVE the divider, so we
3027                                // subtract one from our child
3028                                // position. Give -1 when there is no child above the
3029                                // divider.
3030                                drawDivider(canvas, bounds, i - 1);
3031                            } else if (fillForMissingDividers) {
3032                                bounds.top = top - dividerHeight;
3033                                bounds.bottom = top;
3034                                canvas.drawRect(bounds, paint);
3035                            }
3036                        }
3037                    }
3038                }
3039
3040                if (count > 0 && scrollY > 0 && drawDividers) {
3041                    bounds.top = listBottom;
3042                    bounds.bottom = listBottom + dividerHeight;
3043                    drawDivider(canvas, bounds, -1);
3044                }
3045            }
3046        }
3047
3048        // Draw the indicators (these should be drawn above the dividers) and children
3049        super.dispatchDraw(canvas);
3050    }
3051
3052    /**
3053     * Draws a divider for the given child in the given bounds.
3054     *
3055     * @param canvas The canvas to draw to.
3056     * @param bounds The bounds of the divider.
3057     * @param childIndex The index of child (of the View) above the divider.
3058     *            This will be -1 if there is no child above the divider to be
3059     *            drawn.
3060     */
3061    void drawDivider(Canvas canvas, Rect bounds, int childIndex) {
3062        // This widget draws the same divider for all children
3063        final Drawable divider = mDivider;
3064        final boolean clipDivider = mClipDivider;
3065
3066        if (!clipDivider) {
3067            divider.setBounds(bounds);
3068        } else {
3069            canvas.save();
3070            canvas.clipRect(bounds);
3071        }
3072
3073        divider.draw(canvas);
3074
3075        if (clipDivider) {
3076            canvas.restore();
3077        }
3078    }
3079
3080    /**
3081     * Returns the drawable that will be drawn between each item in the list.
3082     *
3083     * @return the current drawable drawn between list elements
3084     */
3085    public Drawable getDivider() {
3086        return mDivider;
3087    }
3088
3089    /**
3090     * Sets the drawable that will be drawn between each item in the list. If the drawable does
3091     * not have an intrinsic height, you should also call {@link #setDividerHeight(int)}
3092     *
3093     * @param divider The drawable to use.
3094     */
3095    public void setDivider(Drawable divider) {
3096        if (divider != null) {
3097            mDividerHeight = divider.getIntrinsicHeight();
3098            mClipDivider = divider instanceof ColorDrawable;
3099        } else {
3100            mDividerHeight = 0;
3101            mClipDivider = false;
3102        }
3103        mDivider = divider;
3104        mDividerIsOpaque = divider == null || divider.getOpacity() == PixelFormat.OPAQUE;
3105        requestLayoutIfNecessary();
3106    }
3107
3108    /**
3109     * @return Returns the height of the divider that will be drawn between each item in the list.
3110     */
3111    public int getDividerHeight() {
3112        return mDividerHeight;
3113    }
3114
3115    /**
3116     * Sets the height of the divider that will be drawn between each item in the list. Calling
3117     * this will override the intrinsic height as set by {@link #setDivider(Drawable)}
3118     *
3119     * @param height The new height of the divider in pixels.
3120     */
3121    public void setDividerHeight(int height) {
3122        mDividerHeight = height;
3123        requestLayoutIfNecessary();
3124    }
3125
3126    /**
3127     * Enables or disables the drawing of the divider for header views.
3128     *
3129     * @param headerDividersEnabled True to draw the headers, false otherwise.
3130     *
3131     * @see #setFooterDividersEnabled(boolean)
3132     * @see #addHeaderView(android.view.View)
3133     */
3134    public void setHeaderDividersEnabled(boolean headerDividersEnabled) {
3135        mHeaderDividersEnabled = headerDividersEnabled;
3136        invalidate();
3137    }
3138
3139    /**
3140     * Enables or disables the drawing of the divider for footer views.
3141     *
3142     * @param footerDividersEnabled True to draw the footers, false otherwise.
3143     *
3144     * @see #setHeaderDividersEnabled(boolean)
3145     * @see #addFooterView(android.view.View)
3146     */
3147    public void setFooterDividersEnabled(boolean footerDividersEnabled) {
3148        mFooterDividersEnabled = footerDividersEnabled;
3149        invalidate();
3150    }
3151
3152    @Override
3153    protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
3154        super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
3155
3156        int closetChildIndex = -1;
3157        if (gainFocus && previouslyFocusedRect != null) {
3158            previouslyFocusedRect.offset(mScrollX, mScrollY);
3159
3160            final ListAdapter adapter = mAdapter;
3161            // Don't cache the result of getChildCount or mFirstPosition here,
3162            // it could change in layoutChildren.
3163            if (adapter.getCount() < getChildCount() + mFirstPosition) {
3164                mLayoutMode = LAYOUT_NORMAL;
3165                layoutChildren();
3166            }
3167
3168            // figure out which item should be selected based on previously
3169            // focused rect
3170            Rect otherRect = mTempRect;
3171            int minDistance = Integer.MAX_VALUE;
3172            final int childCount = getChildCount();
3173            final int firstPosition = mFirstPosition;
3174
3175            for (int i = 0; i < childCount; i++) {
3176                // only consider selectable views
3177                if (!adapter.isEnabled(firstPosition + i)) {
3178                    continue;
3179                }
3180
3181                View other = getChildAt(i);
3182                other.getDrawingRect(otherRect);
3183                offsetDescendantRectToMyCoords(other, otherRect);
3184                int distance = getDistance(previouslyFocusedRect, otherRect, direction);
3185
3186                if (distance < minDistance) {
3187                    minDistance = distance;
3188                    closetChildIndex = i;
3189                }
3190            }
3191        }
3192
3193        if (closetChildIndex >= 0) {
3194            setSelection(closetChildIndex + mFirstPosition);
3195        } else {
3196            requestLayout();
3197        }
3198    }
3199
3200
3201    /*
3202     * (non-Javadoc)
3203     *
3204     * Children specified in XML are assumed to be header views. After we have
3205     * parsed them move them out of the children list and into mHeaderViews.
3206     */
3207    @Override
3208    protected void onFinishInflate() {
3209        super.onFinishInflate();
3210
3211        int count = getChildCount();
3212        if (count > 0) {
3213            for (int i = 0; i < count; ++i) {
3214                addHeaderView(getChildAt(i));
3215            }
3216            removeAllViews();
3217        }
3218    }
3219
3220    /* (non-Javadoc)
3221     * @see android.view.View#findViewById(int)
3222     * First look in our children, then in any header and footer views that may be scrolled off.
3223     */
3224    @Override
3225    protected View findViewTraversal(int id) {
3226        View v;
3227        v = super.findViewTraversal(id);
3228        if (v == null) {
3229            v = findViewInHeadersOrFooters(mHeaderViewInfos, id);
3230            if (v != null) {
3231                return v;
3232            }
3233            v = findViewInHeadersOrFooters(mFooterViewInfos, id);
3234            if (v != null) {
3235                return v;
3236            }
3237        }
3238        return v;
3239    }
3240
3241    /* (non-Javadoc)
3242     *
3243     * Look in the passed in list of headers or footers for the view.
3244     */
3245    View findViewInHeadersOrFooters(ArrayList<FixedViewInfo> where, int id) {
3246        if (where != null) {
3247            int len = where.size();
3248            View v;
3249
3250            for (int i = 0; i < len; i++) {
3251                v = where.get(i).view;
3252
3253                if (!v.isRootNamespace()) {
3254                    v = v.findViewById(id);
3255
3256                    if (v != null) {
3257                        return v;
3258                    }
3259                }
3260            }
3261        }
3262        return null;
3263    }
3264
3265    /* (non-Javadoc)
3266     * @see android.view.View#findViewWithTag(String)
3267     * First look in our children, then in any header and footer views that may be scrolled off.
3268     */
3269    @Override
3270    protected View findViewWithTagTraversal(Object tag) {
3271        View v;
3272        v = super.findViewWithTagTraversal(tag);
3273        if (v == null) {
3274            v = findViewTagInHeadersOrFooters(mHeaderViewInfos, tag);
3275            if (v != null) {
3276                return v;
3277            }
3278
3279            v = findViewTagInHeadersOrFooters(mFooterViewInfos, tag);
3280            if (v != null) {
3281                return v;
3282            }
3283        }
3284        return v;
3285    }
3286
3287    /* (non-Javadoc)
3288     *
3289     * Look in the passed in list of headers or footers for the view with the tag.
3290     */
3291    View findViewTagInHeadersOrFooters(ArrayList<FixedViewInfo> where, Object tag) {
3292        if (where != null) {
3293            int len = where.size();
3294            View v;
3295
3296            for (int i = 0; i < len; i++) {
3297                v = where.get(i).view;
3298
3299                if (!v.isRootNamespace()) {
3300                    v = v.findViewWithTag(tag);
3301
3302                    if (v != null) {
3303                        return v;
3304                    }
3305                }
3306            }
3307        }
3308        return null;
3309    }
3310
3311    @Override
3312    public boolean onTouchEvent(MotionEvent ev) {
3313        if (mItemsCanFocus && ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) {
3314            // Don't handle edge touches immediately -- they may actually belong to one of our
3315            // descendants.
3316            return false;
3317        }
3318        return super.onTouchEvent(ev);
3319    }
3320
3321    /**
3322     * @see #setChoiceMode(int)
3323     *
3324     * @return The current choice mode
3325     */
3326    public int getChoiceMode() {
3327        return mChoiceMode;
3328    }
3329
3330    /**
3331     * Defines the choice behavior for the List. By default, Lists do not have any choice behavior
3332     * ({@link #CHOICE_MODE_NONE}). By setting the choiceMode to {@link #CHOICE_MODE_SINGLE}, the
3333     * List allows up to one item to  be in a chosen state. By setting the choiceMode to
3334     * {@link #CHOICE_MODE_MULTIPLE}, the list allows any number of items to be chosen.
3335     *
3336     * @param choiceMode One of {@link #CHOICE_MODE_NONE}, {@link #CHOICE_MODE_SINGLE}, or
3337     * {@link #CHOICE_MODE_MULTIPLE}
3338     */
3339    public void setChoiceMode(int choiceMode) {
3340        mChoiceMode = choiceMode;
3341        if (mChoiceMode != CHOICE_MODE_NONE) {
3342            if (mCheckStates == null) {
3343                mCheckStates = new SparseBooleanArray();
3344            }
3345            if (mCheckedIdStates == null && mAdapter != null && mAdapter.hasStableIds()) {
3346                mCheckedIdStates = new LongSparseArray<Boolean>();
3347            }
3348        }
3349    }
3350
3351    @Override
3352    public boolean performItemClick(View view, int position, long id) {
3353        boolean handled = false;
3354
3355        if (mChoiceMode != CHOICE_MODE_NONE) {
3356            handled = true;
3357
3358            if (mChoiceMode == CHOICE_MODE_MULTIPLE) {
3359                boolean newValue = !mCheckStates.get(position, false);
3360                mCheckStates.put(position, newValue);
3361                if (mCheckedIdStates != null && mAdapter.hasStableIds()) {
3362                    if (newValue) {
3363                        mCheckedIdStates.put(mAdapter.getItemId(position), Boolean.TRUE);
3364                    } else {
3365                        mCheckedIdStates.delete(mAdapter.getItemId(position));
3366                    }
3367                }
3368            } else {
3369                boolean newValue = !mCheckStates.get(position, false);
3370                if (newValue) {
3371                    mCheckStates.clear();
3372                    mCheckStates.put(position, true);
3373                    if (mCheckedIdStates != null && mAdapter.hasStableIds()) {
3374                        mCheckedIdStates.clear();
3375                        mCheckedIdStates.put(mAdapter.getItemId(position), Boolean.TRUE);
3376                    }
3377                }
3378            }
3379
3380            mDataChanged = true;
3381            rememberSyncState();
3382            requestLayout();
3383        }
3384
3385        handled |= super.performItemClick(view, position, id);
3386
3387        return handled;
3388    }
3389
3390    /**
3391     * Sets the checked state of the specified position. The is only valid if
3392     * the choice mode has been set to {@link #CHOICE_MODE_SINGLE} or
3393     * {@link #CHOICE_MODE_MULTIPLE}.
3394     *
3395     * @param position The item whose checked state is to be checked
3396     * @param value The new checked state for the item
3397     */
3398    public void setItemChecked(int position, boolean value) {
3399        if (mChoiceMode == CHOICE_MODE_NONE) {
3400            return;
3401        }
3402
3403        if (mChoiceMode == CHOICE_MODE_MULTIPLE) {
3404            mCheckStates.put(position, value);
3405            if (mCheckedIdStates != null && mAdapter.hasStableIds()) {
3406                if (value) {
3407                    mCheckedIdStates.put(mAdapter.getItemId(position), Boolean.TRUE);
3408                } else {
3409                    mCheckedIdStates.delete(mAdapter.getItemId(position));
3410                }
3411            }
3412        } else {
3413            boolean updateIds = mCheckedIdStates != null && mAdapter.hasStableIds();
3414            // Clear all values if we're checking something, or unchecking the currently
3415            // selected item
3416            if (value || isItemChecked(position)) {
3417                mCheckStates.clear();
3418                if (updateIds) {
3419                    mCheckedIdStates.clear();
3420                }
3421            }
3422            // this may end up selecting the value we just cleared but this way
3423            // we ensure length of mCheckStates is 1, a fact getCheckedItemPosition relies on
3424            if (value) {
3425                mCheckStates.put(position, true);
3426                if (updateIds) {
3427                    mCheckedIdStates.put(mAdapter.getItemId(position), Boolean.TRUE);
3428                }
3429            }
3430        }
3431
3432        // Do not generate a data change while we are in the layout phase
3433        if (!mInLayout && !mBlockLayoutRequests) {
3434            mDataChanged = true;
3435            rememberSyncState();
3436            requestLayout();
3437        }
3438    }
3439
3440    /**
3441     * Returns the checked state of the specified position. The result is only
3442     * valid if the choice mode has been set to {@link #CHOICE_MODE_SINGLE}
3443     * or {@link #CHOICE_MODE_MULTIPLE}.
3444     *
3445     * @param position The item whose checked state to return
3446     * @return The item's checked state or <code>false</code> if choice mode
3447     *         is invalid
3448     *
3449     * @see #setChoiceMode(int)
3450     */
3451    public boolean isItemChecked(int position) {
3452        if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null) {
3453            return mCheckStates.get(position);
3454        }
3455
3456        return false;
3457    }
3458
3459    /**
3460     * Returns the currently checked item. The result is only valid if the choice
3461     * mode has been set to {@link #CHOICE_MODE_SINGLE}.
3462     *
3463     * @return The position of the currently checked item or
3464     *         {@link #INVALID_POSITION} if nothing is selected
3465     *
3466     * @see #setChoiceMode(int)
3467     */
3468    public int getCheckedItemPosition() {
3469        if (mChoiceMode == CHOICE_MODE_SINGLE && mCheckStates != null && mCheckStates.size() == 1) {
3470            return mCheckStates.keyAt(0);
3471        }
3472
3473        return INVALID_POSITION;
3474    }
3475
3476    /**
3477     * Returns the set of checked items in the list. The result is only valid if
3478     * the choice mode has not been set to {@link #CHOICE_MODE_NONE}.
3479     *
3480     * @return  A SparseBooleanArray which will return true for each call to
3481     *          get(int position) where position is a position in the list,
3482     *          or <code>null</code> if the choice mode is set to
3483     *          {@link #CHOICE_MODE_NONE}.
3484     */
3485    public SparseBooleanArray getCheckedItemPositions() {
3486        if (mChoiceMode != CHOICE_MODE_NONE) {
3487            return mCheckStates;
3488        }
3489        return null;
3490    }
3491
3492    /**
3493     * Returns the set of checked items ids. The result is only valid if the
3494     * choice mode has not been set to {@link #CHOICE_MODE_NONE}.
3495     *
3496     * @return A new array which contains the id of each checked item in the
3497     *         list.
3498     *
3499     * @deprecated Use {@link #getCheckedItemIds()} instead.
3500     */
3501    public long[] getCheckItemIds() {
3502        // Use new behavior that correctly handles stable ID mapping.
3503        if (mAdapter != null && mAdapter.hasStableIds()) {
3504            return getCheckedItemIds();
3505        }
3506
3507        // Old behavior was buggy, but would sort of work for adapters without stable IDs.
3508        // Fall back to it to support legacy apps.
3509        if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null && mAdapter != null) {
3510            final SparseBooleanArray states = mCheckStates;
3511            final int count = states.size();
3512            final long[] ids = new long[count];
3513            final ListAdapter adapter = mAdapter;
3514
3515            int checkedCount = 0;
3516            for (int i = 0; i < count; i++) {
3517                if (states.valueAt(i)) {
3518                    ids[checkedCount++] = adapter.getItemId(states.keyAt(i));
3519                }
3520            }
3521
3522            // Trim array if needed. mCheckStates may contain false values
3523            // resulting in checkedCount being smaller than count.
3524            if (checkedCount == count) {
3525                return ids;
3526            } else {
3527                final long[] result = new long[checkedCount];
3528                System.arraycopy(ids, 0, result, 0, checkedCount);
3529
3530                return result;
3531            }
3532        }
3533        return new long[0];
3534    }
3535
3536    /**
3537     * Returns the set of checked items ids. The result is only valid if the
3538     * choice mode has not been set to {@link #CHOICE_MODE_NONE} and the adapter
3539     * has stable IDs. ({@link ListAdapter#hasStableIds()} == {@code true})
3540     *
3541     * @return A new array which contains the id of each checked item in the
3542     *         list.
3543     */
3544    public long[] getCheckedItemIds() {
3545        if (mChoiceMode == CHOICE_MODE_NONE || mCheckedIdStates == null || mAdapter == null) {
3546            return new long[0];
3547        }
3548
3549        final LongSparseArray<Boolean> idStates = mCheckedIdStates;
3550        final int count = idStates.size();
3551        final long[] ids = new long[count];
3552
3553        for (int i = 0; i < count; i++) {
3554            ids[i] = idStates.keyAt(i);
3555        }
3556
3557        return ids;
3558    }
3559
3560    /**
3561     * Clear any choices previously set
3562     */
3563    public void clearChoices() {
3564        if (mCheckStates != null) {
3565            mCheckStates.clear();
3566        }
3567        if (mCheckedIdStates != null) {
3568            mCheckedIdStates.clear();
3569        }
3570    }
3571
3572    static class SavedState extends BaseSavedState {
3573        SparseBooleanArray checkState;
3574        LongSparseArray<Boolean> checkIdState;
3575
3576        /**
3577         * Constructor called from {@link ListView#onSaveInstanceState()}
3578         */
3579        SavedState(Parcelable superState, SparseBooleanArray checkState,
3580                LongSparseArray<Boolean> checkIdState) {
3581            super(superState);
3582            this.checkState = checkState;
3583            this.checkIdState = checkIdState;
3584        }
3585
3586        /**
3587         * Constructor called from {@link #CREATOR}
3588         */
3589        private SavedState(Parcel in) {
3590            super(in);
3591            checkState = in.readSparseBooleanArray();
3592            long[] idState = in.createLongArray();
3593
3594            if (idState.length > 0) {
3595                checkIdState = new LongSparseArray<Boolean>();
3596                checkIdState.setValues(idState, Boolean.TRUE);
3597            }
3598        }
3599
3600        @Override
3601        public void writeToParcel(Parcel out, int flags) {
3602            super.writeToParcel(out, flags);
3603            out.writeSparseBooleanArray(checkState);
3604            out.writeLongArray(checkIdState != null ? checkIdState.getKeys() : new long[0]);
3605        }
3606
3607        @Override
3608        public String toString() {
3609            return "ListView.SavedState{"
3610                    + Integer.toHexString(System.identityHashCode(this))
3611                    + " checkState=" + checkState + "}";
3612        }
3613
3614        public static final Parcelable.Creator<SavedState> CREATOR
3615                = new Parcelable.Creator<SavedState>() {
3616            public SavedState createFromParcel(Parcel in) {
3617                return new SavedState(in);
3618            }
3619
3620            public SavedState[] newArray(int size) {
3621                return new SavedState[size];
3622            }
3623        };
3624    }
3625
3626    @Override
3627    public Parcelable onSaveInstanceState() {
3628        Parcelable superState = super.onSaveInstanceState();
3629        return new SavedState(superState, mCheckStates, mCheckedIdStates);
3630    }
3631
3632    @Override
3633    public void onRestoreInstanceState(Parcelable state) {
3634        SavedState ss = (SavedState) state;
3635
3636        super.onRestoreInstanceState(ss.getSuperState());
3637
3638        if (ss.checkState != null) {
3639           mCheckStates = ss.checkState;
3640        }
3641
3642        if (ss.checkIdState != null) {
3643            mCheckedIdStates = ss.checkIdState;
3644        }
3645    }
3646}
3647