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