ListView.java revision 3ba8f5d675831647e45d0ce11507c85dfb5f753b
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                    handled = resurrectSelectionIfNeeded();
2085                    if (!handled) {
2086                        while (count-- > 0) {
2087                            if (arrowScroll(FOCUS_UP)) {
2088                                handled = true;
2089                            } else {
2090                                break;
2091                            }
2092                        }
2093                    }
2094                } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) {
2095                    handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_UP);
2096                }
2097                break;
2098
2099            case KeyEvent.KEYCODE_DPAD_DOWN:
2100                if (event.hasNoModifiers()) {
2101                    handled = resurrectSelectionIfNeeded();
2102                    if (!handled) {
2103                        while (count-- > 0) {
2104                            if (arrowScroll(FOCUS_DOWN)) {
2105                                handled = true;
2106                            } else {
2107                                break;
2108                            }
2109                        }
2110                    }
2111                } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) {
2112                    handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_DOWN);
2113                }
2114                break;
2115
2116            case KeyEvent.KEYCODE_DPAD_LEFT:
2117                if (event.hasNoModifiers()) {
2118                    handled = handleHorizontalFocusWithinListItem(View.FOCUS_LEFT);
2119                }
2120                break;
2121
2122            case KeyEvent.KEYCODE_DPAD_RIGHT:
2123                if (event.hasNoModifiers()) {
2124                    handled = handleHorizontalFocusWithinListItem(View.FOCUS_RIGHT);
2125                }
2126                break;
2127
2128            case KeyEvent.KEYCODE_DPAD_CENTER:
2129            case KeyEvent.KEYCODE_ENTER:
2130                if (event.hasNoModifiers()) {
2131                    handled = resurrectSelectionIfNeeded();
2132                    if (!handled
2133                            && event.getRepeatCount() == 0 && getChildCount() > 0) {
2134                        keyPressed();
2135                        handled = true;
2136                    }
2137                }
2138                break;
2139
2140            case KeyEvent.KEYCODE_SPACE:
2141                if (mPopup == null || !mPopup.isShowing()) {
2142                    if (event.hasNoModifiers()) {
2143                        handled = resurrectSelectionIfNeeded() || pageScroll(FOCUS_DOWN);
2144                    } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
2145                        handled = resurrectSelectionIfNeeded() || pageScroll(FOCUS_UP);
2146                    }
2147                    handled = true;
2148                }
2149                break;
2150
2151            case KeyEvent.KEYCODE_PAGE_UP:
2152                if (event.hasNoModifiers()) {
2153                    handled = resurrectSelectionIfNeeded() || pageScroll(FOCUS_UP);
2154                } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) {
2155                    handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_UP);
2156                }
2157                break;
2158
2159            case KeyEvent.KEYCODE_PAGE_DOWN:
2160                if (event.hasNoModifiers()) {
2161                    handled = resurrectSelectionIfNeeded() || pageScroll(FOCUS_DOWN);
2162                } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) {
2163                    handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_DOWN);
2164                }
2165                break;
2166
2167            case KeyEvent.KEYCODE_MOVE_HOME:
2168                if (event.hasNoModifiers()) {
2169                    handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_UP);
2170                }
2171                break;
2172
2173            case KeyEvent.KEYCODE_MOVE_END:
2174                if (event.hasNoModifiers()) {
2175                    handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_DOWN);
2176                }
2177                break;
2178
2179            case KeyEvent.KEYCODE_TAB:
2180                // XXX Sometimes it is useful to be able to TAB through the items in
2181                //     a ListView sequentially.  Unfortunately this can create an
2182                //     asymmetry in TAB navigation order unless the list selection
2183                //     always reverts to the top or bottom when receiving TAB focus from
2184                //     another widget.  Leaving this behavior disabled for now but
2185                //     perhaps it should be configurable (and more comprehensive).
2186                if (false) {
2187                    if (event.hasNoModifiers()) {
2188                        handled = resurrectSelectionIfNeeded() || arrowScroll(FOCUS_DOWN);
2189                    } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
2190                        handled = resurrectSelectionIfNeeded() || arrowScroll(FOCUS_UP);
2191                    }
2192                }
2193                break;
2194            }
2195        }
2196
2197        if (handled) {
2198            return true;
2199        }
2200
2201        if (sendToTextFilter(keyCode, count, event)) {
2202            return true;
2203        }
2204
2205        switch (action) {
2206            case KeyEvent.ACTION_DOWN:
2207                return super.onKeyDown(keyCode, event);
2208
2209            case KeyEvent.ACTION_UP:
2210                return super.onKeyUp(keyCode, event);
2211
2212            case KeyEvent.ACTION_MULTIPLE:
2213                return super.onKeyMultiple(keyCode, count, event);
2214
2215            default: // shouldn't happen
2216                return false;
2217        }
2218    }
2219
2220    /**
2221     * Scrolls up or down by the number of items currently present on screen.
2222     *
2223     * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN}
2224     * @return whether selection was moved
2225     */
2226    boolean pageScroll(int direction) {
2227        int nextPage = -1;
2228        boolean down = false;
2229
2230        if (direction == FOCUS_UP) {
2231            nextPage = Math.max(0, mSelectedPosition - getChildCount() - 1);
2232        } else if (direction == FOCUS_DOWN) {
2233            nextPage = Math.min(mItemCount - 1, mSelectedPosition + getChildCount() - 1);
2234            down = true;
2235        }
2236
2237        if (nextPage >= 0) {
2238            int position = lookForSelectablePosition(nextPage, down);
2239            if (position >= 0) {
2240                mLayoutMode = LAYOUT_SPECIFIC;
2241                mSpecificTop = mPaddingTop + getVerticalFadingEdgeLength();
2242
2243                if (down && position > mItemCount - getChildCount()) {
2244                    mLayoutMode = LAYOUT_FORCE_BOTTOM;
2245                }
2246
2247                if (!down && position < getChildCount()) {
2248                    mLayoutMode = LAYOUT_FORCE_TOP;
2249                }
2250
2251                setSelectionInt(position);
2252                invokeOnItemScrollListener();
2253                if (!awakenScrollBars()) {
2254                    invalidate();
2255                }
2256
2257                return true;
2258            }
2259        }
2260
2261        return false;
2262    }
2263
2264    /**
2265     * Go to the last or first item if possible (not worrying about panning across or navigating
2266     * within the internal focus of the currently selected item.)
2267     *
2268     * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN}
2269     *
2270     * @return whether selection was moved
2271     */
2272    boolean fullScroll(int direction) {
2273        boolean moved = false;
2274        if (direction == FOCUS_UP) {
2275            if (mSelectedPosition != 0) {
2276                int position = lookForSelectablePosition(0, true);
2277                if (position >= 0) {
2278                    mLayoutMode = LAYOUT_FORCE_TOP;
2279                    setSelectionInt(position);
2280                    invokeOnItemScrollListener();
2281                }
2282                moved = true;
2283            }
2284        } else if (direction == FOCUS_DOWN) {
2285            if (mSelectedPosition < mItemCount - 1) {
2286                int position = lookForSelectablePosition(mItemCount - 1, true);
2287                if (position >= 0) {
2288                    mLayoutMode = LAYOUT_FORCE_BOTTOM;
2289                    setSelectionInt(position);
2290                    invokeOnItemScrollListener();
2291                }
2292                moved = true;
2293            }
2294        }
2295
2296        if (moved && !awakenScrollBars()) {
2297            awakenScrollBars();
2298            invalidate();
2299        }
2300
2301        return moved;
2302    }
2303
2304    /**
2305     * To avoid horizontal focus searches changing the selected item, we
2306     * manually focus search within the selected item (as applicable), and
2307     * prevent focus from jumping to something within another item.
2308     * @param direction one of {View.FOCUS_LEFT, View.FOCUS_RIGHT}
2309     * @return Whether this consumes the key event.
2310     */
2311    private boolean handleHorizontalFocusWithinListItem(int direction) {
2312        if (direction != View.FOCUS_LEFT && direction != View.FOCUS_RIGHT)  {
2313            throw new IllegalArgumentException("direction must be one of"
2314                    + " {View.FOCUS_LEFT, View.FOCUS_RIGHT}");
2315        }
2316
2317        final int numChildren = getChildCount();
2318        if (mItemsCanFocus && numChildren > 0 && mSelectedPosition != INVALID_POSITION) {
2319            final View selectedView = getSelectedView();
2320            if (selectedView != null && selectedView.hasFocus() &&
2321                    selectedView instanceof ViewGroup) {
2322
2323                final View currentFocus = selectedView.findFocus();
2324                final View nextFocus = FocusFinder.getInstance().findNextFocus(
2325                        (ViewGroup) selectedView, currentFocus, direction);
2326                if (nextFocus != null) {
2327                    // do the math to get interesting rect in next focus' coordinates
2328                    currentFocus.getFocusedRect(mTempRect);
2329                    offsetDescendantRectToMyCoords(currentFocus, mTempRect);
2330                    offsetRectIntoDescendantCoords(nextFocus, mTempRect);
2331                    if (nextFocus.requestFocus(direction, mTempRect)) {
2332                        return true;
2333                    }
2334                }
2335                // we are blocking the key from being handled (by returning true)
2336                // if the global result is going to be some other view within this
2337                // list.  this is to acheive the overall goal of having
2338                // horizontal d-pad navigation remain in the current item.
2339                final View globalNextFocus = FocusFinder.getInstance().findNextFocus(
2340                        (ViewGroup) getRootView(), currentFocus, direction);
2341                if (globalNextFocus != null) {
2342                    return isViewAncestorOf(globalNextFocus, this);
2343                }
2344            }
2345        }
2346        return false;
2347    }
2348
2349    /**
2350     * Scrolls to the next or previous item if possible.
2351     *
2352     * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN}
2353     *
2354     * @return whether selection was moved
2355     */
2356    boolean arrowScroll(int direction) {
2357        try {
2358            mInLayout = true;
2359            final boolean handled = arrowScrollImpl(direction);
2360            if (handled) {
2361                playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction));
2362            }
2363            return handled;
2364        } finally {
2365            mInLayout = false;
2366        }
2367    }
2368
2369    /**
2370     * Handle an arrow scroll going up or down.  Take into account whether items are selectable,
2371     * whether there are focusable items etc.
2372     *
2373     * @param direction Either {@link android.view.View#FOCUS_UP} or {@link android.view.View#FOCUS_DOWN}.
2374     * @return Whether any scrolling, selection or focus change occured.
2375     */
2376    private boolean arrowScrollImpl(int direction) {
2377        if (getChildCount() <= 0) {
2378            return false;
2379        }
2380
2381        View selectedView = getSelectedView();
2382        int selectedPos = mSelectedPosition;
2383
2384        int nextSelectedPosition = lookForSelectablePositionOnScreen(direction);
2385        int amountToScroll = amountToScroll(direction, nextSelectedPosition);
2386
2387        // if we are moving focus, we may OVERRIDE the default behavior
2388        final ArrowScrollFocusResult focusResult = mItemsCanFocus ? arrowScrollFocused(direction) : null;
2389        if (focusResult != null) {
2390            nextSelectedPosition = focusResult.getSelectedPosition();
2391            amountToScroll = focusResult.getAmountToScroll();
2392        }
2393
2394        boolean needToRedraw = focusResult != null;
2395        if (nextSelectedPosition != INVALID_POSITION) {
2396            handleNewSelectionChange(selectedView, direction, nextSelectedPosition, focusResult != null);
2397            setSelectedPositionInt(nextSelectedPosition);
2398            setNextSelectedPositionInt(nextSelectedPosition);
2399            selectedView = getSelectedView();
2400            selectedPos = nextSelectedPosition;
2401            if (mItemsCanFocus && focusResult == null) {
2402                // there was no new view found to take focus, make sure we
2403                // don't leave focus with the old selection
2404                final View focused = getFocusedChild();
2405                if (focused != null) {
2406                    focused.clearFocus();
2407                }
2408            }
2409            needToRedraw = true;
2410            checkSelectionChanged();
2411        }
2412
2413        if (amountToScroll > 0) {
2414            scrollListItemsBy((direction == View.FOCUS_UP) ? amountToScroll : -amountToScroll);
2415            needToRedraw = true;
2416        }
2417
2418        // if we didn't find a new focusable, make sure any existing focused
2419        // item that was panned off screen gives up focus.
2420        if (mItemsCanFocus && (focusResult == null)
2421                && selectedView != null && selectedView.hasFocus()) {
2422            final View focused = selectedView.findFocus();
2423            if (distanceToView(focused) > 0) {
2424                focused.clearFocus();
2425            }
2426        }
2427
2428        // if  the current selection is panned off, we need to remove the selection
2429        if (nextSelectedPosition == INVALID_POSITION && selectedView != null
2430                && !isViewAncestorOf(selectedView, this)) {
2431            selectedView = null;
2432            hideSelector();
2433
2434            // but we don't want to set the ressurect position (that would make subsequent
2435            // unhandled key events bring back the item we just scrolled off!)
2436            mResurrectToPosition = INVALID_POSITION;
2437        }
2438
2439        if (needToRedraw) {
2440            if (selectedView != null) {
2441                positionSelector(selectedPos, selectedView);
2442                mSelectedTop = selectedView.getTop();
2443            }
2444            if (!awakenScrollBars()) {
2445                invalidate();
2446            }
2447            invokeOnItemScrollListener();
2448            return true;
2449        }
2450
2451        return false;
2452    }
2453
2454    /**
2455     * When selection changes, it is possible that the previously selected or the
2456     * next selected item will change its size.  If so, we need to offset some folks,
2457     * and re-layout the items as appropriate.
2458     *
2459     * @param selectedView The currently selected view (before changing selection).
2460     *   should be <code>null</code> if there was no previous selection.
2461     * @param direction Either {@link android.view.View#FOCUS_UP} or
2462     *        {@link android.view.View#FOCUS_DOWN}.
2463     * @param newSelectedPosition The position of the next selection.
2464     * @param newFocusAssigned whether new focus was assigned.  This matters because
2465     *        when something has focus, we don't want to show selection (ugh).
2466     */
2467    private void handleNewSelectionChange(View selectedView, int direction, int newSelectedPosition,
2468            boolean newFocusAssigned) {
2469        if (newSelectedPosition == INVALID_POSITION) {
2470            throw new IllegalArgumentException("newSelectedPosition needs to be valid");
2471        }
2472
2473        // whether or not we are moving down or up, we want to preserve the
2474        // top of whatever view is on top:
2475        // - moving down: the view that had selection
2476        // - moving up: the view that is getting selection
2477        View topView;
2478        View bottomView;
2479        int topViewIndex, bottomViewIndex;
2480        boolean topSelected = false;
2481        final int selectedIndex = mSelectedPosition - mFirstPosition;
2482        final int nextSelectedIndex = newSelectedPosition - mFirstPosition;
2483        if (direction == View.FOCUS_UP) {
2484            topViewIndex = nextSelectedIndex;
2485            bottomViewIndex = selectedIndex;
2486            topView = getChildAt(topViewIndex);
2487            bottomView = selectedView;
2488            topSelected = true;
2489        } else {
2490            topViewIndex = selectedIndex;
2491            bottomViewIndex = nextSelectedIndex;
2492            topView = selectedView;
2493            bottomView = getChildAt(bottomViewIndex);
2494        }
2495
2496        final int numChildren = getChildCount();
2497
2498        // start with top view: is it changing size?
2499        if (topView != null) {
2500            topView.setSelected(!newFocusAssigned && topSelected);
2501            measureAndAdjustDown(topView, topViewIndex, numChildren);
2502        }
2503
2504        // is the bottom view changing size?
2505        if (bottomView != null) {
2506            bottomView.setSelected(!newFocusAssigned && !topSelected);
2507            measureAndAdjustDown(bottomView, bottomViewIndex, numChildren);
2508        }
2509    }
2510
2511    /**
2512     * Re-measure a child, and if its height changes, lay it out preserving its
2513     * top, and adjust the children below it appropriately.
2514     * @param child The child
2515     * @param childIndex The view group index of the child.
2516     * @param numChildren The number of children in the view group.
2517     */
2518    private void measureAndAdjustDown(View child, int childIndex, int numChildren) {
2519        int oldHeight = child.getHeight();
2520        measureItem(child);
2521        if (child.getMeasuredHeight() != oldHeight) {
2522            // lay out the view, preserving its top
2523            relayoutMeasuredItem(child);
2524
2525            // adjust views below appropriately
2526            final int heightDelta = child.getMeasuredHeight() - oldHeight;
2527            for (int i = childIndex + 1; i < numChildren; i++) {
2528                getChildAt(i).offsetTopAndBottom(heightDelta);
2529            }
2530        }
2531    }
2532
2533    /**
2534     * Measure a particular list child.
2535     * TODO: unify with setUpChild.
2536     * @param child The child.
2537     */
2538    private void measureItem(View child) {
2539        ViewGroup.LayoutParams p = child.getLayoutParams();
2540        if (p == null) {
2541            p = new ViewGroup.LayoutParams(
2542                    ViewGroup.LayoutParams.MATCH_PARENT,
2543                    ViewGroup.LayoutParams.WRAP_CONTENT);
2544        }
2545
2546        int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
2547                mListPadding.left + mListPadding.right, p.width);
2548        int lpHeight = p.height;
2549        int childHeightSpec;
2550        if (lpHeight > 0) {
2551            childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
2552        } else {
2553            childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
2554        }
2555        child.measure(childWidthSpec, childHeightSpec);
2556    }
2557
2558    /**
2559     * Layout a child that has been measured, preserving its top position.
2560     * TODO: unify with setUpChild.
2561     * @param child The child.
2562     */
2563    private void relayoutMeasuredItem(View child) {
2564        final int w = child.getMeasuredWidth();
2565        final int h = child.getMeasuredHeight();
2566        final int childLeft = mListPadding.left;
2567        final int childRight = childLeft + w;
2568        final int childTop = child.getTop();
2569        final int childBottom = childTop + h;
2570        child.layout(childLeft, childTop, childRight, childBottom);
2571    }
2572
2573    /**
2574     * @return The amount to preview next items when arrow srolling.
2575     */
2576    private int getArrowScrollPreviewLength() {
2577        return Math.max(MIN_SCROLL_PREVIEW_PIXELS, getVerticalFadingEdgeLength());
2578    }
2579
2580    /**
2581     * Determine how much we need to scroll in order to get the next selected view
2582     * visible, with a fading edge showing below as applicable.  The amount is
2583     * capped at {@link #getMaxScrollAmount()} .
2584     *
2585     * @param direction either {@link android.view.View#FOCUS_UP} or
2586     *        {@link android.view.View#FOCUS_DOWN}.
2587     * @param nextSelectedPosition The position of the next selection, or
2588     *        {@link #INVALID_POSITION} if there is no next selectable position
2589     * @return The amount to scroll. Note: this is always positive!  Direction
2590     *         needs to be taken into account when actually scrolling.
2591     */
2592    private int amountToScroll(int direction, int nextSelectedPosition) {
2593        final int listBottom = getHeight() - mListPadding.bottom;
2594        final int listTop = mListPadding.top;
2595
2596        final int numChildren = getChildCount();
2597
2598        if (direction == View.FOCUS_DOWN) {
2599            int indexToMakeVisible = numChildren - 1;
2600            if (nextSelectedPosition != INVALID_POSITION) {
2601                indexToMakeVisible = nextSelectedPosition - mFirstPosition;
2602            }
2603
2604            final int positionToMakeVisible = mFirstPosition + indexToMakeVisible;
2605            final View viewToMakeVisible = getChildAt(indexToMakeVisible);
2606
2607            int goalBottom = listBottom;
2608            if (positionToMakeVisible < mItemCount - 1) {
2609                goalBottom -= getArrowScrollPreviewLength();
2610            }
2611
2612            if (viewToMakeVisible.getBottom() <= goalBottom) {
2613                // item is fully visible.
2614                return 0;
2615            }
2616
2617            if (nextSelectedPosition != INVALID_POSITION
2618                    && (goalBottom - viewToMakeVisible.getTop()) >= getMaxScrollAmount()) {
2619                // item already has enough of it visible, changing selection is good enough
2620                return 0;
2621            }
2622
2623            int amountToScroll = (viewToMakeVisible.getBottom() - goalBottom);
2624
2625            if ((mFirstPosition + numChildren) == mItemCount) {
2626                // last is last in list -> make sure we don't scroll past it
2627                final int max = getChildAt(numChildren - 1).getBottom() - listBottom;
2628                amountToScroll = Math.min(amountToScroll, max);
2629            }
2630
2631            return Math.min(amountToScroll, getMaxScrollAmount());
2632        } else {
2633            int indexToMakeVisible = 0;
2634            if (nextSelectedPosition != INVALID_POSITION) {
2635                indexToMakeVisible = nextSelectedPosition - mFirstPosition;
2636            }
2637            final int positionToMakeVisible = mFirstPosition + indexToMakeVisible;
2638            final View viewToMakeVisible = getChildAt(indexToMakeVisible);
2639            int goalTop = listTop;
2640            if (positionToMakeVisible > 0) {
2641                goalTop += getArrowScrollPreviewLength();
2642            }
2643            if (viewToMakeVisible.getTop() >= goalTop) {
2644                // item is fully visible.
2645                return 0;
2646            }
2647
2648            if (nextSelectedPosition != INVALID_POSITION &&
2649                    (viewToMakeVisible.getBottom() - goalTop) >= getMaxScrollAmount()) {
2650                // item already has enough of it visible, changing selection is good enough
2651                return 0;
2652            }
2653
2654            int amountToScroll = (goalTop - viewToMakeVisible.getTop());
2655            if (mFirstPosition == 0) {
2656                // first is first in list -> make sure we don't scroll past it
2657                final int max = listTop - getChildAt(0).getTop();
2658                amountToScroll = Math.min(amountToScroll,  max);
2659            }
2660            return Math.min(amountToScroll, getMaxScrollAmount());
2661        }
2662    }
2663
2664    /**
2665     * Holds results of focus aware arrow scrolling.
2666     */
2667    static private class ArrowScrollFocusResult {
2668        private int mSelectedPosition;
2669        private int mAmountToScroll;
2670
2671        /**
2672         * How {@link android.widget.ListView#arrowScrollFocused} returns its values.
2673         */
2674        void populate(int selectedPosition, int amountToScroll) {
2675            mSelectedPosition = selectedPosition;
2676            mAmountToScroll = amountToScroll;
2677        }
2678
2679        public int getSelectedPosition() {
2680            return mSelectedPosition;
2681        }
2682
2683        public int getAmountToScroll() {
2684            return mAmountToScroll;
2685        }
2686    }
2687
2688    /**
2689     * @param direction either {@link android.view.View#FOCUS_UP} or
2690     *        {@link android.view.View#FOCUS_DOWN}.
2691     * @return The position of the next selectable position of the views that
2692     *         are currently visible, taking into account the fact that there might
2693     *         be no selection.  Returns {@link #INVALID_POSITION} if there is no
2694     *         selectable view on screen in the given direction.
2695     */
2696    private int lookForSelectablePositionOnScreen(int direction) {
2697        final int firstPosition = mFirstPosition;
2698        if (direction == View.FOCUS_DOWN) {
2699            int startPos = (mSelectedPosition != INVALID_POSITION) ?
2700                    mSelectedPosition + 1 :
2701                    firstPosition;
2702            if (startPos >= mAdapter.getCount()) {
2703                return INVALID_POSITION;
2704            }
2705            if (startPos < firstPosition) {
2706                startPos = firstPosition;
2707            }
2708
2709            final int lastVisiblePos = getLastVisiblePosition();
2710            final ListAdapter adapter = getAdapter();
2711            for (int pos = startPos; pos <= lastVisiblePos; pos++) {
2712                if (adapter.isEnabled(pos)
2713                        && getChildAt(pos - firstPosition).getVisibility() == View.VISIBLE) {
2714                    return pos;
2715                }
2716            }
2717        } else {
2718            int last = firstPosition + getChildCount() - 1;
2719            int startPos = (mSelectedPosition != INVALID_POSITION) ?
2720                    mSelectedPosition - 1 :
2721                    firstPosition + getChildCount() - 1;
2722            if (startPos < 0 || startPos >= mAdapter.getCount()) {
2723                return INVALID_POSITION;
2724            }
2725            if (startPos > last) {
2726                startPos = last;
2727            }
2728
2729            final ListAdapter adapter = getAdapter();
2730            for (int pos = startPos; pos >= firstPosition; pos--) {
2731                if (adapter.isEnabled(pos)
2732                        && getChildAt(pos - firstPosition).getVisibility() == View.VISIBLE) {
2733                    return pos;
2734                }
2735            }
2736        }
2737        return INVALID_POSITION;
2738    }
2739
2740    /**
2741     * Do an arrow scroll based on focus searching.  If a new view is
2742     * given focus, return the selection delta and amount to scroll via
2743     * an {@link ArrowScrollFocusResult}, otherwise, return null.
2744     *
2745     * @param direction either {@link android.view.View#FOCUS_UP} or
2746     *        {@link android.view.View#FOCUS_DOWN}.
2747     * @return The result if focus has changed, or <code>null</code>.
2748     */
2749    private ArrowScrollFocusResult arrowScrollFocused(final int direction) {
2750        final View selectedView = getSelectedView();
2751        View newFocus;
2752        if (selectedView != null && selectedView.hasFocus()) {
2753            View oldFocus = selectedView.findFocus();
2754            newFocus = FocusFinder.getInstance().findNextFocus(this, oldFocus, direction);
2755        } else {
2756            if (direction == View.FOCUS_DOWN) {
2757                final boolean topFadingEdgeShowing = (mFirstPosition > 0);
2758                final int listTop = mListPadding.top +
2759                        (topFadingEdgeShowing ? getArrowScrollPreviewLength() : 0);
2760                final int ySearchPoint =
2761                        (selectedView != null && selectedView.getTop() > listTop) ?
2762                                selectedView.getTop() :
2763                                listTop;
2764                mTempRect.set(0, ySearchPoint, 0, ySearchPoint);
2765            } else {
2766                final boolean bottomFadingEdgeShowing =
2767                        (mFirstPosition + getChildCount() - 1) < mItemCount;
2768                final int listBottom = getHeight() - mListPadding.bottom -
2769                        (bottomFadingEdgeShowing ? getArrowScrollPreviewLength() : 0);
2770                final int ySearchPoint =
2771                        (selectedView != null && selectedView.getBottom() < listBottom) ?
2772                                selectedView.getBottom() :
2773                                listBottom;
2774                mTempRect.set(0, ySearchPoint, 0, ySearchPoint);
2775            }
2776            newFocus = FocusFinder.getInstance().findNextFocusFromRect(this, mTempRect, direction);
2777        }
2778
2779        if (newFocus != null) {
2780            final int positionOfNewFocus = positionOfNewFocus(newFocus);
2781
2782            // if the focus change is in a different new position, make sure
2783            // we aren't jumping over another selectable position
2784            if (mSelectedPosition != INVALID_POSITION && positionOfNewFocus != mSelectedPosition) {
2785                final int selectablePosition = lookForSelectablePositionOnScreen(direction);
2786                if (selectablePosition != INVALID_POSITION &&
2787                        ((direction == View.FOCUS_DOWN && selectablePosition < positionOfNewFocus) ||
2788                        (direction == View.FOCUS_UP && selectablePosition > positionOfNewFocus))) {
2789                    return null;
2790                }
2791            }
2792
2793            int focusScroll = amountToScrollToNewFocus(direction, newFocus, positionOfNewFocus);
2794
2795            final int maxScrollAmount = getMaxScrollAmount();
2796            if (focusScroll < maxScrollAmount) {
2797                // not moving too far, safe to give next view focus
2798                newFocus.requestFocus(direction);
2799                mArrowScrollFocusResult.populate(positionOfNewFocus, focusScroll);
2800                return mArrowScrollFocusResult;
2801            } else if (distanceToView(newFocus) < maxScrollAmount){
2802                // Case to consider:
2803                // too far to get entire next focusable on screen, but by going
2804                // max scroll amount, we are getting it at least partially in view,
2805                // so give it focus and scroll the max ammount.
2806                newFocus.requestFocus(direction);
2807                mArrowScrollFocusResult.populate(positionOfNewFocus, maxScrollAmount);
2808                return mArrowScrollFocusResult;
2809            }
2810        }
2811        return null;
2812    }
2813
2814    /**
2815     * @param newFocus The view that would have focus.
2816     * @return the position that contains newFocus
2817     */
2818    private int positionOfNewFocus(View newFocus) {
2819        final int numChildren = getChildCount();
2820        for (int i = 0; i < numChildren; i++) {
2821            final View child = getChildAt(i);
2822            if (isViewAncestorOf(newFocus, child)) {
2823                return mFirstPosition + i;
2824            }
2825        }
2826        throw new IllegalArgumentException("newFocus is not a child of any of the"
2827                + " children of the list!");
2828    }
2829
2830    /**
2831     * Return true if child is an ancestor of parent, (or equal to the parent).
2832     */
2833    private boolean isViewAncestorOf(View child, View parent) {
2834        if (child == parent) {
2835            return true;
2836        }
2837
2838        final ViewParent theParent = child.getParent();
2839        return (theParent instanceof ViewGroup) && isViewAncestorOf((View) theParent, parent);
2840    }
2841
2842    /**
2843     * Determine how much we need to scroll in order to get newFocus in view.
2844     * @param direction either {@link android.view.View#FOCUS_UP} or
2845     *        {@link android.view.View#FOCUS_DOWN}.
2846     * @param newFocus The view that would take focus.
2847     * @param positionOfNewFocus The position of the list item containing newFocus
2848     * @return The amount to scroll.  Note: this is always positive!  Direction
2849     *   needs to be taken into account when actually scrolling.
2850     */
2851    private int amountToScrollToNewFocus(int direction, View newFocus, int positionOfNewFocus) {
2852        int amountToScroll = 0;
2853        newFocus.getDrawingRect(mTempRect);
2854        offsetDescendantRectToMyCoords(newFocus, mTempRect);
2855        if (direction == View.FOCUS_UP) {
2856            if (mTempRect.top < mListPadding.top) {
2857                amountToScroll = mListPadding.top - mTempRect.top;
2858                if (positionOfNewFocus > 0) {
2859                    amountToScroll += getArrowScrollPreviewLength();
2860                }
2861            }
2862        } else {
2863            final int listBottom = getHeight() - mListPadding.bottom;
2864            if (mTempRect.bottom > listBottom) {
2865                amountToScroll = mTempRect.bottom - listBottom;
2866                if (positionOfNewFocus < mItemCount - 1) {
2867                    amountToScroll += getArrowScrollPreviewLength();
2868                }
2869            }
2870        }
2871        return amountToScroll;
2872    }
2873
2874    /**
2875     * Determine the distance to the nearest edge of a view in a particular
2876     * direction.
2877     *
2878     * @param descendant A descendant of this list.
2879     * @return The distance, or 0 if the nearest edge is already on screen.
2880     */
2881    private int distanceToView(View descendant) {
2882        int distance = 0;
2883        descendant.getDrawingRect(mTempRect);
2884        offsetDescendantRectToMyCoords(descendant, mTempRect);
2885        final int listBottom = mBottom - mTop - mListPadding.bottom;
2886        if (mTempRect.bottom < mListPadding.top) {
2887            distance = mListPadding.top - mTempRect.bottom;
2888        } else if (mTempRect.top > listBottom) {
2889            distance = mTempRect.top - listBottom;
2890        }
2891        return distance;
2892    }
2893
2894
2895    /**
2896     * Scroll the children by amount, adding a view at the end and removing
2897     * views that fall off as necessary.
2898     *
2899     * @param amount The amount (positive or negative) to scroll.
2900     */
2901    private void scrollListItemsBy(int amount) {
2902        offsetChildrenTopAndBottom(amount);
2903
2904        final int listBottom = getHeight() - mListPadding.bottom;
2905        final int listTop = mListPadding.top;
2906        final AbsListView.RecycleBin recycleBin = mRecycler;
2907
2908        if (amount < 0) {
2909            // shifted items up
2910
2911            // may need to pan views into the bottom space
2912            int numChildren = getChildCount();
2913            View last = getChildAt(numChildren - 1);
2914            while (last.getBottom() < listBottom) {
2915                final int lastVisiblePosition = mFirstPosition + numChildren - 1;
2916                if (lastVisiblePosition < mItemCount - 1) {
2917                    last = addViewBelow(last, lastVisiblePosition);
2918                    numChildren++;
2919                } else {
2920                    break;
2921                }
2922            }
2923
2924            // may have brought in the last child of the list that is skinnier
2925            // than the fading edge, thereby leaving space at the end.  need
2926            // to shift back
2927            if (last.getBottom() < listBottom) {
2928                offsetChildrenTopAndBottom(listBottom - last.getBottom());
2929            }
2930
2931            // top views may be panned off screen
2932            View first = getChildAt(0);
2933            while (first.getBottom() < listTop) {
2934                AbsListView.LayoutParams layoutParams = (LayoutParams) first.getLayoutParams();
2935                if (recycleBin.shouldRecycleViewType(layoutParams.viewType)) {
2936                    detachViewFromParent(first);
2937                    recycleBin.addScrapView(first, mFirstPosition);
2938                } else {
2939                    removeViewInLayout(first);
2940                }
2941                first = getChildAt(0);
2942                mFirstPosition++;
2943            }
2944        } else {
2945            // shifted items down
2946            View first = getChildAt(0);
2947
2948            // may need to pan views into top
2949            while ((first.getTop() > listTop) && (mFirstPosition > 0)) {
2950                first = addViewAbove(first, mFirstPosition);
2951                mFirstPosition--;
2952            }
2953
2954            // may have brought the very first child of the list in too far and
2955            // need to shift it back
2956            if (first.getTop() > listTop) {
2957                offsetChildrenTopAndBottom(listTop - first.getTop());
2958            }
2959
2960            int lastIndex = getChildCount() - 1;
2961            View last = getChildAt(lastIndex);
2962
2963            // bottom view may be panned off screen
2964            while (last.getTop() > listBottom) {
2965                AbsListView.LayoutParams layoutParams = (LayoutParams) last.getLayoutParams();
2966                if (recycleBin.shouldRecycleViewType(layoutParams.viewType)) {
2967                    detachViewFromParent(last);
2968                    recycleBin.addScrapView(last, mFirstPosition+lastIndex);
2969                } else {
2970                    removeViewInLayout(last);
2971                }
2972                last = getChildAt(--lastIndex);
2973            }
2974        }
2975    }
2976
2977    private View addViewAbove(View theView, int position) {
2978        int abovePosition = position - 1;
2979        View view = obtainView(abovePosition, mIsScrap);
2980        int edgeOfNewChild = theView.getTop() - mDividerHeight;
2981        setupChild(view, abovePosition, edgeOfNewChild, false, mListPadding.left,
2982                false, mIsScrap[0]);
2983        return view;
2984    }
2985
2986    private View addViewBelow(View theView, int position) {
2987        int belowPosition = position + 1;
2988        View view = obtainView(belowPosition, mIsScrap);
2989        int edgeOfNewChild = theView.getBottom() + mDividerHeight;
2990        setupChild(view, belowPosition, edgeOfNewChild, true, mListPadding.left,
2991                false, mIsScrap[0]);
2992        return view;
2993    }
2994
2995    /**
2996     * Indicates that the views created by the ListAdapter can contain focusable
2997     * items.
2998     *
2999     * @param itemsCanFocus true if items can get focus, false otherwise
3000     */
3001    public void setItemsCanFocus(boolean itemsCanFocus) {
3002        mItemsCanFocus = itemsCanFocus;
3003        if (!itemsCanFocus) {
3004            setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
3005        }
3006    }
3007
3008    /**
3009     * @return Whether the views created by the ListAdapter can contain focusable
3010     * items.
3011     */
3012    public boolean getItemsCanFocus() {
3013        return mItemsCanFocus;
3014    }
3015
3016    @Override
3017    public boolean isOpaque() {
3018        boolean retValue = (mCachingActive && mIsCacheColorOpaque && mDividerIsOpaque &&
3019                hasOpaqueScrollbars()) || super.isOpaque();
3020        if (retValue) {
3021            // only return true if the list items cover the entire area of the view
3022            final int listTop = mListPadding != null ? mListPadding.top : mPaddingTop;
3023            View first = getChildAt(0);
3024            if (first == null || first.getTop() > listTop) {
3025                return false;
3026            }
3027            final int listBottom = getHeight() -
3028                    (mListPadding != null ? mListPadding.bottom : mPaddingBottom);
3029            View last = getChildAt(getChildCount() - 1);
3030            if (last == null || last.getBottom() < listBottom) {
3031                return false;
3032            }
3033        }
3034        return retValue;
3035    }
3036
3037    @Override
3038    public void setCacheColorHint(int color) {
3039        final boolean opaque = (color >>> 24) == 0xFF;
3040        mIsCacheColorOpaque = opaque;
3041        if (opaque) {
3042            if (mDividerPaint == null) {
3043                mDividerPaint = new Paint();
3044            }
3045            mDividerPaint.setColor(color);
3046        }
3047        super.setCacheColorHint(color);
3048    }
3049
3050    void drawOverscrollHeader(Canvas canvas, Drawable drawable, Rect bounds) {
3051        final int height = drawable.getMinimumHeight();
3052
3053        canvas.save();
3054        canvas.clipRect(bounds);
3055
3056        final int span = bounds.bottom - bounds.top;
3057        if (span < height) {
3058            bounds.top = bounds.bottom - height;
3059        }
3060
3061        drawable.setBounds(bounds);
3062        drawable.draw(canvas);
3063
3064        canvas.restore();
3065    }
3066
3067    void drawOverscrollFooter(Canvas canvas, Drawable drawable, Rect bounds) {
3068        final int height = drawable.getMinimumHeight();
3069
3070        canvas.save();
3071        canvas.clipRect(bounds);
3072
3073        final int span = bounds.bottom - bounds.top;
3074        if (span < height) {
3075            bounds.bottom = bounds.top + height;
3076        }
3077
3078        drawable.setBounds(bounds);
3079        drawable.draw(canvas);
3080
3081        canvas.restore();
3082    }
3083
3084    @Override
3085    protected void dispatchDraw(Canvas canvas) {
3086        if (mCachingStarted) {
3087            mCachingActive = true;
3088        }
3089
3090        // Draw the dividers
3091        final int dividerHeight = mDividerHeight;
3092        final Drawable overscrollHeader = mOverScrollHeader;
3093        final Drawable overscrollFooter = mOverScrollFooter;
3094        final boolean drawOverscrollHeader = overscrollHeader != null;
3095        final boolean drawOverscrollFooter = overscrollFooter != null;
3096        final boolean drawDividers = dividerHeight > 0 && mDivider != null;
3097
3098        if (drawDividers || drawOverscrollHeader || drawOverscrollFooter) {
3099            // Only modify the top and bottom in the loop, we set the left and right here
3100            final Rect bounds = mTempRect;
3101            bounds.left = mPaddingLeft;
3102            bounds.right = mRight - mLeft - mPaddingRight;
3103
3104            final int count = getChildCount();
3105            final int headerCount = mHeaderViewInfos.size();
3106            final int itemCount = mItemCount;
3107            final int footerLimit = itemCount - mFooterViewInfos.size() - 1;
3108            final boolean headerDividers = mHeaderDividersEnabled;
3109            final boolean footerDividers = mFooterDividersEnabled;
3110            final int first = mFirstPosition;
3111            final boolean areAllItemsSelectable = mAreAllItemsSelectable;
3112            final ListAdapter adapter = mAdapter;
3113            // If the list is opaque *and* the background is not, we want to
3114            // fill a rect where the dividers would be for non-selectable items
3115            // If the list is opaque and the background is also opaque, we don't
3116            // need to draw anything since the background will do it for us
3117            final boolean fillForMissingDividers = isOpaque() && !super.isOpaque();
3118
3119            if (fillForMissingDividers && mDividerPaint == null && mIsCacheColorOpaque) {
3120                mDividerPaint = new Paint();
3121                mDividerPaint.setColor(getCacheColorHint());
3122            }
3123            final Paint paint = mDividerPaint;
3124
3125            int effectivePaddingTop = 0;
3126            int effectivePaddingBottom = 0;
3127            if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
3128                effectivePaddingTop = mListPadding.top;
3129                effectivePaddingBottom = mListPadding.bottom;
3130            }
3131
3132            final int listBottom = mBottom - mTop - effectivePaddingBottom + mScrollY;
3133            if (!mStackFromBottom) {
3134                int bottom = 0;
3135
3136                // Draw top divider or header for overscroll
3137                final int scrollY = mScrollY;
3138                if (count > 0 && scrollY < 0) {
3139                    if (drawOverscrollHeader) {
3140                        bounds.bottom = 0;
3141                        bounds.top = scrollY;
3142                        drawOverscrollHeader(canvas, overscrollHeader, bounds);
3143                    } else if (drawDividers) {
3144                        bounds.bottom = 0;
3145                        bounds.top = -dividerHeight;
3146                        drawDivider(canvas, bounds, -1);
3147                    }
3148                }
3149
3150                for (int i = 0; i < count; i++) {
3151                    if ((headerDividers || first + i >= headerCount) &&
3152                            (footerDividers || first + i < footerLimit)) {
3153                        View child = getChildAt(i);
3154                        bottom = child.getBottom();
3155                        // Don't draw dividers next to items that are not enabled
3156
3157                        if (drawDividers &&
3158                                (bottom < listBottom && !(drawOverscrollFooter && i == count - 1))) {
3159                            if ((areAllItemsSelectable ||
3160                                    (adapter.isEnabled(first + i) && (i == count - 1 ||
3161                                            adapter.isEnabled(first + i + 1))))) {
3162                                bounds.top = bottom;
3163                                bounds.bottom = bottom + dividerHeight;
3164                                drawDivider(canvas, bounds, i);
3165                            } else if (fillForMissingDividers) {
3166                                bounds.top = bottom;
3167                                bounds.bottom = bottom + dividerHeight;
3168                                canvas.drawRect(bounds, paint);
3169                            }
3170                        }
3171                    }
3172                }
3173
3174                final int overFooterBottom = mBottom + mScrollY;
3175                if (drawOverscrollFooter && first + count == itemCount &&
3176                        overFooterBottom > bottom) {
3177                    bounds.top = bottom;
3178                    bounds.bottom = overFooterBottom;
3179                    drawOverscrollFooter(canvas, overscrollFooter, bounds);
3180                }
3181            } else {
3182                int top;
3183
3184                final int scrollY = mScrollY;
3185
3186                if (count > 0 && drawOverscrollHeader) {
3187                    bounds.top = scrollY;
3188                    bounds.bottom = getChildAt(0).getTop();
3189                    drawOverscrollHeader(canvas, overscrollHeader, bounds);
3190                }
3191
3192                final int start = drawOverscrollHeader ? 1 : 0;
3193                for (int i = start; i < count; i++) {
3194                    if ((headerDividers || first + i >= headerCount) &&
3195                            (footerDividers || first + i < footerLimit)) {
3196                        View child = getChildAt(i);
3197                        top = child.getTop();
3198                        // Don't draw dividers next to items that are not enabled
3199                        if (top > effectivePaddingTop) {
3200                            if ((areAllItemsSelectable ||
3201                                    (adapter.isEnabled(first + i) && (i == count - 1 ||
3202                                            adapter.isEnabled(first + i + 1))))) {
3203                                bounds.top = top - dividerHeight;
3204                                bounds.bottom = top;
3205                                // Give the method the child ABOVE the divider, so we
3206                                // subtract one from our child
3207                                // position. Give -1 when there is no child above the
3208                                // divider.
3209                                drawDivider(canvas, bounds, i - 1);
3210                            } else if (fillForMissingDividers) {
3211                                bounds.top = top - dividerHeight;
3212                                bounds.bottom = top;
3213                                canvas.drawRect(bounds, paint);
3214                            }
3215                        }
3216                    }
3217                }
3218
3219                if (count > 0 && scrollY > 0) {
3220                    if (drawOverscrollFooter) {
3221                        final int absListBottom = mBottom;
3222                        bounds.top = absListBottom;
3223                        bounds.bottom = absListBottom + scrollY;
3224                        drawOverscrollFooter(canvas, overscrollFooter, bounds);
3225                    } else if (drawDividers) {
3226                        bounds.top = listBottom;
3227                        bounds.bottom = listBottom + dividerHeight;
3228                        drawDivider(canvas, bounds, -1);
3229                    }
3230                }
3231            }
3232        }
3233
3234        // Draw the indicators (these should be drawn above the dividers) and children
3235        super.dispatchDraw(canvas);
3236    }
3237
3238    @Override
3239    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
3240        boolean more = super.drawChild(canvas, child, drawingTime);
3241        if (mCachingActive && child.mCachingFailed) {
3242            mCachingActive = false;
3243        }
3244        return more;
3245    }
3246
3247    /**
3248     * Draws a divider for the given child in the given bounds.
3249     *
3250     * @param canvas The canvas to draw to.
3251     * @param bounds The bounds of the divider.
3252     * @param childIndex The index of child (of the View) above the divider.
3253     *            This will be -1 if there is no child above the divider to be
3254     *            drawn.
3255     */
3256    void drawDivider(Canvas canvas, Rect bounds, int childIndex) {
3257        // This widget draws the same divider for all children
3258        final Drawable divider = mDivider;
3259
3260        divider.setBounds(bounds);
3261        divider.draw(canvas);
3262    }
3263
3264    /**
3265     * Returns the drawable that will be drawn between each item in the list.
3266     *
3267     * @return the current drawable drawn between list elements
3268     */
3269    public Drawable getDivider() {
3270        return mDivider;
3271    }
3272
3273    /**
3274     * Sets the drawable that will be drawn between each item in the list. If the drawable does
3275     * not have an intrinsic height, you should also call {@link #setDividerHeight(int)}
3276     *
3277     * @param divider The drawable to use.
3278     */
3279    public void setDivider(Drawable divider) {
3280        if (divider != null) {
3281            mDividerHeight = divider.getIntrinsicHeight();
3282        } else {
3283            mDividerHeight = 0;
3284        }
3285        mDivider = divider;
3286        mDividerIsOpaque = divider == null || divider.getOpacity() == PixelFormat.OPAQUE;
3287        requestLayout();
3288        invalidate();
3289    }
3290
3291    /**
3292     * @return Returns the height of the divider that will be drawn between each item in the list.
3293     */
3294    public int getDividerHeight() {
3295        return mDividerHeight;
3296    }
3297
3298    /**
3299     * Sets the height of the divider that will be drawn between each item in the list. Calling
3300     * this will override the intrinsic height as set by {@link #setDivider(Drawable)}
3301     *
3302     * @param height The new height of the divider in pixels.
3303     */
3304    public void setDividerHeight(int height) {
3305        mDividerHeight = height;
3306        requestLayout();
3307        invalidate();
3308    }
3309
3310    /**
3311     * Enables or disables the drawing of the divider for header views.
3312     *
3313     * @param headerDividersEnabled True to draw the headers, false otherwise.
3314     *
3315     * @see #setFooterDividersEnabled(boolean)
3316     * @see #addHeaderView(android.view.View)
3317     */
3318    public void setHeaderDividersEnabled(boolean headerDividersEnabled) {
3319        mHeaderDividersEnabled = headerDividersEnabled;
3320        invalidate();
3321    }
3322
3323    /**
3324     * Enables or disables the drawing of the divider for footer views.
3325     *
3326     * @param footerDividersEnabled True to draw the footers, false otherwise.
3327     *
3328     * @see #setHeaderDividersEnabled(boolean)
3329     * @see #addFooterView(android.view.View)
3330     */
3331    public void setFooterDividersEnabled(boolean footerDividersEnabled) {
3332        mFooterDividersEnabled = footerDividersEnabled;
3333        invalidate();
3334    }
3335
3336    /**
3337     * Sets the drawable that will be drawn above all other list content.
3338     * This area can become visible when the user overscrolls the list.
3339     *
3340     * @param header The drawable to use
3341     */
3342    public void setOverscrollHeader(Drawable header) {
3343        mOverScrollHeader = header;
3344        if (mScrollY < 0) {
3345            invalidate();
3346        }
3347    }
3348
3349    /**
3350     * @return The drawable that will be drawn above all other list content
3351     */
3352    public Drawable getOverscrollHeader() {
3353        return mOverScrollHeader;
3354    }
3355
3356    /**
3357     * Sets the drawable that will be drawn below all other list content.
3358     * This area can become visible when the user overscrolls the list,
3359     * or when the list's content does not fully fill the container area.
3360     *
3361     * @param footer The drawable to use
3362     */
3363    public void setOverscrollFooter(Drawable footer) {
3364        mOverScrollFooter = footer;
3365        invalidate();
3366    }
3367
3368    /**
3369     * @return The drawable that will be drawn below all other list content
3370     */
3371    public Drawable getOverscrollFooter() {
3372        return mOverScrollFooter;
3373    }
3374
3375    @Override
3376    protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
3377        super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
3378
3379        int closetChildIndex = -1;
3380        if (gainFocus && previouslyFocusedRect != null) {
3381            previouslyFocusedRect.offset(mScrollX, mScrollY);
3382
3383            final ListAdapter adapter = mAdapter;
3384            // Don't cache the result of getChildCount or mFirstPosition here,
3385            // it could change in layoutChildren.
3386            if (adapter.getCount() < getChildCount() + mFirstPosition) {
3387                mLayoutMode = LAYOUT_NORMAL;
3388                layoutChildren();
3389            }
3390
3391            // figure out which item should be selected based on previously
3392            // focused rect
3393            Rect otherRect = mTempRect;
3394            int minDistance = Integer.MAX_VALUE;
3395            final int childCount = getChildCount();
3396            final int firstPosition = mFirstPosition;
3397
3398            for (int i = 0; i < childCount; i++) {
3399                // only consider selectable views
3400                if (!adapter.isEnabled(firstPosition + i)) {
3401                    continue;
3402                }
3403
3404                View other = getChildAt(i);
3405                other.getDrawingRect(otherRect);
3406                offsetDescendantRectToMyCoords(other, otherRect);
3407                int distance = getDistance(previouslyFocusedRect, otherRect, direction);
3408
3409                if (distance < minDistance) {
3410                    minDistance = distance;
3411                    closetChildIndex = i;
3412                }
3413            }
3414        }
3415
3416        if (closetChildIndex >= 0) {
3417            setSelection(closetChildIndex + mFirstPosition);
3418        } else {
3419            requestLayout();
3420        }
3421    }
3422
3423
3424    /*
3425     * (non-Javadoc)
3426     *
3427     * Children specified in XML are assumed to be header views. After we have
3428     * parsed them move them out of the children list and into mHeaderViews.
3429     */
3430    @Override
3431    protected void onFinishInflate() {
3432        super.onFinishInflate();
3433
3434        int count = getChildCount();
3435        if (count > 0) {
3436            for (int i = 0; i < count; ++i) {
3437                addHeaderView(getChildAt(i));
3438            }
3439            removeAllViews();
3440        }
3441    }
3442
3443    /* (non-Javadoc)
3444     * @see android.view.View#findViewById(int)
3445     * First look in our children, then in any header and footer views that may be scrolled off.
3446     */
3447    @Override
3448    protected View findViewTraversal(int id) {
3449        View v;
3450        v = super.findViewTraversal(id);
3451        if (v == null) {
3452            v = findViewInHeadersOrFooters(mHeaderViewInfos, id);
3453            if (v != null) {
3454                return v;
3455            }
3456            v = findViewInHeadersOrFooters(mFooterViewInfos, id);
3457            if (v != null) {
3458                return v;
3459            }
3460        }
3461        return v;
3462    }
3463
3464    /* (non-Javadoc)
3465     *
3466     * Look in the passed in list of headers or footers for the view.
3467     */
3468    View findViewInHeadersOrFooters(ArrayList<FixedViewInfo> where, int id) {
3469        if (where != null) {
3470            int len = where.size();
3471            View v;
3472
3473            for (int i = 0; i < len; i++) {
3474                v = where.get(i).view;
3475
3476                if (!v.isRootNamespace()) {
3477                    v = v.findViewById(id);
3478
3479                    if (v != null) {
3480                        return v;
3481                    }
3482                }
3483            }
3484        }
3485        return null;
3486    }
3487
3488    /* (non-Javadoc)
3489     * @see android.view.View#findViewWithTag(Object)
3490     * First look in our children, then in any header and footer views that may be scrolled off.
3491     */
3492    @Override
3493    protected View findViewWithTagTraversal(Object tag) {
3494        View v;
3495        v = super.findViewWithTagTraversal(tag);
3496        if (v == null) {
3497            v = findViewWithTagInHeadersOrFooters(mHeaderViewInfos, tag);
3498            if (v != null) {
3499                return v;
3500            }
3501
3502            v = findViewWithTagInHeadersOrFooters(mFooterViewInfos, tag);
3503            if (v != null) {
3504                return v;
3505            }
3506        }
3507        return v;
3508    }
3509
3510    /* (non-Javadoc)
3511     *
3512     * Look in the passed in list of headers or footers for the view with the tag.
3513     */
3514    View findViewWithTagInHeadersOrFooters(ArrayList<FixedViewInfo> where, Object tag) {
3515        if (where != null) {
3516            int len = where.size();
3517            View v;
3518
3519            for (int i = 0; i < len; i++) {
3520                v = where.get(i).view;
3521
3522                if (!v.isRootNamespace()) {
3523                    v = v.findViewWithTag(tag);
3524
3525                    if (v != null) {
3526                        return v;
3527                    }
3528                }
3529            }
3530        }
3531        return null;
3532    }
3533
3534    /**
3535     * @hide
3536     * @see android.view.View#findViewByPredicate(Predicate)
3537     * First look in our children, then in any header and footer views that may be scrolled off.
3538     */
3539    @Override
3540    protected View findViewByPredicateTraversal(Predicate<View> predicate) {
3541        View v;
3542        v = super.findViewByPredicateTraversal(predicate);
3543        if (v == null) {
3544            v = findViewByPredicateInHeadersOrFooters(mHeaderViewInfos, predicate);
3545            if (v != null) {
3546                return v;
3547            }
3548
3549            v = findViewByPredicateInHeadersOrFooters(mFooterViewInfos, predicate);
3550            if (v != null) {
3551                return v;
3552            }
3553        }
3554        return v;
3555    }
3556
3557    /* (non-Javadoc)
3558     *
3559     * Look in the passed in list of headers or footers for the first view that matches
3560     * the predicate.
3561     */
3562    View findViewByPredicateInHeadersOrFooters(ArrayList<FixedViewInfo> where,
3563            Predicate<View> predicate) {
3564        if (where != null) {
3565            int len = where.size();
3566            View v;
3567
3568            for (int i = 0; i < len; i++) {
3569                v = where.get(i).view;
3570
3571                if (!v.isRootNamespace()) {
3572                    v = v.findViewByPredicate(predicate);
3573
3574                    if (v != null) {
3575                        return v;
3576                    }
3577                }
3578            }
3579        }
3580        return null;
3581    }
3582
3583    @Override
3584    public boolean onTouchEvent(MotionEvent ev) {
3585        //noinspection SimplifiableIfStatement
3586        if (mItemsCanFocus && ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) {
3587            // Don't handle edge touches immediately -- they may actually belong to one of our
3588            // descendants.
3589            return false;
3590        }
3591        return super.onTouchEvent(ev);
3592    }
3593
3594    /**
3595     * Returns the set of checked items ids. The result is only valid if the
3596     * choice mode has not been set to {@link #CHOICE_MODE_NONE}.
3597     *
3598     * @return A new array which contains the id of each checked item in the
3599     *         list.
3600     *
3601     * @deprecated Use {@link #getCheckedItemIds()} instead.
3602     */
3603    @Deprecated
3604    public long[] getCheckItemIds() {
3605        // Use new behavior that correctly handles stable ID mapping.
3606        if (mAdapter != null && mAdapter.hasStableIds()) {
3607            return getCheckedItemIds();
3608        }
3609
3610        // Old behavior was buggy, but would sort of work for adapters without stable IDs.
3611        // Fall back to it to support legacy apps.
3612        if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null && mAdapter != null) {
3613            final SparseBooleanArray states = mCheckStates;
3614            final int count = states.size();
3615            final long[] ids = new long[count];
3616            final ListAdapter adapter = mAdapter;
3617
3618            int checkedCount = 0;
3619            for (int i = 0; i < count; i++) {
3620                if (states.valueAt(i)) {
3621                    ids[checkedCount++] = adapter.getItemId(states.keyAt(i));
3622                }
3623            }
3624
3625            // Trim array if needed. mCheckStates may contain false values
3626            // resulting in checkedCount being smaller than count.
3627            if (checkedCount == count) {
3628                return ids;
3629            } else {
3630                final long[] result = new long[checkedCount];
3631                System.arraycopy(ids, 0, result, 0, checkedCount);
3632
3633                return result;
3634            }
3635        }
3636        return new long[0];
3637    }
3638}
3639