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