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