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