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