1 /*
2  * Copyright (C) 2013 Lucas Rocha
3  *
4  * This code is based on bits and pieces of Android's AbsListView,
5  * Listview, and StaggeredGridView.
6  *
7  * Copyright (C) 2012 The Android Open Source Project
8  *
9  * Licensed under the Apache License, Version 2.0 (the "License");
10  * you may not use this file except in compliance with the License.
11  * You may obtain a copy of the License at
12  *
13  *      http://www.apache.org/licenses/LICENSE-2.0
14  *
15  * Unless required by applicable law or agreed to in writing, software
16  * distributed under the License is distributed on an "AS IS" BASIS,
17  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18  * See the License for the specific language governing permissions and
19  * limitations under the License.
20  */
21 
22 package org.mozilla.gecko.widget;
23 
24 import org.mozilla.gecko.R;
25 
26 import java.util.ArrayList;
27 import java.util.List;
28 
29 import android.annotation.TargetApi;
30 import android.content.Context;
31 import android.content.res.TypedArray;
32 import android.database.DataSetObserver;
33 import android.graphics.Canvas;
34 import android.graphics.Rect;
35 import android.graphics.drawable.Drawable;
36 import android.graphics.drawable.TransitionDrawable;
37 import android.os.Build;
38 import android.os.Bundle;
39 import android.os.Parcel;
40 import android.os.Parcelable;
41 import android.os.SystemClock;
42 import android.support.v4.util.LongSparseArray;
43 import android.support.v4.util.SparseArrayCompat;
44 import android.support.v4.view.AccessibilityDelegateCompat;
45 import android.support.v4.view.KeyEventCompat;
46 import android.support.v4.view.MotionEventCompat;
47 import android.support.v4.view.VelocityTrackerCompat;
48 import android.support.v4.view.ViewCompat;
49 import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
50 import android.support.v4.widget.EdgeEffectCompat;
51 import android.util.AttributeSet;
52 import android.util.Log;
53 import android.util.SparseBooleanArray;
54 import android.view.ContextMenu.ContextMenuInfo;
55 import android.view.FocusFinder;
56 import android.view.HapticFeedbackConstants;
57 import android.view.KeyEvent;
58 import android.view.MotionEvent;
59 import android.view.SoundEffectConstants;
60 import android.view.VelocityTracker;
61 import android.view.View;
62 import android.view.ViewConfiguration;
63 import android.view.ViewGroup;
64 import android.view.ViewParent;
65 import android.view.ViewTreeObserver;
66 import android.view.accessibility.AccessibilityEvent;
67 import android.view.accessibility.AccessibilityNodeInfo;
68 import android.widget.AdapterView;
69 import android.widget.Checkable;
70 import android.widget.ListAdapter;
71 import android.widget.Scroller;
72 
73 import static android.os.Build.VERSION_CODES.HONEYCOMB;
74 
75 /*
76  * Implementation Notes:
77  *
78  * Some terminology:
79  *
80  *     index    - index of the items that are currently visible
81  *     position - index of the items in the cursor
82  *
83  * Given the bi-directional nature of this view, the source code
84  * usually names variables with 'start' to mean 'top' or 'left'; and
85  * 'end' to mean 'bottom' or 'right', depending on the current
86  * orientation of the widget.
87  */
88 
89 /**
90  * A view that shows items in a vertical or horizontal scrolling list.
91  * The items come from the {@link ListAdapter} associated with this view.
92  */
93 public class TwoWayView extends AdapterView<ListAdapter> implements
94         ViewTreeObserver.OnTouchModeChangeListener {
95     private static final String LOGTAG = "TwoWayView";
96 
97     private static final int NO_POSITION = -1;
98     private static final int INVALID_POINTER = -1;
99 
100     public static final int[] STATE_NOTHING = new int[] { 0 };
101 
102     private static final int TOUCH_MODE_REST = -1;
103     private static final int TOUCH_MODE_DOWN = 0;
104     private static final int TOUCH_MODE_TAP = 1;
105     private static final int TOUCH_MODE_DONE_WAITING = 2;
106     private static final int TOUCH_MODE_DRAGGING = 3;
107     private static final int TOUCH_MODE_FLINGING = 4;
108     private static final int TOUCH_MODE_OVERSCROLL = 5;
109 
110     private static final int TOUCH_MODE_UNKNOWN = -1;
111     private static final int TOUCH_MODE_ON = 0;
112     private static final int TOUCH_MODE_OFF = 1;
113 
114     private static final int LAYOUT_NORMAL = 0;
115     private static final int LAYOUT_FORCE_TOP = 1;
116     private static final int LAYOUT_SET_SELECTION = 2;
117     private static final int LAYOUT_FORCE_BOTTOM = 3;
118     private static final int LAYOUT_SPECIFIC = 4;
119     private static final int LAYOUT_SYNC = 5;
120     private static final int LAYOUT_MOVE_SELECTION = 6;
121 
122     private static final int SYNC_SELECTED_POSITION = 0;
123     private static final int SYNC_FIRST_POSITION = 1;
124 
125     private static final int SYNC_MAX_DURATION_MILLIS = 100;
126 
127     private static final int CHECK_POSITION_SEARCH_DISTANCE = 20;
128 
129     private static final float MAX_SCROLL_FACTOR = 0.33f;
130 
131     private static final int MIN_SCROLL_PREVIEW_PIXELS = 10;
132 
133     public static enum ChoiceMode {
134         NONE,
135         SINGLE,
136         MULTIPLE
137     }
138 
139     public static enum Orientation {
140         HORIZONTAL,
141         VERTICAL
142     }
143 
144     private final Context mContext;
145 
146     private ListAdapter mAdapter;
147 
148     private boolean mIsVertical;
149 
150     private int mItemMargin;
151 
152     private boolean mInLayout;
153     private boolean mBlockLayoutRequests;
154 
155     private boolean mIsAttached;
156 
157     private final RecycleBin mRecycler;
158     private AdapterDataSetObserver mDataSetObserver;
159 
160     private boolean mItemsCanFocus;
161 
162     final boolean[] mIsScrap = new boolean[1];
163 
164     private boolean mDataChanged;
165     private int mItemCount;
166     private int mOldItemCount;
167     private boolean mHasStableIds;
168     private boolean mAreAllItemsSelectable;
169 
170     private int mFirstPosition;
171     private int mSpecificStart;
172 
173     private SavedState mPendingSync;
174 
175     private PositionScroller mPositionScroller;
176     private Runnable mPositionScrollAfterLayout;
177 
178     private final int mTouchSlop;
179     private final int mMaximumVelocity;
180     private final int mFlingVelocity;
181     private float mLastTouchPos;
182     private float mTouchRemainderPos;
183     private int mActivePointerId;
184 
185     private final Rect mTempRect;
186 
187     private final ArrowScrollFocusResult mArrowScrollFocusResult;
188 
189     private Rect mTouchFrame;
190     private int mMotionPosition;
191     private CheckForTap mPendingCheckForTap;
192     private CheckForLongPress mPendingCheckForLongPress;
193     private CheckForKeyLongPress mPendingCheckForKeyLongPress;
194     private PerformClick mPerformClick;
195     private Runnable mTouchModeReset;
196     private int mResurrectToPosition;
197 
198     private boolean mIsChildViewEnabled;
199 
200     private boolean mDrawSelectorOnTop;
201     private Drawable mSelector;
202     private int mSelectorPosition;
203     private final Rect mSelectorRect;
204 
205     private int mOverScroll;
206     private final int mOverscrollDistance;
207 
208     private boolean mDesiredFocusableState;
209     private boolean mDesiredFocusableInTouchModeState;
210 
211     private SelectionNotifier mSelectionNotifier;
212 
213     private boolean mNeedSync;
214     private int mSyncMode;
215     private int mSyncPosition;
216     private long mSyncRowId;
217     private long mSyncSize;
218     private int mSelectedStart;
219 
220     private int mNextSelectedPosition;
221     private long mNextSelectedRowId;
222     private int mSelectedPosition;
223     private long mSelectedRowId;
224     private int mOldSelectedPosition;
225     private long mOldSelectedRowId;
226 
227     private ChoiceMode mChoiceMode;
228     private int mCheckedItemCount;
229     private SparseBooleanArray mCheckStates;
230     LongSparseArray<Integer> mCheckedIdStates;
231 
232     private ContextMenuInfo mContextMenuInfo;
233 
234     private int mLayoutMode;
235     private int mTouchMode;
236     private int mLastTouchMode;
237     private VelocityTracker mVelocityTracker;
238     private final Scroller mScroller;
239 
240     private EdgeEffectCompat mStartEdge;
241     private EdgeEffectCompat mEndEdge;
242 
243     private OnScrollListener mOnScrollListener;
244     private int mLastScrollState;
245 
246     private View mEmptyView;
247 
248     private ListItemAccessibilityDelegate mAccessibilityDelegate;
249 
250     private int mLastAccessibilityScrollEventFromIndex;
251     private int mLastAccessibilityScrollEventToIndex;
252 
253     public interface OnScrollListener {
254 
255         /**
256          * The view is not scrolling. Note navigating the list using the trackball counts as
257          * being in the idle state since these transitions are not animated.
258          */
259         public static int SCROLL_STATE_IDLE = 0;
260 
261         /**
262          * The user is scrolling using touch, and their finger is still on the screen
263          */
264         public static int SCROLL_STATE_TOUCH_SCROLL = 1;
265 
266         /**
267          * The user had previously been scrolling using touch and had performed a fling. The
268          * animation is now coasting to a stop
269          */
270         public static int SCROLL_STATE_FLING = 2;
271 
272         /**
273          * Callback method to be invoked while the list view or grid view is being scrolled. If the
274          * view is being scrolled, this method will be called before the next frame of the scroll is
275          * rendered. In particular, it will be called before any calls to
276          * {@link android.widget.Adapter#getView(int, View, ViewGroup)}.
277          *
278          * @param view The view whose scroll state is being reported
279          *
280          * @param scrollState The current scroll state. One of {@link #SCROLL_STATE_IDLE},
281          * {@link #SCROLL_STATE_TOUCH_SCROLL} or {@link #SCROLL_STATE_IDLE}.
282          */
onScrollStateChanged(TwoWayView view, int scrollState)283         public void onScrollStateChanged(TwoWayView view, int scrollState);
284 
285         /**
286          * Callback method to be invoked when the list or grid has been scrolled. This will be
287          * called after the scroll has completed
288          * @param view The view whose scroll state is being reported
289          * @param firstVisibleItem the index of the first visible cell (ignore if
290          *        visibleItemCount == 0)
291          * @param visibleItemCount the number of visible cells
292          * @param totalItemCount the number of items in the list adaptor
293          */
onScroll(TwoWayView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)294         public void onScroll(TwoWayView view, int firstVisibleItem, int visibleItemCount,
295                 int totalItemCount);
296     }
297 
298     /**
299      * A RecyclerListener is used to receive a notification whenever a View is placed
300      * inside the RecycleBin's scrap heap. This listener is used to free resources
301      * associated to Views placed in the RecycleBin.
302      *
303      * @see TwoWayView.RecycleBin
304      * @see TwoWayView#setRecyclerListener(TwoWayView.RecyclerListener)
305      */
306     public static interface RecyclerListener {
307         /**
308          * Indicates that the specified View was moved into the recycler's scrap heap.
309          * The view is not displayed on screen any more and any expensive resource
310          * associated with the view should be discarded.
311          *
312          * @param view
313          */
onMovedToScrapHeap(View view)314         void onMovedToScrapHeap(View view);
315     }
316 
TwoWayView(Context context)317     public TwoWayView(Context context) {
318         this(context, null);
319     }
320 
TwoWayView(Context context, AttributeSet attrs)321     public TwoWayView(Context context, AttributeSet attrs) {
322         this(context, attrs, 0);
323     }
324 
TwoWayView(Context context, AttributeSet attrs, int defStyle)325     public TwoWayView(Context context, AttributeSet attrs, int defStyle) {
326         super(context, attrs, defStyle);
327 
328         mContext = context;
329 
330         mLayoutMode = LAYOUT_NORMAL;
331         mTouchMode = TOUCH_MODE_REST;
332         mLastTouchMode = TOUCH_MODE_UNKNOWN;
333 
334         mLastScrollState = OnScrollListener.SCROLL_STATE_IDLE;
335 
336         final ViewConfiguration vc = ViewConfiguration.get(context);
337         mTouchSlop = vc.getScaledTouchSlop();
338         mMaximumVelocity = vc.getScaledMaximumFlingVelocity();
339         mFlingVelocity = vc.getScaledMinimumFlingVelocity();
340         mOverscrollDistance = getScaledOverscrollDistance(vc);
341 
342         mScroller = new Scroller(context);
343 
344         mIsVertical = true;
345 
346         mTempRect = new Rect();
347 
348         mArrowScrollFocusResult = new ArrowScrollFocusResult();
349 
350         mSelectorPosition = INVALID_POSITION;
351 
352         mSelectorRect = new Rect();
353 
354         mResurrectToPosition = INVALID_POSITION;
355 
356         mNextSelectedPosition = INVALID_POSITION;
357         mNextSelectedRowId = INVALID_ROW_ID;
358         mSelectedPosition = INVALID_POSITION;
359         mSelectedRowId = INVALID_ROW_ID;
360         mOldSelectedPosition = INVALID_POSITION;
361         mOldSelectedRowId = INVALID_ROW_ID;
362 
363         mChoiceMode = ChoiceMode.NONE;
364 
365         mRecycler = new RecycleBin();
366 
367         mAreAllItemsSelectable = true;
368 
369         setClickable(true);
370         setFocusableInTouchMode(true);
371         setWillNotDraw(false);
372         setAlwaysDrawnWithCacheEnabled(false);
373         setWillNotDraw(false);
374         setClipToPadding(false);
375 
376         ViewCompat.setOverScrollMode(this, ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS);
377 
378         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TwoWayView, defStyle, 0);
379 
380         mDrawSelectorOnTop = a.getBoolean(
381                 R.styleable.TwoWayView_android_drawSelectorOnTop, false);
382 
383         Drawable d = a.getDrawable(R.styleable.TwoWayView_android_listSelector);
384         if (d != null) {
385             setSelector(d);
386         }
387 
388         int orientation = a.getInt(R.styleable.TwoWayView_android_orientation, -1);
389         if (orientation >= 0) {
390             setOrientation(Orientation.values()[orientation]);
391         }
392 
393         int choiceMode = a.getInt(R.styleable.TwoWayView_android_choiceMode, -1);
394         if (choiceMode >= 0) {
395             setChoiceMode(ChoiceMode.values()[choiceMode]);
396         }
397 
398         a.recycle();
399     }
400 
setOrientation(Orientation orientation)401     public void setOrientation(Orientation orientation) {
402         final boolean isVertical = (orientation == Orientation.VERTICAL);
403         if (mIsVertical == isVertical) {
404             return;
405         }
406 
407         mIsVertical = isVertical;
408 
409         resetState();
410         mRecycler.clear();
411 
412         requestLayout();
413     }
414 
getOrientation()415     public Orientation getOrientation() {
416         return (mIsVertical ? Orientation.VERTICAL : Orientation.HORIZONTAL);
417     }
418 
setItemMargin(int itemMargin)419     public void setItemMargin(int itemMargin) {
420         if (mItemMargin == itemMargin) {
421             return;
422         }
423 
424         mItemMargin = itemMargin;
425         requestLayout();
426     }
427 
428     @SuppressWarnings("unused")
getItemMargin()429     public int getItemMargin() {
430         return mItemMargin;
431     }
432 
433     /**
434      * Indicates that the views created by the ListAdapter can contain focusable
435      * items.
436      *
437      * @param itemsCanFocus true if items can get focus, false otherwise
438      */
439     @SuppressWarnings("unused")
setItemsCanFocus(boolean itemsCanFocus)440     public void setItemsCanFocus(boolean itemsCanFocus) {
441         mItemsCanFocus = itemsCanFocus;
442         if (!itemsCanFocus) {
443             setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
444         }
445     }
446 
447     /**
448      * @return Whether the views created by the ListAdapter can contain focusable
449      * items.
450      */
451     @SuppressWarnings("unused")
getItemsCanFocus()452     public boolean getItemsCanFocus() {
453         return mItemsCanFocus;
454     }
455 
456     /**
457      * Set the listener that will receive notifications every time the list scrolls.
458      *
459      * @param l the scroll listener
460      */
setOnScrollListener(OnScrollListener l)461     public void setOnScrollListener(OnScrollListener l) {
462         mOnScrollListener = l;
463         invokeOnItemScrollListener();
464     }
465 
466     /**
467      * Sets the recycler listener to be notified whenever a View is set aside in
468      * the recycler for later reuse. This listener can be used to free resources
469      * associated to the View.
470      *
471      * @param l The recycler listener to be notified of views set aside
472      *        in the recycler.
473      *
474      * @see TwoWayView.RecycleBin
475      * @see TwoWayView.RecyclerListener
476      */
setRecyclerListener(RecyclerListener l)477     public void setRecyclerListener(RecyclerListener l) {
478         mRecycler.mRecyclerListener = l;
479     }
480 
481     /**
482      * Controls whether the selection highlight drawable should be drawn on top of the item or
483      * behind it.
484      *
485      * @param drawSelectorOnTop If true, the selector will be drawn on the item it is highlighting.
486      *                          The default is false.
487      *
488      * @attr ref android.R.styleable#AbsListView_drawSelectorOnTop
489      */
490     @SuppressWarnings("unused")
setDrawSelectorOnTop(boolean drawSelectorOnTop)491     public void setDrawSelectorOnTop(boolean drawSelectorOnTop) {
492         mDrawSelectorOnTop = drawSelectorOnTop;
493     }
494 
495     /**
496      * Set a Drawable that should be used to highlight the currently selected item.
497      *
498      * @param resID A Drawable resource to use as the selection highlight.
499      *
500      * @attr ref android.R.styleable#AbsListView_listSelector
501      */
502     @SuppressWarnings("unused")
setSelector(int resID)503     public void setSelector(int resID) {
504         setSelector(getResources().getDrawable(resID));
505     }
506 
507     /**
508      * Set a Drawable that should be used to highlight the currently selected item.
509      *
510      * @param selector A Drawable to use as the selection highlight.
511      *
512      * @attr ref android.R.styleable#AbsListView_listSelector
513      */
setSelector(Drawable selector)514     public void setSelector(Drawable selector) {
515         if (mSelector != null) {
516             mSelector.setCallback(null);
517             unscheduleDrawable(mSelector);
518         }
519 
520         mSelector = selector;
521         Rect padding = new Rect();
522         selector.getPadding(padding);
523 
524         selector.setCallback(this);
525         updateSelectorState();
526     }
527 
528     /**
529      * Returns the selector {@link android.graphics.drawable.Drawable} that is used to draw the
530      * selection in the list.
531      *
532      * @return the drawable used to display the selector
533      */
534     @SuppressWarnings("unused")
getSelector()535     public Drawable getSelector() {
536         return mSelector;
537     }
538 
539     /**
540      * {@inheritDoc}
541      */
542     @Override
getSelectedItemPosition()543     public int getSelectedItemPosition() {
544         return mNextSelectedPosition;
545     }
546 
547     /**
548      * {@inheritDoc}
549      */
550     @Override
getSelectedItemId()551     public long getSelectedItemId() {
552         return mNextSelectedRowId;
553     }
554 
555     /**
556      * Returns the number of items currently selected. This will only be valid
557      * if the choice mode is not {@link ChoiceMode#NONE} (default).
558      *
559      * <p>To determine the specific items that are currently selected, use one of
560      * the <code>getChecked*</code> methods.
561      *
562      * @return The number of items currently selected
563      *
564      * @see #getCheckedItemPosition()
565      * @see #getCheckedItemPositions()
566      * @see #getCheckedItemIds()
567      */
568     @SuppressWarnings("unused")
getCheckedItemCount()569     public int getCheckedItemCount() {
570         return mCheckedItemCount;
571     }
572 
573     /**
574      * Returns the checked state of the specified position. The result is only
575      * valid if the choice mode has been set to {@link ChoiceMode#SINGLE}
576      * or {@link ChoiceMode#MULTIPLE}.
577      *
578      * @param position The item whose checked state to return
579      * @return The item's checked state or <code>false</code> if choice mode
580      *         is invalid
581      *
582      * @see #setChoiceMode(ChoiceMode)
583      */
isItemChecked(int position)584     public boolean isItemChecked(int position) {
585         if (mChoiceMode == ChoiceMode.NONE && mCheckStates != null) {
586             return mCheckStates.get(position);
587         }
588 
589         return false;
590     }
591 
592     /**
593      * Returns the currently checked item. The result is only valid if the choice
594      * mode has been set to {@link ChoiceMode#SINGLE}.
595      *
596      * @return The position of the currently checked item or
597      *         {@link #INVALID_POSITION} if nothing is selected
598      *
599      * @see #setChoiceMode(ChoiceMode)
600      */
getCheckedItemPosition()601     public int getCheckedItemPosition() {
602         if (mChoiceMode == ChoiceMode.SINGLE && mCheckStates != null && mCheckStates.size() == 1) {
603             return mCheckStates.keyAt(0);
604         }
605 
606         return INVALID_POSITION;
607     }
608 
609     /**
610      * Returns the set of checked items in the list. The result is only valid if
611      * the choice mode has not been set to {@link ChoiceMode#NONE}.
612      *
613      * @return  A SparseBooleanArray which will return true for each call to
614      *          get(int position) where position is a position in the list,
615      *          or <code>null</code> if the choice mode is set to
616      *          {@link ChoiceMode#NONE}.
617      */
getCheckedItemPositions()618     public SparseBooleanArray getCheckedItemPositions() {
619         if (mChoiceMode != ChoiceMode.NONE) {
620             return mCheckStates;
621         }
622 
623         return null;
624     }
625 
626     /**
627      * Returns the set of checked items ids. The result is only valid if the
628      * choice mode has not been set to {@link ChoiceMode#NONE} and the adapter
629      * has stable IDs. ({@link ListAdapter#hasStableIds()} == {@code true})
630      *
631      * @return A new array which contains the id of each checked item in the
632      *         list.
633      */
getCheckedItemIds()634     public long[] getCheckedItemIds() {
635         if (mChoiceMode == ChoiceMode.NONE || mCheckedIdStates == null || mAdapter == null) {
636             return new long[0];
637         }
638 
639         final LongSparseArray<Integer> idStates = mCheckedIdStates;
640         final int count = idStates.size();
641         final long[] ids = new long[count];
642 
643         for (int i = 0; i < count; i++) {
644             ids[i] = idStates.keyAt(i);
645         }
646 
647         return ids;
648     }
649 
650     /**
651      * Sets the checked state of the specified position. The is only valid if
652      * the choice mode has been set to {@link ChoiceMode#SINGLE} or
653      * {@link ChoiceMode#MULTIPLE}.
654      *
655      * @param position The item whose checked state is to be checked
656      * @param value The new checked state for the item
657      */
658     @SuppressWarnings("unused")
setItemChecked(int position, boolean value)659     public void setItemChecked(int position, boolean value) {
660         if (mChoiceMode == ChoiceMode.NONE) {
661             return;
662         }
663 
664         if (mChoiceMode == ChoiceMode.MULTIPLE) {
665             boolean oldValue = mCheckStates.get(position);
666             mCheckStates.put(position, value);
667 
668             if (mCheckedIdStates != null && mAdapter.hasStableIds()) {
669                 if (value) {
670                     mCheckedIdStates.put(mAdapter.getItemId(position), position);
671                 } else {
672                     mCheckedIdStates.delete(mAdapter.getItemId(position));
673                 }
674             }
675 
676             if (oldValue != value) {
677                 if (value) {
678                     mCheckedItemCount++;
679                 } else {
680                     mCheckedItemCount--;
681                 }
682             }
683         } else {
684             boolean updateIds = mCheckedIdStates != null && mAdapter.hasStableIds();
685 
686             // Clear all values if we're checking something, or unchecking the currently
687             // selected item
688             if (value || isItemChecked(position)) {
689                 mCheckStates.clear();
690 
691                 if (updateIds) {
692                     mCheckedIdStates.clear();
693                 }
694             }
695 
696             // This may end up selecting the value we just cleared but this way
697             // we ensure length of mCheckStates is 1, a fact getCheckedItemPosition relies on
698             if (value) {
699                 mCheckStates.put(position, true);
700 
701                 if (updateIds) {
702                     mCheckedIdStates.put(mAdapter.getItemId(position), position);
703                 }
704 
705                 mCheckedItemCount = 1;
706             } else if (mCheckStates.size() == 0 || !mCheckStates.valueAt(0)) {
707                 mCheckedItemCount = 0;
708             }
709         }
710 
711         // Do not generate a data change while we are in the layout phase
712         if (!mInLayout && !mBlockLayoutRequests) {
713             mDataChanged = true;
714             rememberSyncState();
715             requestLayout();
716         }
717     }
718 
719     /**
720      * Clear any choices previously set
721      */
722     @SuppressWarnings("unused")
clearChoices()723     public void clearChoices() {
724         if (mCheckStates != null) {
725             mCheckStates.clear();
726         }
727 
728         if (mCheckedIdStates != null) {
729             mCheckedIdStates.clear();
730         }
731 
732         mCheckedItemCount = 0;
733     }
734 
735     /**
736      * @see #setChoiceMode(ChoiceMode)
737      *
738      * @return The current choice mode
739      */
740     @SuppressWarnings("unused")
getChoiceMode()741     public ChoiceMode getChoiceMode() {
742         return mChoiceMode;
743     }
744 
745     /**
746      * Defines the choice behavior for the List. By default, Lists do not have any choice behavior
747      * ({@link ChoiceMode#NONE}). By setting the choiceMode to {@link ChoiceMode#SINGLE}, the
748      * List allows up to one item to  be in a chosen state. By setting the choiceMode to
749      * {@link ChoiceMode#MULTIPLE}, the list allows any number of items to be chosen.
750      *
751      * @param choiceMode One of {@link ChoiceMode#NONE}, {@link ChoiceMode#SINGLE}, or
752      * {@link ChoiceMode#MULTIPLE}
753      */
setChoiceMode(ChoiceMode choiceMode)754     public void setChoiceMode(ChoiceMode choiceMode) {
755         mChoiceMode = choiceMode;
756 
757         if (mChoiceMode != ChoiceMode.NONE) {
758             if (mCheckStates == null) {
759                 mCheckStates = new SparseBooleanArray();
760             }
761 
762             if (mCheckedIdStates == null && mAdapter != null && mAdapter.hasStableIds()) {
763                 mCheckedIdStates = new LongSparseArray<Integer>();
764             }
765         }
766     }
767 
768     @Override
getAdapter()769     public ListAdapter getAdapter() {
770         return mAdapter;
771     }
772 
773     @Override
setAdapter(ListAdapter adapter)774     public void setAdapter(ListAdapter adapter) {
775         if (mAdapter != null && mDataSetObserver != null) {
776             mAdapter.unregisterDataSetObserver(mDataSetObserver);
777         }
778 
779         resetState();
780         mRecycler.clear();
781 
782         mAdapter = adapter;
783         mDataChanged = true;
784 
785         mOldSelectedPosition = INVALID_POSITION;
786         mOldSelectedRowId = INVALID_ROW_ID;
787 
788         if (mCheckStates != null) {
789             mCheckStates.clear();
790         }
791 
792         if (mCheckedIdStates != null) {
793             mCheckedIdStates.clear();
794         }
795 
796         if (mAdapter != null) {
797             mOldItemCount = mItemCount;
798             mItemCount = adapter.getCount();
799 
800             mDataSetObserver = new AdapterDataSetObserver();
801             mAdapter.registerDataSetObserver(mDataSetObserver);
802 
803             mRecycler.setViewTypeCount(adapter.getViewTypeCount());
804 
805             mHasStableIds = adapter.hasStableIds();
806             mAreAllItemsSelectable = adapter.areAllItemsEnabled();
807 
808             if (mChoiceMode != ChoiceMode.NONE && mHasStableIds && mCheckedIdStates == null) {
809                 mCheckedIdStates = new LongSparseArray<Integer>();
810             }
811 
812             final int position = lookForSelectablePosition(0);
813             setSelectedPositionInt(position);
814             setNextSelectedPositionInt(position);
815 
816             if (mItemCount == 0) {
817                 checkSelectionChanged();
818             }
819         } else {
820             mItemCount = 0;
821             mHasStableIds = false;
822             mAreAllItemsSelectable = true;
823 
824             checkSelectionChanged();
825         }
826 
827         checkFocus();
828         requestLayout();
829     }
830 
831     @Override
getFirstVisiblePosition()832     public int getFirstVisiblePosition() {
833         return mFirstPosition;
834     }
835 
836     @Override
getLastVisiblePosition()837     public int getLastVisiblePosition() {
838         return mFirstPosition + getChildCount() - 1;
839     }
840 
841     @Override
getCount()842     public int getCount() {
843         return mItemCount;
844     }
845 
846     @Override
getPositionForView(View view)847     public int getPositionForView(View view) {
848         View child = view;
849         try {
850             View v;
851             while (!(v = (View) child.getParent()).equals(this)) {
852                 child = v;
853             }
854         } catch (ClassCastException e) {
855             // We made it up to the window without find this list view
856             return INVALID_POSITION;
857         }
858 
859         // Search the children for the list item
860         final int childCount = getChildCount();
861         for (int i = 0; i < childCount; i++) {
862             if (getChildAt(i).equals(child)) {
863                 return mFirstPosition + i;
864             }
865         }
866 
867         // Child not found!
868         return INVALID_POSITION;
869     }
870 
871     @Override
getFocusedRect(Rect r)872     public void getFocusedRect(Rect r) {
873         View view = getSelectedView();
874 
875         if (view != null && view.getParent() == this) {
876             // The focused rectangle of the selected view offset into the
877             // coordinate space of this view.
878             view.getFocusedRect(r);
879             offsetDescendantRectToMyCoords(view, r);
880         } else {
881             super.getFocusedRect(r);
882         }
883     }
884 
885     @Override
onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect)886     protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
887         super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
888 
889         if (gainFocus && mSelectedPosition < 0 && !isInTouchMode()) {
890             if (!mIsAttached && mAdapter != null) {
891                 // Data may have changed while we were detached and it's valid
892                 // to change focus while detached. Refresh so we don't die.
893                 mDataChanged = true;
894                 mOldItemCount = mItemCount;
895                 mItemCount = mAdapter.getCount();
896             }
897 
898             resurrectSelection();
899         }
900 
901         final ListAdapter adapter = mAdapter;
902         int closetChildIndex = INVALID_POSITION;
903         int closestChildStart = 0;
904 
905         if (adapter != null && gainFocus && previouslyFocusedRect != null) {
906             previouslyFocusedRect.offset(getScrollX(), getScrollY());
907 
908             // Don't cache the result of getChildCount or mFirstPosition here,
909             // it could change in layoutChildren.
910             if (adapter.getCount() < getChildCount() + mFirstPosition) {
911                 mLayoutMode = LAYOUT_NORMAL;
912                 layoutChildren();
913             }
914 
915             // Figure out which item should be selected based on previously
916             // focused rect.
917             Rect otherRect = mTempRect;
918             int minDistance = Integer.MAX_VALUE;
919             final int childCount = getChildCount();
920             final int firstPosition = mFirstPosition;
921 
922             for (int i = 0; i < childCount; i++) {
923                 // Only consider selectable views
924                 if (!adapter.isEnabled(firstPosition + i)) {
925                     continue;
926                 }
927 
928                 View other = getChildAt(i);
929                 other.getDrawingRect(otherRect);
930                 offsetDescendantRectToMyCoords(other, otherRect);
931                 int distance = getDistance(previouslyFocusedRect, otherRect, direction);
932 
933                 if (distance < minDistance) {
934                     minDistance = distance;
935                     closetChildIndex = i;
936                     closestChildStart = getChildStartEdge(other);
937                 }
938             }
939         }
940 
941         if (closetChildIndex >= 0) {
942             setSelectionFromOffset(closetChildIndex + mFirstPosition, closestChildStart);
943         } else {
944             requestLayout();
945         }
946     }
947 
948     @Override
onAttachedToWindow()949     protected void onAttachedToWindow() {
950         super.onAttachedToWindow();
951 
952         final ViewTreeObserver treeObserver = getViewTreeObserver();
953         treeObserver.addOnTouchModeChangeListener(this);
954 
955         if (mAdapter != null && mDataSetObserver == null) {
956             mDataSetObserver = new AdapterDataSetObserver();
957             mAdapter.registerDataSetObserver(mDataSetObserver);
958 
959             // Data may have changed while we were detached. Refresh.
960             mDataChanged = true;
961             mOldItemCount = mItemCount;
962             mItemCount = mAdapter.getCount();
963         }
964 
965         mIsAttached = true;
966     }
967 
968     @Override
onDetachedFromWindow()969     protected void onDetachedFromWindow() {
970         super.onDetachedFromWindow();
971 
972         // Detach any view left in the scrap heap
973         mRecycler.clear();
974 
975         final ViewTreeObserver treeObserver = getViewTreeObserver();
976         treeObserver.removeOnTouchModeChangeListener(this);
977 
978         if (mAdapter != null) {
979             mAdapter.unregisterDataSetObserver(mDataSetObserver);
980             mDataSetObserver = null;
981         }
982 
983         if (mPerformClick != null) {
984             removeCallbacks(mPerformClick);
985         }
986 
987         if (mTouchModeReset != null) {
988             removeCallbacks(mTouchModeReset);
989             mTouchModeReset.run();
990         }
991 
992         finishSmoothScrolling();
993 
994         mIsAttached = false;
995     }
996 
997     @Override
onWindowFocusChanged(boolean hasWindowFocus)998     public void onWindowFocusChanged(boolean hasWindowFocus) {
999         super.onWindowFocusChanged(hasWindowFocus);
1000 
1001         final int touchMode = isInTouchMode() ? TOUCH_MODE_ON : TOUCH_MODE_OFF;
1002 
1003         if (!hasWindowFocus) {
1004             if (!mScroller.isFinished()) {
1005                 finishSmoothScrolling();
1006                 if (mOverScroll != 0) {
1007                     mOverScroll = 0;
1008                     finishEdgeGlows();
1009                     invalidate();
1010                 }
1011             }
1012 
1013             if (touchMode == TOUCH_MODE_OFF) {
1014                 // Remember the last selected element
1015                 mResurrectToPosition = mSelectedPosition;
1016             }
1017         } else {
1018             // If we changed touch mode since the last time we had focus
1019             if (touchMode != mLastTouchMode && mLastTouchMode != TOUCH_MODE_UNKNOWN) {
1020                 // If we come back in trackball mode, we bring the selection back
1021                 if (touchMode == TOUCH_MODE_OFF) {
1022                     // This will trigger a layout
1023                     resurrectSelection();
1024 
1025                 // If we come back in touch mode, then we want to hide the selector
1026                 } else {
1027                     hideSelector();
1028                     mLayoutMode = LAYOUT_NORMAL;
1029                     layoutChildren();
1030                 }
1031             }
1032         }
1033 
1034         mLastTouchMode = touchMode;
1035     }
1036 
1037     @Override
onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY)1038     protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
1039         boolean needsInvalidate = false;
1040 
1041         if (mIsVertical && mOverScroll != scrollY) {
1042             onScrollChanged(getScrollX(), scrollY, getScrollX(), mOverScroll);
1043             mOverScroll = scrollY;
1044             needsInvalidate = true;
1045         } else if (!mIsVertical && mOverScroll != scrollX) {
1046             onScrollChanged(scrollX, getScrollY(), mOverScroll, getScrollY());
1047             mOverScroll = scrollX;
1048             needsInvalidate = true;
1049         }
1050 
1051         if (needsInvalidate) {
1052             invalidate();
1053             awakenScrollbarsInternal();
1054         }
1055     }
1056 
1057     @TargetApi(9)
overScrollByInternal(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent)1058     private boolean overScrollByInternal(int deltaX, int deltaY,
1059             int scrollX, int scrollY,
1060             int scrollRangeX, int scrollRangeY,
1061             int maxOverScrollX, int maxOverScrollY,
1062             boolean isTouchEvent) {
1063         if (Build.VERSION.SDK_INT < 9) {
1064             return false;
1065         }
1066 
1067         return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX,
1068                 scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent);
1069     }
1070 
1071     @Override
1072     @TargetApi(9)
setOverScrollMode(int mode)1073     public void setOverScrollMode(int mode) {
1074         if (Build.VERSION.SDK_INT < 9) {
1075             return;
1076         }
1077 
1078         if (mode != ViewCompat.OVER_SCROLL_NEVER) {
1079             if (mStartEdge == null) {
1080                 Context context = getContext();
1081 
1082                 mStartEdge = new EdgeEffectCompat(context);
1083                 mEndEdge = new EdgeEffectCompat(context);
1084             }
1085         } else {
1086             mStartEdge = null;
1087             mEndEdge = null;
1088         }
1089 
1090         super.setOverScrollMode(mode);
1091     }
1092 
pointToPosition(int x, int y)1093     public int pointToPosition(int x, int y) {
1094         Rect frame = mTouchFrame;
1095         if (frame == null) {
1096             mTouchFrame = new Rect();
1097             frame = mTouchFrame;
1098         }
1099 
1100         final int count = getChildCount();
1101         for (int i = count - 1; i >= 0; i--) {
1102             final View child = getChildAt(i);
1103 
1104             if (child.getVisibility() == View.VISIBLE) {
1105                 child.getHitRect(frame);
1106 
1107                 if (frame.contains(x, y)) {
1108                     return mFirstPosition + i;
1109                 }
1110             }
1111         }
1112         return INVALID_POSITION;
1113     }
1114 
1115     @Override
getTopFadingEdgeStrength()1116     protected float getTopFadingEdgeStrength() {
1117         if (!mIsVertical) {
1118             return 0f;
1119         }
1120 
1121         final float fadingEdge = super.getTopFadingEdgeStrength();
1122 
1123         final int childCount = getChildCount();
1124         if (childCount == 0) {
1125             return fadingEdge;
1126         } else {
1127             if (mFirstPosition > 0) {
1128                 return 1.0f;
1129             }
1130 
1131             final int top = getChildAt(0).getTop();
1132             final int paddingTop = getPaddingTop();
1133 
1134             final float length = (float) getVerticalFadingEdgeLength();
1135 
1136             return (top < paddingTop ? (float) -(top - paddingTop) / length : fadingEdge);
1137         }
1138     }
1139 
1140     @Override
getBottomFadingEdgeStrength()1141     protected float getBottomFadingEdgeStrength() {
1142         if (!mIsVertical) {
1143             return 0f;
1144         }
1145 
1146         final float fadingEdge = super.getBottomFadingEdgeStrength();
1147 
1148         final int childCount = getChildCount();
1149         if (childCount == 0) {
1150             return fadingEdge;
1151         } else {
1152             if (mFirstPosition + childCount - 1 < mItemCount - 1) {
1153                 return 1.0f;
1154             }
1155 
1156             final int bottom = getChildAt(childCount - 1).getBottom();
1157             final int paddingBottom = getPaddingBottom();
1158 
1159             final int height = getHeight();
1160             final float length = (float) getVerticalFadingEdgeLength();
1161 
1162             return (bottom > height - paddingBottom ?
1163                     (float) (bottom - height + paddingBottom) / length : fadingEdge);
1164         }
1165     }
1166 
1167     @Override
getLeftFadingEdgeStrength()1168     protected float getLeftFadingEdgeStrength() {
1169         if (mIsVertical) {
1170             return 0f;
1171         }
1172 
1173         final float fadingEdge = super.getLeftFadingEdgeStrength();
1174 
1175         final int childCount = getChildCount();
1176         if (childCount == 0) {
1177             return fadingEdge;
1178         } else {
1179             if (mFirstPosition > 0) {
1180                 return 1.0f;
1181             }
1182 
1183             final int left = getChildAt(0).getLeft();
1184             final int paddingLeft = getPaddingLeft();
1185 
1186             final float length = (float) getHorizontalFadingEdgeLength();
1187 
1188             return (left < paddingLeft ? (float) -(left - paddingLeft) / length : fadingEdge);
1189         }
1190     }
1191 
1192     @Override
getRightFadingEdgeStrength()1193     protected float getRightFadingEdgeStrength() {
1194         if (mIsVertical) {
1195             return 0f;
1196         }
1197 
1198         final float fadingEdge = super.getRightFadingEdgeStrength();
1199 
1200         final int childCount = getChildCount();
1201         if (childCount == 0) {
1202             return fadingEdge;
1203         } else {
1204             if (mFirstPosition + childCount - 1 < mItemCount - 1) {
1205                 return 1.0f;
1206             }
1207 
1208             final int right = getChildAt(childCount - 1).getRight();
1209             final int paddingRight = getPaddingRight();
1210 
1211             final int width = getWidth();
1212             final float length = (float) getHorizontalFadingEdgeLength();
1213 
1214             return (right > width - paddingRight ?
1215                     (float) (right - width + paddingRight) / length : fadingEdge);
1216         }
1217     }
1218 
1219     @Override
computeVerticalScrollExtent()1220     protected int computeVerticalScrollExtent() {
1221         final int count = getChildCount();
1222         if (count == 0) {
1223             return 0;
1224         }
1225 
1226         int extent = count * 100;
1227 
1228         View child = getChildAt(0);
1229         final int childTop = child.getTop();
1230 
1231         int childHeight = child.getHeight();
1232         if (childHeight > 0) {
1233             extent += (childTop * 100) / childHeight;
1234         }
1235 
1236         child = getChildAt(count - 1);
1237         final int childBottom = child.getBottom();
1238 
1239         childHeight = child.getHeight();
1240         if (childHeight > 0) {
1241             extent -= ((childBottom - getHeight()) * 100) / childHeight;
1242         }
1243 
1244         return extent;
1245     }
1246 
1247     @Override
computeHorizontalScrollExtent()1248     protected int computeHorizontalScrollExtent() {
1249         final int count = getChildCount();
1250         if (count == 0) {
1251             return 0;
1252         }
1253 
1254         int extent = count * 100;
1255 
1256         View child = getChildAt(0);
1257         final int childLeft = child.getLeft();
1258 
1259         int childWidth = child.getWidth();
1260         if (childWidth > 0) {
1261             extent += (childLeft * 100) / childWidth;
1262         }
1263 
1264         child = getChildAt(count - 1);
1265         final int childRight = child.getRight();
1266 
1267         childWidth = child.getWidth();
1268         if (childWidth > 0) {
1269             extent -= ((childRight - getWidth()) * 100) / childWidth;
1270         }
1271 
1272         return extent;
1273     }
1274 
1275     @Override
computeVerticalScrollOffset()1276     protected int computeVerticalScrollOffset() {
1277         final int firstPosition = mFirstPosition;
1278         final int childCount = getChildCount();
1279 
1280         if (firstPosition < 0 || childCount == 0) {
1281             return 0;
1282         }
1283 
1284         final View child = getChildAt(0);
1285         final int childTop = child.getTop();
1286 
1287         int childHeight = child.getHeight();
1288         if (childHeight > 0) {
1289             return Math.max(firstPosition * 100 - (childTop * 100) / childHeight, 0);
1290         }
1291 
1292         return 0;
1293     }
1294 
1295     @Override
computeHorizontalScrollOffset()1296     protected int computeHorizontalScrollOffset() {
1297         final int firstPosition = mFirstPosition;
1298         final int childCount = getChildCount();
1299 
1300         if (firstPosition < 0 || childCount == 0) {
1301             return 0;
1302         }
1303 
1304         final View child = getChildAt(0);
1305         final int childLeft = child.getLeft();
1306 
1307         int childWidth = child.getWidth();
1308         if (childWidth > 0) {
1309             return Math.max(firstPosition * 100 - (childLeft * 100) / childWidth, 0);
1310         }
1311 
1312         return 0;
1313     }
1314 
1315     @Override
computeVerticalScrollRange()1316     protected int computeVerticalScrollRange() {
1317         int result = Math.max(mItemCount * 100, 0);
1318 
1319         if (mIsVertical && mOverScroll != 0) {
1320             // Compensate for overscroll
1321             result += Math.abs((int) ((float) mOverScroll / getHeight() * mItemCount * 100));
1322         }
1323 
1324         return result;
1325     }
1326 
1327     @Override
computeHorizontalScrollRange()1328     protected int computeHorizontalScrollRange() {
1329         int result = Math.max(mItemCount * 100, 0);
1330 
1331         if (!mIsVertical && mOverScroll != 0) {
1332             // Compensate for overscroll
1333             result += Math.abs((int) ((float) mOverScroll / getWidth() * mItemCount * 100));
1334         }
1335 
1336         return result;
1337     }
1338 
1339     @Override
showContextMenuForChild(View originalView)1340     public boolean showContextMenuForChild(View originalView) {
1341         final int longPressPosition = getPositionForView(originalView);
1342         if (longPressPosition >= 0) {
1343             final long longPressId = mAdapter.getItemId(longPressPosition);
1344             boolean handled = false;
1345 
1346             OnItemLongClickListener listener = getOnItemLongClickListener();
1347             if (listener != null) {
1348                 handled = listener.onItemLongClick(TwoWayView.this, originalView,
1349                         longPressPosition, longPressId);
1350             }
1351 
1352             if (!handled) {
1353                 mContextMenuInfo = createContextMenuInfo(
1354                         getChildAt(longPressPosition - mFirstPosition),
1355                         longPressPosition, longPressId);
1356 
1357                 handled = super.showContextMenuForChild(originalView);
1358             }
1359 
1360             return handled;
1361         }
1362 
1363         return false;
1364     }
1365 
1366     @Override
requestDisallowInterceptTouchEvent(boolean disallowIntercept)1367     public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
1368         if (disallowIntercept) {
1369             recycleVelocityTracker();
1370         }
1371 
1372         super.requestDisallowInterceptTouchEvent(disallowIntercept);
1373     }
1374 
1375     @Override
onInterceptTouchEvent(MotionEvent ev)1376     public boolean onInterceptTouchEvent(MotionEvent ev) {
1377         if (!mIsAttached || mAdapter == null) {
1378             return false;
1379         }
1380 
1381         final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
1382         switch (action) {
1383         case MotionEvent.ACTION_DOWN:
1384             initOrResetVelocityTracker();
1385             mVelocityTracker.addMovement(ev);
1386 
1387             mScroller.abortAnimation();
1388             if (mPositionScroller != null) {
1389                 mPositionScroller.stop();
1390             }
1391 
1392             final float x = ev.getX();
1393             final float y = ev.getY();
1394 
1395             mLastTouchPos = (mIsVertical ? y : x);
1396 
1397             final int motionPosition = findMotionRowOrColumn((int) mLastTouchPos);
1398 
1399             mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
1400             mTouchRemainderPos = 0;
1401 
1402             if (mTouchMode == TOUCH_MODE_FLINGING) {
1403                 return true;
1404             } else if (motionPosition >= 0) {
1405                 mMotionPosition = motionPosition;
1406                 mTouchMode = TOUCH_MODE_DOWN;
1407             }
1408 
1409             break;
1410 
1411         case MotionEvent.ACTION_MOVE: {
1412             if (mTouchMode != TOUCH_MODE_DOWN) {
1413                 break;
1414             }
1415 
1416             initVelocityTrackerIfNotExists();
1417             mVelocityTracker.addMovement(ev);
1418 
1419             final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
1420             if (index < 0) {
1421                 Log.e(LOGTAG, "onInterceptTouchEvent could not find pointer with id " +
1422                         mActivePointerId + " - did TwoWayView receive an inconsistent " +
1423                         "event stream?");
1424                 return false;
1425             }
1426 
1427             final float pos;
1428             if (mIsVertical) {
1429                 pos = MotionEventCompat.getY(ev, index);
1430             } else {
1431                 pos = MotionEventCompat.getX(ev, index);
1432             }
1433 
1434             final float diff = pos - mLastTouchPos + mTouchRemainderPos;
1435             final int delta = (int) diff;
1436             mTouchRemainderPos = diff - delta;
1437 
1438             if (maybeStartScrolling(delta)) {
1439                 return true;
1440             }
1441 
1442             break;
1443         }
1444 
1445         case MotionEvent.ACTION_CANCEL:
1446         case MotionEvent.ACTION_UP:
1447             mActivePointerId = INVALID_POINTER;
1448             mTouchMode = TOUCH_MODE_REST;
1449             recycleVelocityTracker();
1450             reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
1451 
1452             break;
1453         }
1454 
1455         return false;
1456     }
1457 
1458     @Override
onTouchEvent(MotionEvent ev)1459     public boolean onTouchEvent(MotionEvent ev) {
1460         if (!isEnabled()) {
1461             // A disabled view that is clickable still consumes the touch
1462             // events, it just doesn't respond to them.
1463             return isClickable() || isLongClickable();
1464         }
1465 
1466         if (!mIsAttached || mAdapter == null) {
1467             return false;
1468         }
1469 
1470         boolean needsInvalidate = false;
1471 
1472         initVelocityTrackerIfNotExists();
1473         mVelocityTracker.addMovement(ev);
1474 
1475         final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
1476         switch (action) {
1477         case MotionEvent.ACTION_DOWN: {
1478             if (mDataChanged) {
1479                 break;
1480             }
1481 
1482             mVelocityTracker.clear();
1483             mScroller.abortAnimation();
1484             if (mPositionScroller != null) {
1485                 mPositionScroller.stop();
1486             }
1487 
1488             final float x = ev.getX();
1489             final float y = ev.getY();
1490 
1491             mLastTouchPos = (mIsVertical ? y : x);
1492 
1493             int motionPosition = pointToPosition((int) x, (int) y);
1494 
1495             mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
1496             mTouchRemainderPos = 0;
1497 
1498             if (mDataChanged) {
1499                 break;
1500             }
1501 
1502             if (mTouchMode == TOUCH_MODE_FLINGING) {
1503                 mTouchMode = TOUCH_MODE_DRAGGING;
1504                 reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
1505                 motionPosition = findMotionRowOrColumn((int) mLastTouchPos);
1506             } else if (mMotionPosition >= 0 && mAdapter.isEnabled(mMotionPosition)) {
1507                 mTouchMode = TOUCH_MODE_DOWN;
1508                 triggerCheckForTap();
1509             }
1510 
1511             mMotionPosition = motionPosition;
1512 
1513             break;
1514         }
1515 
1516         case MotionEvent.ACTION_MOVE: {
1517             final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
1518             if (index < 0) {
1519                 Log.e(LOGTAG, "onInterceptTouchEvent could not find pointer with id " +
1520                         mActivePointerId + " - did TwoWayView receive an inconsistent " +
1521                         "event stream?");
1522                 return false;
1523             }
1524 
1525             final float pos;
1526             if (mIsVertical) {
1527                 pos = MotionEventCompat.getY(ev, index);
1528             } else {
1529                 pos = MotionEventCompat.getX(ev, index);
1530             }
1531 
1532             if (mDataChanged) {
1533                 // Re-sync everything if data has been changed
1534                 // since the scroll operation can query the adapter.
1535                 layoutChildren();
1536             }
1537 
1538             final float diff = pos - mLastTouchPos + mTouchRemainderPos;
1539             final int delta = (int) diff;
1540             mTouchRemainderPos = diff - delta;
1541 
1542             switch (mTouchMode) {
1543             case TOUCH_MODE_DOWN:
1544             case TOUCH_MODE_TAP:
1545             case TOUCH_MODE_DONE_WAITING:
1546                 // Check if we have moved far enough that it looks more like a
1547                 // scroll than a tap
1548                 maybeStartScrolling(delta);
1549                 break;
1550 
1551             case TOUCH_MODE_DRAGGING:
1552             case TOUCH_MODE_OVERSCROLL:
1553                 mLastTouchPos = pos;
1554                 maybeScroll(delta);
1555                 break;
1556             }
1557 
1558             break;
1559         }
1560 
1561         case MotionEvent.ACTION_CANCEL:
1562             cancelCheckForTap();
1563             mTouchMode = TOUCH_MODE_REST;
1564             reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
1565 
1566             setPressed(false);
1567             View motionView = this.getChildAt(mMotionPosition - mFirstPosition);
1568             if (motionView != null) {
1569                 motionView.setPressed(false);
1570             }
1571 
1572             if (mStartEdge != null && mEndEdge != null) {
1573                 needsInvalidate = mStartEdge.onRelease() | mEndEdge.onRelease();
1574             }
1575 
1576             recycleVelocityTracker();
1577 
1578             break;
1579 
1580         case MotionEvent.ACTION_UP: {
1581             switch (mTouchMode) {
1582             case TOUCH_MODE_DOWN:
1583             case TOUCH_MODE_TAP:
1584             case TOUCH_MODE_DONE_WAITING: {
1585                 final int motionPosition = mMotionPosition;
1586                 final View child = getChildAt(motionPosition - mFirstPosition);
1587 
1588                 final float x = ev.getX();
1589                 final float y = ev.getY();
1590 
1591                 final boolean inList;
1592                 if (mIsVertical) {
1593                     inList = x > getPaddingLeft() && x < getWidth() - getPaddingRight();
1594                 } else {
1595                     inList = y > getPaddingTop() && y < getHeight() - getPaddingBottom();
1596                 }
1597 
1598                 if (child != null && !child.hasFocusable() && inList) {
1599                     if (mTouchMode != TOUCH_MODE_DOWN) {
1600                         child.setPressed(false);
1601                     }
1602 
1603                     if (mPerformClick == null) {
1604                         mPerformClick = new PerformClick();
1605                     }
1606 
1607                     final PerformClick performClick = mPerformClick;
1608                     performClick.mClickMotionPosition = motionPosition;
1609                     performClick.rememberWindowAttachCount();
1610 
1611                     mResurrectToPosition = motionPosition;
1612 
1613                     if (mTouchMode == TOUCH_MODE_DOWN || mTouchMode == TOUCH_MODE_TAP) {
1614                         if (mTouchMode == TOUCH_MODE_DOWN) {
1615                             cancelCheckForTap();
1616                         } else {
1617                             cancelCheckForLongPress();
1618                         }
1619 
1620                         mLayoutMode = LAYOUT_NORMAL;
1621 
1622                         if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {
1623                             mTouchMode = TOUCH_MODE_TAP;
1624 
1625                             setPressed(true);
1626                             positionSelector(mMotionPosition, child);
1627                             child.setPressed(true);
1628 
1629                             if (mSelector != null) {
1630                                 Drawable d = mSelector.getCurrent();
1631                                 if (d != null && d instanceof TransitionDrawable) {
1632                                     ((TransitionDrawable) d).resetTransition();
1633                                 }
1634                             }
1635 
1636                             if (mTouchModeReset != null) {
1637                                 removeCallbacks(mTouchModeReset);
1638                             }
1639 
1640                             mTouchModeReset = new Runnable() {
1641                                 @Override
1642                                 public void run() {
1643                                     mTouchMode = TOUCH_MODE_REST;
1644 
1645                                     setPressed(false);
1646                                     child.setPressed(false);
1647 
1648                                     if (!mDataChanged) {
1649                                         performClick.run();
1650                                     }
1651 
1652                                     mTouchModeReset = null;
1653                                 }
1654                             };
1655 
1656                             postDelayed(mTouchModeReset,
1657                                     ViewConfiguration.getPressedStateDuration());
1658                         } else {
1659                             mTouchMode = TOUCH_MODE_REST;
1660                             updateSelectorState();
1661                         }
1662                     } else if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {
1663                         performClick.run();
1664                     }
1665                 }
1666 
1667                 mTouchMode = TOUCH_MODE_REST;
1668 
1669                 finishSmoothScrolling();
1670                 updateSelectorState();
1671 
1672                 break;
1673             }
1674 
1675             case TOUCH_MODE_DRAGGING:
1676                 if (contentFits()) {
1677                     mTouchMode = TOUCH_MODE_REST;
1678                     reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
1679                     break;
1680                 }
1681 
1682                 mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
1683 
1684                 final float velocity;
1685                 if (mIsVertical) {
1686                     velocity = VelocityTrackerCompat.getYVelocity(mVelocityTracker,
1687                             mActivePointerId);
1688                 } else {
1689                     velocity = VelocityTrackerCompat.getXVelocity(mVelocityTracker,
1690                             mActivePointerId);
1691                 }
1692 
1693                 if (Math.abs(velocity) >= mFlingVelocity) {
1694                     mTouchMode = TOUCH_MODE_FLINGING;
1695                     reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
1696 
1697                     mScroller.fling(0, 0,
1698                                     (int) (mIsVertical ? 0 : velocity),
1699                                     (int) (mIsVertical ? velocity : 0),
1700                                     (mIsVertical ? 0 : Integer.MIN_VALUE),
1701                                     (mIsVertical ? 0 : Integer.MAX_VALUE),
1702                                     (mIsVertical ? Integer.MIN_VALUE : 0),
1703                                     (mIsVertical ? Integer.MAX_VALUE : 0));
1704 
1705                     mLastTouchPos = 0;
1706                     needsInvalidate = true;
1707                 } else {
1708                     mTouchMode = TOUCH_MODE_REST;
1709                     reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
1710                 }
1711 
1712                 break;
1713 
1714             case TOUCH_MODE_OVERSCROLL:
1715                 mTouchMode = TOUCH_MODE_REST;
1716                 reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
1717                 break;
1718             }
1719 
1720             cancelCheckForTap();
1721             cancelCheckForLongPress();
1722             setPressed(false);
1723 
1724             if (mStartEdge != null && mEndEdge != null) {
1725                 needsInvalidate |= mStartEdge.onRelease() | mEndEdge.onRelease();
1726             }
1727 
1728             recycleVelocityTracker();
1729 
1730             break;
1731         }
1732         }
1733 
1734         if (needsInvalidate) {
1735             ViewCompat.postInvalidateOnAnimation(this);
1736         }
1737 
1738         return true;
1739     }
1740 
1741     @Override
onTouchModeChanged(boolean isInTouchMode)1742     public void onTouchModeChanged(boolean isInTouchMode) {
1743         if (isInTouchMode) {
1744             // Get rid of the selection when we enter touch mode
1745             hideSelector();
1746 
1747             // Layout, but only if we already have done so previously.
1748             // (Otherwise may clobber a LAYOUT_SYNC layout that was requested to restore
1749             // state.)
1750             if (getWidth() > 0 && getHeight() > 0 && getChildCount() > 0) {
1751                 layoutChildren();
1752             }
1753 
1754             updateSelectorState();
1755         } else {
1756             final int touchMode = mTouchMode;
1757             if (touchMode == TOUCH_MODE_OVERSCROLL) {
1758                 finishSmoothScrolling();
1759                 if (mOverScroll != 0) {
1760                     mOverScroll = 0;
1761                     finishEdgeGlows();
1762                     invalidate();
1763                 }
1764             }
1765         }
1766     }
1767 
1768     @Override
onKeyDown(int keyCode, KeyEvent event)1769     public boolean onKeyDown(int keyCode, KeyEvent event) {
1770         return handleKeyEvent(keyCode, 1, event);
1771     }
1772 
1773     @Override
onKeyMultiple(int keyCode, int repeatCount, KeyEvent event)1774     public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
1775         return handleKeyEvent(keyCode, repeatCount, event);
1776     }
1777 
1778     @Override
onKeyUp(int keyCode, KeyEvent event)1779     public boolean onKeyUp(int keyCode, KeyEvent event) {
1780         return handleKeyEvent(keyCode, 1, event);
1781     }
1782 
1783     @Override
sendAccessibilityEvent(int eventType)1784     public void sendAccessibilityEvent(int eventType) {
1785         // Since this class calls onScrollChanged even if the mFirstPosition and the
1786         // child count have not changed we will avoid sending duplicate accessibility
1787         // events.
1788         if (eventType == AccessibilityEvent.TYPE_VIEW_SCROLLED) {
1789             final int firstVisiblePosition = getFirstVisiblePosition();
1790             final int lastVisiblePosition = getLastVisiblePosition();
1791 
1792             if (mLastAccessibilityScrollEventFromIndex == firstVisiblePosition
1793                     && mLastAccessibilityScrollEventToIndex == lastVisiblePosition) {
1794                 return;
1795             } else {
1796                 mLastAccessibilityScrollEventFromIndex = firstVisiblePosition;
1797                 mLastAccessibilityScrollEventToIndex = lastVisiblePosition;
1798             }
1799         }
1800 
1801         super.sendAccessibilityEvent(eventType);
1802     }
1803 
1804     @Override
1805     @TargetApi(14)
onInitializeAccessibilityEvent(AccessibilityEvent event)1806     public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
1807         super.onInitializeAccessibilityEvent(event);
1808         event.setClassName(TwoWayView.class.getName());
1809     }
1810 
1811     @Override
1812     @TargetApi(14)
onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)1813     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
1814         super.onInitializeAccessibilityNodeInfo(info);
1815         info.setClassName(TwoWayView.class.getName());
1816 
1817         AccessibilityNodeInfoCompat infoCompat = new AccessibilityNodeInfoCompat(info);
1818 
1819         if (isEnabled()) {
1820             if (getFirstVisiblePosition() > 0) {
1821                 infoCompat.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD);
1822             }
1823 
1824             if (getLastVisiblePosition() < getCount() - 1) {
1825                 infoCompat.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD);
1826             }
1827         }
1828     }
1829 
1830     @Override
1831     @TargetApi(16)
performAccessibilityAction(int action, Bundle arguments)1832     public boolean performAccessibilityAction(int action, Bundle arguments) {
1833         if (super.performAccessibilityAction(action, arguments)) {
1834             return true;
1835         }
1836 
1837         switch (action) {
1838         case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
1839             if (isEnabled() && getLastVisiblePosition() < getCount() - 1) {
1840                 // TODO: Use some form of smooth scroll instead
1841                 scrollListItemsBy(getAvailableSize());
1842                 return true;
1843             }
1844             return false;
1845 
1846         case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD:
1847             if (isEnabled() && mFirstPosition > 0) {
1848                 // TODO: Use some form of smooth scroll instead
1849                 scrollListItemsBy(-getAvailableSize());
1850                 return true;
1851             }
1852             return false;
1853         }
1854 
1855         return false;
1856     }
1857 
1858     /**
1859      * Return true if child is an ancestor of parent, (or equal to the parent).
1860      */
isViewAncestorOf(View child, View parent)1861     private boolean isViewAncestorOf(View child, View parent) {
1862         if (child == parent) {
1863             return true;
1864         }
1865 
1866         final ViewParent theParent = child.getParent();
1867 
1868         return (theParent instanceof ViewGroup) &&
1869                  isViewAncestorOf((View) theParent, parent);
1870     }
1871 
forceValidFocusDirection(int direction)1872     private void forceValidFocusDirection(int direction) {
1873         if (mIsVertical && direction != View.FOCUS_UP && direction != View.FOCUS_DOWN)  {
1874             throw new IllegalArgumentException("Focus direction must be one of"
1875                     + " {View.FOCUS_UP, View.FOCUS_DOWN} for vertical orientation");
1876         } else if (!mIsVertical && direction != View.FOCUS_LEFT && direction != View.FOCUS_RIGHT)  {
1877             throw new IllegalArgumentException("Focus direction must be one of"
1878                     + " {View.FOCUS_LEFT, View.FOCUS_RIGHT} for vertical orientation");
1879         }
1880     }
1881 
forceValidInnerFocusDirection(int direction)1882     private void forceValidInnerFocusDirection(int direction) {
1883         if (mIsVertical && direction != View.FOCUS_LEFT && direction != View.FOCUS_RIGHT)  {
1884             throw new IllegalArgumentException("Direction must be one of"
1885                     + " {View.FOCUS_LEFT, View.FOCUS_RIGHT} for vertical orientation");
1886         } else if (!mIsVertical && direction != View.FOCUS_UP && direction != View.FOCUS_DOWN)  {
1887             throw new IllegalArgumentException("direction must be one of"
1888                     + " {View.FOCUS_UP, View.FOCUS_DOWN} for horizontal orientation");
1889         }
1890     }
1891 
1892     /**
1893      * Scrolls up or down by the number of items currently present on screen.
1894      *
1895      * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or
1896      *        {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the
1897      *        current view orientation.
1898      *
1899      * @return whether selection was moved
1900      */
pageScroll(int direction)1901     boolean pageScroll(int direction) {
1902         forceValidFocusDirection(direction);
1903 
1904         boolean forward = false;
1905         int nextPage = -1;
1906 
1907         if (direction == View.FOCUS_UP || direction == View.FOCUS_LEFT) {
1908             nextPage = Math.max(0, mSelectedPosition - getChildCount() - 1);
1909         } else if (direction == View.FOCUS_DOWN || direction == View.FOCUS_RIGHT) {
1910             nextPage = Math.min(mItemCount - 1, mSelectedPosition + getChildCount() - 1);
1911             forward = true;
1912         }
1913 
1914         if (nextPage < 0) {
1915             return false;
1916         }
1917 
1918         final int position = lookForSelectablePosition(nextPage, forward);
1919         if (position >= 0) {
1920             mLayoutMode = LAYOUT_SPECIFIC;
1921             mSpecificStart = getStartEdge() + getFadingEdgeLength();
1922 
1923             if (forward && position > mItemCount - getChildCount()) {
1924                 mLayoutMode = LAYOUT_FORCE_BOTTOM;
1925             }
1926 
1927             if (!forward && position < getChildCount()) {
1928                 mLayoutMode = LAYOUT_FORCE_TOP;
1929             }
1930 
1931             setSelectionInt(position);
1932             invokeOnItemScrollListener();
1933 
1934             if (!awakenScrollbarsInternal()) {
1935                 invalidate();
1936             }
1937 
1938             return true;
1939         }
1940 
1941         return false;
1942     }
1943 
1944     /**
1945      * Go to the last or first item if possible (not worrying about panning across or navigating
1946      * within the internal focus of the currently selected item.)
1947      *
1948      * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or
1949      *        {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the
1950      *        current view orientation.
1951      *
1952      * @return whether selection was moved
1953      */
fullScroll(int direction)1954     boolean fullScroll(int direction) {
1955         forceValidFocusDirection(direction);
1956 
1957         boolean moved = false;
1958         if (direction == View.FOCUS_UP || direction == View.FOCUS_LEFT) {
1959             if (mSelectedPosition != 0) {
1960                 int position = lookForSelectablePosition(0, true);
1961                 if (position >= 0) {
1962                     mLayoutMode = LAYOUT_FORCE_TOP;
1963                     setSelectionInt(position);
1964                     invokeOnItemScrollListener();
1965                 }
1966 
1967                 moved = true;
1968             }
1969         } else if (direction == View.FOCUS_DOWN || direction == View.FOCUS_RIGHT) {
1970             if (mSelectedPosition < mItemCount - 1) {
1971                 int position = lookForSelectablePosition(mItemCount - 1, true);
1972                 if (position >= 0) {
1973                     mLayoutMode = LAYOUT_FORCE_BOTTOM;
1974                     setSelectionInt(position);
1975                     invokeOnItemScrollListener();
1976                 }
1977 
1978                 moved = true;
1979             }
1980         }
1981 
1982         if (moved && !awakenScrollbarsInternal()) {
1983             awakenScrollbarsInternal();
1984             invalidate();
1985         }
1986 
1987         return moved;
1988     }
1989 
1990     /**
1991      * To avoid horizontal/vertical focus searches changing the selected item,
1992      * we manually focus search within the selected item (as applicable), and
1993      * prevent focus from jumping to something within another item.
1994      *
1995      * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or
1996      *        {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the
1997      *        current view orientation.
1998      *
1999      * @return Whether this consumes the key event.
2000      */
handleFocusWithinItem(int direction)2001     private boolean handleFocusWithinItem(int direction) {
2002         forceValidInnerFocusDirection(direction);
2003 
2004         final int numChildren = getChildCount();
2005 
2006         if (mItemsCanFocus && numChildren > 0 && mSelectedPosition != INVALID_POSITION) {
2007             final View selectedView = getSelectedView();
2008 
2009             if (selectedView != null && selectedView.hasFocus() &&
2010                     selectedView instanceof ViewGroup) {
2011 
2012                 final View currentFocus = selectedView.findFocus();
2013                 final View nextFocus = FocusFinder.getInstance().findNextFocus(
2014                         (ViewGroup) selectedView, currentFocus, direction);
2015 
2016                 if (nextFocus != null) {
2017                     // Do the math to get interesting rect in next focus' coordinates
2018                     currentFocus.getFocusedRect(mTempRect);
2019                     offsetDescendantRectToMyCoords(currentFocus, mTempRect);
2020                     offsetRectIntoDescendantCoords(nextFocus, mTempRect);
2021 
2022                     if (nextFocus.requestFocus(direction, mTempRect)) {
2023                         return true;
2024                     }
2025                 }
2026 
2027                 // We are blocking the key from being handled (by returning true)
2028                 // if the global result is going to be some other view within this
2029                 // list. This is to achieve the overall goal of having horizontal/vertical
2030                 // d-pad navigation remain in the current item depending on the current
2031                 // orientation in this view.
2032                 final View globalNextFocus = FocusFinder.getInstance().findNextFocus(
2033                         (ViewGroup) getRootView(), currentFocus, direction);
2034 
2035                 if (globalNextFocus != null) {
2036                     return isViewAncestorOf(globalNextFocus, this);
2037                 }
2038             }
2039         }
2040 
2041         return false;
2042     }
2043 
2044     /**
2045      * Scrolls to the next or previous item if possible.
2046      *
2047      * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or
2048      *        {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the
2049      *        current view orientation.
2050      *
2051      * @return whether selection was moved
2052      */
arrowScroll(int direction)2053     private boolean arrowScroll(int direction) {
2054         forceValidFocusDirection(direction);
2055 
2056         try {
2057             mInLayout = true;
2058 
2059             final boolean handled = arrowScrollImpl(direction);
2060             if (handled) {
2061                 playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction));
2062             }
2063 
2064             return handled;
2065         } finally {
2066             mInLayout = false;
2067         }
2068     }
2069 
2070     /**
2071      * When selection changes, it is possible that the previously selected or the
2072      * next selected item will change its size.  If so, we need to offset some folks,
2073      * and re-layout the items as appropriate.
2074      *
2075      * @param selectedView The currently selected view (before changing selection).
2076      *   should be <code>null</code> if there was no previous selection.
2077      * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or
2078      *        {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the
2079      *        current view orientation.
2080      * @param newSelectedPosition The position of the next selection.
2081      * @param newFocusAssigned whether new focus was assigned.  This matters because
2082      *        when something has focus, we don't want to show selection (ugh).
2083      */
handleNewSelectionChange(View selectedView, int direction, int newSelectedPosition, boolean newFocusAssigned)2084     private void handleNewSelectionChange(View selectedView, int direction, int newSelectedPosition,
2085             boolean newFocusAssigned) {
2086         forceValidFocusDirection(direction);
2087 
2088         if (newSelectedPosition == INVALID_POSITION) {
2089             throw new IllegalArgumentException("newSelectedPosition needs to be valid");
2090         }
2091 
2092         // Whether or not we are moving down/right or up/left, we want to preserve the
2093         // top/left of whatever view is at the start:
2094         // - moving down/right: the view that had selection
2095         // - moving up/left: the view that is getting selection
2096         final int selectedIndex = mSelectedPosition - mFirstPosition;
2097         final int nextSelectedIndex = newSelectedPosition - mFirstPosition;
2098         int startViewIndex, endViewIndex;
2099         boolean topSelected = false;
2100         View startView;
2101         View endView;
2102 
2103         if (direction == View.FOCUS_UP || direction == View.FOCUS_LEFT) {
2104             startViewIndex = nextSelectedIndex;
2105             endViewIndex = selectedIndex;
2106             startView = getChildAt(startViewIndex);
2107             endView = selectedView;
2108             topSelected = true;
2109         } else {
2110             startViewIndex = selectedIndex;
2111             endViewIndex = nextSelectedIndex;
2112             startView = selectedView;
2113             endView = getChildAt(endViewIndex);
2114         }
2115 
2116         final int numChildren = getChildCount();
2117 
2118         // start with top view: is it changing size?
2119         if (startView != null) {
2120             startView.setSelected(!newFocusAssigned && topSelected);
2121             measureAndAdjustDown(startView, startViewIndex, numChildren);
2122         }
2123 
2124         // is the bottom view changing size?
2125         if (endView != null) {
2126             endView.setSelected(!newFocusAssigned && !topSelected);
2127             measureAndAdjustDown(endView, endViewIndex, numChildren);
2128         }
2129     }
2130 
2131     /**
2132      * Re-measure a child, and if its height changes, lay it out preserving its
2133      * top, and adjust the children below it appropriately.
2134      *
2135      * @param child The child
2136      * @param childIndex The view group index of the child.
2137      * @param numChildren The number of children in the view group.
2138      */
measureAndAdjustDown(View child, int childIndex, int numChildren)2139     private void measureAndAdjustDown(View child, int childIndex, int numChildren) {
2140         int oldSize = getChildSize(child);
2141         measureChild(child);
2142 
2143         if (getChildMeasuredSize(child) == oldSize) {
2144             return;
2145         }
2146 
2147         // lay out the view, preserving its top
2148         relayoutMeasuredChild(child);
2149 
2150         // adjust views below appropriately
2151         final int sizeDelta = getChildMeasuredSize(child) - oldSize;
2152         for (int i = childIndex + 1; i < numChildren; i++) {
2153             getChildAt(i).offsetTopAndBottom(sizeDelta);
2154         }
2155     }
2156 
2157     /**
2158      * Do an arrow scroll based on focus searching.  If a new view is
2159      * given focus, return the selection delta and amount to scroll via
2160      * an {@link ArrowScrollFocusResult}, otherwise, return null.
2161      *
2162      * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or
2163      *        {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the
2164      *        current view orientation.
2165      *
2166      * @return The result if focus has changed, or <code>null</code>.
2167      */
arrowScrollFocused(final int direction)2168     private ArrowScrollFocusResult arrowScrollFocused(final int direction) {
2169         forceValidFocusDirection(direction);
2170 
2171         final View selectedView = getSelectedView();
2172         final View newFocus;
2173         final int searchPoint;
2174 
2175         if (selectedView != null && selectedView.hasFocus()) {
2176             View oldFocus = selectedView.findFocus();
2177             newFocus = FocusFinder.getInstance().findNextFocus(this, oldFocus, direction);
2178         } else {
2179             if (direction == View.FOCUS_DOWN || direction == View.FOCUS_RIGHT) {
2180                 boolean fadingEdgeShowing = (mFirstPosition > 0);
2181                 final int start = getStartEdge() +
2182                         (fadingEdgeShowing ? getArrowScrollPreviewLength() : 0);
2183 
2184                 final int selectedStart;
2185                 if (selectedView != null) {
2186                     selectedStart = getChildStartEdge(selectedView);
2187                 } else {
2188                     selectedStart = start;
2189                 }
2190 
2191                 searchPoint = Math.max(selectedStart, start);
2192             } else {
2193                 final boolean fadingEdgeShowing =
2194                         (mFirstPosition + getChildCount() - 1) < mItemCount;
2195                 final int end = getEndEdge() - (fadingEdgeShowing ? getArrowScrollPreviewLength() : 0);
2196 
2197                 final int selectedEnd;
2198                 if (selectedView != null) {
2199                     selectedEnd = getChildEndEdge(selectedView);
2200                 } else {
2201                     selectedEnd = end;
2202                 }
2203 
2204                 searchPoint = Math.min(selectedEnd, end);
2205             }
2206 
2207             final int x = (mIsVertical ? 0 : searchPoint);
2208             final int y = (mIsVertical ? searchPoint : 0);
2209             mTempRect.set(x, y, x, y);
2210 
2211             newFocus = FocusFinder.getInstance().findNextFocusFromRect(this, mTempRect, direction);
2212         }
2213 
2214         if (newFocus != null) {
2215             final int positionOfNewFocus = positionOfNewFocus(newFocus);
2216 
2217             // If the focus change is in a different new position, make sure
2218             // we aren't jumping over another selectable position.
2219             if (mSelectedPosition != INVALID_POSITION && positionOfNewFocus != mSelectedPosition) {
2220                 final int selectablePosition = lookForSelectablePositionOnScreen(direction);
2221 
2222                 final boolean movingForward =
2223                         (direction == View.FOCUS_DOWN || direction == View.FOCUS_RIGHT);
2224                 final boolean movingBackward =
2225                         (direction == View.FOCUS_UP || direction == View.FOCUS_LEFT);
2226 
2227                 if (selectablePosition != INVALID_POSITION &&
2228                         ((movingForward && selectablePosition < positionOfNewFocus) ||
2229                          (movingBackward && selectablePosition > positionOfNewFocus))) {
2230                     return null;
2231                 }
2232             }
2233 
2234             int focusScroll = amountToScrollToNewFocus(direction, newFocus, positionOfNewFocus);
2235 
2236             final int maxScrollAmount = getMaxScrollAmount();
2237             if (focusScroll < maxScrollAmount) {
2238                 // Not moving too far, safe to give next view focus
2239                 newFocus.requestFocus(direction);
2240                 mArrowScrollFocusResult.populate(positionOfNewFocus, focusScroll);
2241                 return mArrowScrollFocusResult;
2242             } else if (distanceToView(newFocus) < maxScrollAmount) {
2243                 // Case to consider:
2244                 // Too far to get entire next focusable on screen, but by going
2245                 // max scroll amount, we are getting it at least partially in view,
2246                 // so give it focus and scroll the max amount.
2247                 newFocus.requestFocus(direction);
2248                 mArrowScrollFocusResult.populate(positionOfNewFocus, maxScrollAmount);
2249                 return mArrowScrollFocusResult;
2250             }
2251         }
2252 
2253         return null;
2254     }
2255 
2256     /**
2257      * @return The maximum amount a list view will scroll in response to
2258      *   an arrow event.
2259      */
2260     public int getMaxScrollAmount() {
2261         return (int) (MAX_SCROLL_FACTOR * getSize());
2262     }
2263 
2264     /**
2265      * @return The amount to preview next items when arrow scrolling.
2266      */
2267     private int getArrowScrollPreviewLength() {
2268         return mItemMargin + Math.max(MIN_SCROLL_PREVIEW_PIXELS, getFadingEdgeLength());
2269     }
2270 
2271     /**
2272      * @param newFocus The view that would have focus.
2273      * @return the position that contains newFocus
2274      */
2275     private int positionOfNewFocus(View newFocus) {
2276         final int numChildren = getChildCount();
2277 
2278         for (int i = 0; i < numChildren; i++) {
2279             final View child = getChildAt(i);
2280             if (isViewAncestorOf(newFocus, child)) {
2281                 return mFirstPosition + i;
2282             }
2283         }
2284 
2285         throw new IllegalArgumentException("newFocus is not a child of any of the"
2286                 + " children of the list!");
2287     }
2288 
2289     /**
2290      * Handle an arrow scroll going up or down.  Take into account whether items are selectable,
2291      * whether there are focusable items, etc.
2292      *
2293      * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or
2294      *        {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the
2295      *        current view orientation.
2296      *
2297      * @return Whether any scrolling, selection or focus change occurred.
2298      */
2299     private boolean arrowScrollImpl(int direction) {
2300         forceValidFocusDirection(direction);
2301 
2302         if (getChildCount() <= 0) {
2303             return false;
2304         }
2305 
2306         View selectedView = getSelectedView();
2307         int selectedPos = mSelectedPosition;
2308 
2309         int nextSelectedPosition = lookForSelectablePositionOnScreen(direction);
2310         int amountToScroll = amountToScroll(direction, nextSelectedPosition);
2311 
2312         // If we are moving focus, we may OVERRIDE the default behaviour
2313         final ArrowScrollFocusResult focusResult = (mItemsCanFocus ? arrowScrollFocused(direction) : null);
2314         if (focusResult != null) {
2315             nextSelectedPosition = focusResult.getSelectedPosition();
2316             amountToScroll = focusResult.getAmountToScroll();
2317         }
2318 
2319         boolean needToRedraw = (focusResult != null);
2320         if (nextSelectedPosition != INVALID_POSITION) {
2321             handleNewSelectionChange(selectedView, direction, nextSelectedPosition, focusResult != null);
2322 
2323             setSelectedPositionInt(nextSelectedPosition);
2324             setNextSelectedPositionInt(nextSelectedPosition);
2325 
2326             selectedView = getSelectedView();
2327             selectedPos = nextSelectedPosition;
2328 
2329             if (mItemsCanFocus && focusResult == null) {
2330                 // There was no new view found to take focus, make sure we
2331                 // don't leave focus with the old selection.
2332                 final View focused = getFocusedChild();
2333                 if (focused != null) {
2334                     focused.clearFocus();
2335                 }
2336             }
2337 
2338             needToRedraw = true;
2339             checkSelectionChanged();
2340         }
2341 
2342         if (amountToScroll > 0) {
2343             scrollListItemsBy(direction == View.FOCUS_UP || direction == View.FOCUS_LEFT ?
2344                     amountToScroll : -amountToScroll);
2345             needToRedraw = true;
2346         }
2347 
2348         // If we didn't find a new focusable, make sure any existing focused
2349         // item that was panned off screen gives up focus.
2350         if (mItemsCanFocus && focusResult == null &&
2351                 selectedView != null && selectedView.hasFocus()) {
2352             final View focused = selectedView.findFocus();
2353             if (!isViewAncestorOf(focused, this) || distanceToView(focused) > 0) {
2354                 focused.clearFocus();
2355             }
2356         }
2357 
2358         // If the current selection is panned off, we need to remove the selection
2359         if (nextSelectedPosition == INVALID_POSITION && selectedView != null
2360                 && !isViewAncestorOf(selectedView, this)) {
2361             selectedView = null;
2362             hideSelector();
2363 
2364             // But we don't want to set the ressurect position (that would make subsequent
2365             // unhandled key events bring back the item we just scrolled off)
2366             mResurrectToPosition = INVALID_POSITION;
2367         }
2368 
2369         if (needToRedraw) {
2370             if (selectedView != null) {
2371                 positionSelector(selectedPos, selectedView);
2372                 mSelectedStart = getChildStartEdge(selectedView);
2373             }
2374 
2375             if (!awakenScrollbarsInternal()) {
2376                 invalidate();
2377             }
2378 
2379             invokeOnItemScrollListener();
2380             return true;
2381         }
2382 
2383         return false;
2384     }
2385 
2386     /**
2387      * Determine how much we need to scroll in order to get the next selected view
2388      * visible. The amount is capped at {@link #getMaxScrollAmount()}.
2389      *
2390      * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or
2391      *        {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the
2392      *        current view orientation.
2393      * @param nextSelectedPosition The position of the next selection, or
2394      *        {@link #INVALID_POSITION} if there is no next selectable position
2395      *
2396      * @return The amount to scroll. Note: this is always positive!  Direction
2397      *         needs to be taken into account when actually scrolling.
2398      */
2399     private int amountToScroll(int direction, int nextSelectedPosition) {
2400         forceValidFocusDirection(direction);
2401 
2402         final int numChildren = getChildCount();
2403 
2404         if (direction == View.FOCUS_DOWN || direction == View.FOCUS_RIGHT) {
2405             final int end = getEndEdge();
2406 
2407             int indexToMakeVisible = numChildren - 1;
2408             if (nextSelectedPosition != INVALID_POSITION) {
2409                 indexToMakeVisible = nextSelectedPosition - mFirstPosition;
2410             }
2411 
2412             final int positionToMakeVisible = mFirstPosition + indexToMakeVisible;
2413             final View viewToMakeVisible = getChildAt(indexToMakeVisible);
2414 
2415             int goalEnd = end;
2416             if (positionToMakeVisible < mItemCount - 1) {
2417                 goalEnd -= getArrowScrollPreviewLength();
2418             }
2419 
2420             final int viewToMakeVisibleStart = getChildStartEdge(viewToMakeVisible);
2421             final int viewToMakeVisibleEnd = getChildEndEdge(viewToMakeVisible);
2422 
2423             if (viewToMakeVisibleEnd <= goalEnd) {
2424                 // Target item is fully visible
2425                 return 0;
2426             }
2427 
2428             if (nextSelectedPosition != INVALID_POSITION &&
2429                     (goalEnd - viewToMakeVisibleStart) >= getMaxScrollAmount()) {
2430                 // Item already has enough of it visible, changing selection is good enough
2431                 return 0;
2432             }
2433 
2434             int amountToScroll = (viewToMakeVisibleEnd - goalEnd);
2435 
2436             if (mFirstPosition + numChildren == mItemCount) {
2437                 final int lastChildEnd = getChildEndEdge(getChildAt(numChildren - 1));
2438 
2439                 // Last is last in list -> Make sure we don't scroll past it
2440                 final int max = lastChildEnd - end;
2441                 amountToScroll = Math.min(amountToScroll, max);
2442             }
2443 
2444             return Math.min(amountToScroll, getMaxScrollAmount());
2445         } else {
2446             final int start = getStartEdge();
2447 
2448             int indexToMakeVisible = 0;
2449             if (nextSelectedPosition != INVALID_POSITION) {
2450                 indexToMakeVisible = nextSelectedPosition - mFirstPosition;
2451             }
2452 
2453             final int positionToMakeVisible = mFirstPosition + indexToMakeVisible;
2454             final View viewToMakeVisible = getChildAt(indexToMakeVisible);
2455 
2456             int goalStart = start;
2457             if (positionToMakeVisible > 0) {
2458                 goalStart += getArrowScrollPreviewLength();
2459             }
2460 
2461             final int viewToMakeVisibleStart = getChildStartEdge(viewToMakeVisible);
2462             final int viewToMakeVisibleEnd = getChildEndEdge(viewToMakeVisible);
2463 
2464             if (viewToMakeVisibleStart >= goalStart) {
2465                 // Item is fully visible
2466                 return 0;
2467             }
2468 
2469             if (nextSelectedPosition != INVALID_POSITION &&
2470                     (viewToMakeVisibleEnd - goalStart) >= getMaxScrollAmount()) {
2471                 // Item already has enough of it visible, changing selection is good enough
2472                 return 0;
2473             }
2474 
2475             int amountToScroll = (goalStart - viewToMakeVisibleStart);
2476 
2477             if (mFirstPosition == 0) {
2478                 final int firstChildStart = getChildStartEdge(getChildAt(0));
2479 
2480                 // First is first in list -> make sure we don't scroll past it
2481                 final int max = start - firstChildStart;
2482                 amountToScroll = Math.min(amountToScroll,  max);
2483             }
2484 
2485             return Math.min(amountToScroll, getMaxScrollAmount());
2486         }
2487     }
2488 
2489     /**
2490      * Determine how much we need to scroll in order to get newFocus in view.
2491      *
2492      * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or
2493      *        {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the
2494      *        current view orientation.
2495      * @param newFocus The view that would take focus.
2496      * @param positionOfNewFocus The position of the list item containing newFocus
2497      *
2498      * @return The amount to scroll. Note: this is always positive! Direction
2499      *   needs to be taken into account when actually scrolling.
2500      */
2501     private int amountToScrollToNewFocus(int direction, View newFocus, int positionOfNewFocus) {
2502         forceValidFocusDirection(direction);
2503 
2504         int amountToScroll = 0;
2505 
2506         newFocus.getDrawingRect(mTempRect);
2507         offsetDescendantRectToMyCoords(newFocus, mTempRect);
2508 
2509         if (direction == View.FOCUS_UP || direction == View.FOCUS_LEFT) {
2510             final int start = getStartEdge();
2511             final int newFocusStart = (mIsVertical ? mTempRect.top : mTempRect.left);
2512 
2513             if (newFocusStart < start) {
2514                 amountToScroll = start - newFocusStart;
2515                 if (positionOfNewFocus > 0) {
2516                     amountToScroll += getArrowScrollPreviewLength();
2517                 }
2518             }
2519         } else {
2520             final int end = getEndEdge();
2521             final int newFocusEnd = (mIsVertical ? mTempRect.bottom : mTempRect.right);
2522 
2523             if (newFocusEnd > end) {
2524                 amountToScroll = newFocusEnd - end;
2525                 if (positionOfNewFocus < mItemCount - 1) {
2526                     amountToScroll += getArrowScrollPreviewLength();
2527                 }
2528             }
2529         }
2530 
2531         return amountToScroll;
2532     }
2533 
2534     /**
2535      * Determine the distance to the nearest edge of a view in a particular
2536      * direction.
2537      *
2538      * @param descendant A descendant of this list.
2539      * @return The distance, or 0 if the nearest edge is already on screen.
2540      */
distanceToView(View descendant)2541     private int distanceToView(View descendant) {
2542         descendant.getDrawingRect(mTempRect);
2543         offsetDescendantRectToMyCoords(descendant, mTempRect);
2544 
2545         final int start = getStartEdge();
2546         final int end = getEndEdge();
2547 
2548         final int viewStart = (mIsVertical ? mTempRect.top : mTempRect.left);
2549         final int viewEnd = (mIsVertical ? mTempRect.bottom : mTempRect.right);
2550 
2551         int distance = 0;
2552         if (viewEnd < start) {
2553             distance = start - viewEnd;
2554         } else if (viewStart > end) {
2555             distance = viewStart - end;
2556         }
2557 
2558         return distance;
2559     }
2560 
handleKeyScroll(KeyEvent event, int count, int direction)2561     private boolean handleKeyScroll(KeyEvent event, int count, int direction) {
2562         boolean handled = false;
2563 
2564         if (KeyEventCompat.hasNoModifiers(event)) {
2565             handled = resurrectSelectionIfNeeded();
2566             if (!handled) {
2567                 while (count-- > 0) {
2568                     if (arrowScroll(direction)) {
2569                         handled = true;
2570                     } else {
2571                         break;
2572                     }
2573                 }
2574             }
2575         } else if (KeyEventCompat.hasModifiers(event, KeyEvent.META_ALT_ON)) {
2576             handled = resurrectSelectionIfNeeded() || fullScroll(direction);
2577         }
2578 
2579         return handled;
2580     }
2581 
handleKeyEvent(int keyCode, int count, KeyEvent event)2582     private boolean handleKeyEvent(int keyCode, int count, KeyEvent event) {
2583         if (mAdapter == null || !mIsAttached) {
2584             return false;
2585         }
2586 
2587         if (mDataChanged) {
2588             layoutChildren();
2589         }
2590 
2591         boolean handled = false;
2592         final int action = event.getAction();
2593 
2594         if (action != KeyEvent.ACTION_UP) {
2595             switch (keyCode) {
2596             case KeyEvent.KEYCODE_DPAD_UP:
2597                 if (mIsVertical) {
2598                     handled = handleKeyScroll(event, count, View.FOCUS_UP);
2599                 } else if (KeyEventCompat.hasNoModifiers(event)) {
2600                     handled = handleFocusWithinItem(View.FOCUS_UP);
2601                 }
2602                 break;
2603 
2604             case KeyEvent.KEYCODE_DPAD_DOWN: {
2605                 if (mIsVertical) {
2606                     handled = handleKeyScroll(event, count, View.FOCUS_DOWN);
2607                 } else if (KeyEventCompat.hasNoModifiers(event)) {
2608                     handled = handleFocusWithinItem(View.FOCUS_DOWN);
2609                 }
2610                 break;
2611             }
2612 
2613             case KeyEvent.KEYCODE_DPAD_LEFT:
2614                 if (!mIsVertical) {
2615                     handled = handleKeyScroll(event, count, View.FOCUS_LEFT);
2616                 } else if (KeyEventCompat.hasNoModifiers(event)) {
2617                     handled = handleFocusWithinItem(View.FOCUS_LEFT);
2618                 }
2619                 break;
2620 
2621             case KeyEvent.KEYCODE_DPAD_RIGHT:
2622                 if (!mIsVertical) {
2623                     handled = handleKeyScroll(event, count, View.FOCUS_RIGHT);
2624                 } else if (KeyEventCompat.hasNoModifiers(event)) {
2625                     handled = handleFocusWithinItem(View.FOCUS_RIGHT);
2626                 }
2627                 break;
2628 
2629             case KeyEvent.KEYCODE_DPAD_CENTER:
2630             case KeyEvent.KEYCODE_ENTER:
2631                 if (KeyEventCompat.hasNoModifiers(event)) {
2632                     handled = resurrectSelectionIfNeeded();
2633                     if (!handled
2634                             && event.getRepeatCount() == 0 && getChildCount() > 0) {
2635                         keyPressed();
2636                         handled = true;
2637                     }
2638                 }
2639                 break;
2640 
2641             case KeyEvent.KEYCODE_SPACE:
2642                 if (KeyEventCompat.hasNoModifiers(event)) {
2643                     handled = resurrectSelectionIfNeeded() ||
2644                             pageScroll(mIsVertical ? View.FOCUS_DOWN : View.FOCUS_RIGHT);
2645                 } else if (KeyEventCompat.hasModifiers(event, KeyEvent.META_SHIFT_ON)) {
2646                     handled = resurrectSelectionIfNeeded() ||
2647                             fullScroll(mIsVertical ? View.FOCUS_UP : View.FOCUS_LEFT);
2648                 }
2649 
2650                 handled = true;
2651                 break;
2652 
2653             case KeyEvent.KEYCODE_PAGE_UP:
2654                 if (KeyEventCompat.hasNoModifiers(event)) {
2655                     handled = resurrectSelectionIfNeeded() ||
2656                             pageScroll(mIsVertical ? View.FOCUS_UP : View.FOCUS_LEFT);
2657                 } else if (KeyEventCompat.hasModifiers(event, KeyEvent.META_ALT_ON)) {
2658                     handled = resurrectSelectionIfNeeded() ||
2659                             fullScroll(mIsVertical ? View.FOCUS_UP : View.FOCUS_LEFT);
2660                 }
2661                 break;
2662 
2663             case KeyEvent.KEYCODE_PAGE_DOWN:
2664                 if (KeyEventCompat.hasNoModifiers(event)) {
2665                     handled = resurrectSelectionIfNeeded() ||
2666                             pageScroll(mIsVertical ? View.FOCUS_DOWN : View.FOCUS_RIGHT);
2667                 } else if (KeyEventCompat.hasModifiers(event, KeyEvent.META_ALT_ON)) {
2668                     handled = resurrectSelectionIfNeeded() ||
2669                             fullScroll(mIsVertical ? View.FOCUS_DOWN : View.FOCUS_RIGHT);
2670                 }
2671                 break;
2672 
2673             case KeyEvent.KEYCODE_MOVE_HOME:
2674                 if (KeyEventCompat.hasNoModifiers(event)) {
2675                     handled = resurrectSelectionIfNeeded() ||
2676                             fullScroll(mIsVertical ? View.FOCUS_UP : View.FOCUS_LEFT);
2677                 }
2678                 break;
2679 
2680             case KeyEvent.KEYCODE_MOVE_END:
2681                 if (KeyEventCompat.hasNoModifiers(event)) {
2682                     handled = resurrectSelectionIfNeeded() ||
2683                             fullScroll(mIsVertical ? View.FOCUS_DOWN : View.FOCUS_RIGHT);
2684                 }
2685                 break;
2686             }
2687         }
2688 
2689         if (handled) {
2690             return true;
2691         }
2692 
2693         switch (action) {
2694         case KeyEvent.ACTION_DOWN:
2695             return super.onKeyDown(keyCode, event);
2696 
2697         case KeyEvent.ACTION_UP:
2698             if (!isEnabled()) {
2699                 return true;
2700             }
2701 
2702             if (isClickable() && isPressed() &&
2703                     mSelectedPosition >= 0 && mAdapter != null &&
2704                     mSelectedPosition < mAdapter.getCount()) {
2705 
2706                 final View child = getChildAt(mSelectedPosition - mFirstPosition);
2707                 if (child != null) {
2708                     performItemClick(child, mSelectedPosition, mSelectedRowId);
2709                     child.setPressed(false);
2710                 }
2711 
2712                 setPressed(false);
2713                 return true;
2714             }
2715 
2716             return false;
2717 
2718         case KeyEvent.ACTION_MULTIPLE:
2719             return super.onKeyMultiple(keyCode, count, event);
2720 
2721         default:
2722             return false;
2723         }
2724     }
2725 
initOrResetVelocityTracker()2726     private void initOrResetVelocityTracker() {
2727         if (mVelocityTracker == null) {
2728             mVelocityTracker = VelocityTracker.obtain();
2729         } else {
2730             mVelocityTracker.clear();
2731         }
2732     }
2733 
initVelocityTrackerIfNotExists()2734     private void initVelocityTrackerIfNotExists() {
2735         if (mVelocityTracker == null) {
2736             mVelocityTracker = VelocityTracker.obtain();
2737         }
2738     }
2739 
recycleVelocityTracker()2740     private void recycleVelocityTracker() {
2741         if (mVelocityTracker != null) {
2742             mVelocityTracker.recycle();
2743             mVelocityTracker = null;
2744         }
2745     }
2746 
2747     /**
2748      * Notify our scroll listener (if there is one) of a change in scroll state
2749      */
invokeOnItemScrollListener()2750     private void invokeOnItemScrollListener() {
2751         if (mOnScrollListener != null) {
2752             mOnScrollListener.onScroll(this, mFirstPosition, getChildCount(), mItemCount);
2753         }
2754 
2755         // Dummy values, View's implementation does not use these.
2756         onScrollChanged(0, 0, 0, 0);
2757     }
2758 
reportScrollStateChange(int newState)2759     private void reportScrollStateChange(int newState) {
2760         if (newState == mLastScrollState) {
2761             return;
2762         }
2763 
2764         if (mOnScrollListener != null) {
2765             mLastScrollState = newState;
2766             mOnScrollListener.onScrollStateChanged(this, newState);
2767         }
2768     }
2769 
maybeStartScrolling(int delta)2770     private boolean maybeStartScrolling(int delta) {
2771         final boolean isOverScroll = (mOverScroll != 0);
2772         if (Math.abs(delta) <= mTouchSlop && !isOverScroll) {
2773             return false;
2774         }
2775 
2776         if (isOverScroll) {
2777             mTouchMode = TOUCH_MODE_OVERSCROLL;
2778         } else {
2779             mTouchMode = TOUCH_MODE_DRAGGING;
2780         }
2781 
2782         // Time to start stealing events! Once we've stolen them, don't
2783         // let anyone steal from us.
2784         final ViewParent parent = getParent();
2785         if (parent != null) {
2786             parent.requestDisallowInterceptTouchEvent(true);
2787         }
2788 
2789         cancelCheckForLongPress();
2790 
2791         setPressed(false);
2792         View motionView = getChildAt(mMotionPosition - mFirstPosition);
2793         if (motionView != null) {
2794             motionView.setPressed(false);
2795         }
2796 
2797         reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
2798 
2799         return true;
2800     }
2801 
maybeScroll(int delta)2802     private void maybeScroll(int delta) {
2803         if (mTouchMode == TOUCH_MODE_DRAGGING) {
2804             handleDragChange(delta);
2805         } else if (mTouchMode == TOUCH_MODE_OVERSCROLL) {
2806             handleOverScrollChange(delta);
2807         }
2808     }
2809 
handleDragChange(int delta)2810     private void handleDragChange(int delta) {
2811         // Time to start stealing events! Once we've stolen them, don't
2812         // let anyone steal from us.
2813         final ViewParent parent = getParent();
2814         if (parent != null) {
2815             parent.requestDisallowInterceptTouchEvent(true);
2816         }
2817 
2818         final int motionIndex;
2819         if (mMotionPosition >= 0) {
2820             motionIndex = mMotionPosition - mFirstPosition;
2821         } else {
2822             // If we don't have a motion position that we can reliably track,
2823             // pick something in the middle to make a best guess at things below.
2824             motionIndex = getChildCount() / 2;
2825         }
2826 
2827         int motionViewPrevStart = 0;
2828         View motionView = this.getChildAt(motionIndex);
2829         if (motionView != null) {
2830             motionViewPrevStart = getChildStartEdge(motionView);
2831         }
2832 
2833         boolean atEdge = scrollListItemsBy(delta);
2834 
2835         motionView = this.getChildAt(motionIndex);
2836         if (motionView != null) {
2837             final int motionViewRealStart = getChildStartEdge(motionView);
2838 
2839             if (atEdge) {
2840                 final int overscroll = -delta - (motionViewRealStart - motionViewPrevStart);
2841                 updateOverScrollState(delta, overscroll);
2842             }
2843         }
2844     }
2845 
updateOverScrollState(int delta, int overscroll)2846     private void updateOverScrollState(int delta, int overscroll) {
2847         overScrollByInternal((mIsVertical ? 0 : overscroll),
2848                              (mIsVertical ? overscroll : 0),
2849                              (mIsVertical ? 0 : mOverScroll),
2850                              (mIsVertical ? mOverScroll : 0),
2851                              0, 0,
2852                              (mIsVertical ? 0 : mOverscrollDistance),
2853                              (mIsVertical ? mOverscrollDistance : 0),
2854                              true);
2855 
2856         if (Math.abs(mOverscrollDistance) == Math.abs(mOverScroll)) {
2857             // Break fling velocity if we impacted an edge
2858             if (mVelocityTracker != null) {
2859                 mVelocityTracker.clear();
2860             }
2861         }
2862 
2863         final int overscrollMode = ViewCompat.getOverScrollMode(this);
2864         if (overscrollMode == ViewCompat.OVER_SCROLL_ALWAYS ||
2865                 (overscrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && !contentFits())) {
2866             mTouchMode = TOUCH_MODE_OVERSCROLL;
2867 
2868             float pull = (float) overscroll / getSize();
2869             if (delta > 0) {
2870                 mStartEdge.onPull(pull);
2871 
2872                 if (!mEndEdge.isFinished()) {
2873                     mEndEdge.onRelease();
2874                 }
2875             } else if (delta < 0) {
2876                 mEndEdge.onPull(pull);
2877 
2878                 if (!mStartEdge.isFinished()) {
2879                     mStartEdge.onRelease();
2880                 }
2881             }
2882 
2883             if (delta != 0) {
2884                 ViewCompat.postInvalidateOnAnimation(this);
2885             }
2886         }
2887     }
2888 
handleOverScrollChange(int delta)2889     private void handleOverScrollChange(int delta) {
2890         final int oldOverScroll = mOverScroll;
2891         final int newOverScroll = oldOverScroll - delta;
2892 
2893         int overScrollDistance = -delta;
2894         if ((newOverScroll < 0 && oldOverScroll >= 0) ||
2895                 (newOverScroll > 0 && oldOverScroll <= 0)) {
2896             overScrollDistance = -oldOverScroll;
2897             delta += overScrollDistance;
2898         } else {
2899             delta = 0;
2900         }
2901 
2902         if (overScrollDistance != 0) {
2903             updateOverScrollState(delta, overScrollDistance);
2904         }
2905 
2906         if (delta != 0) {
2907             if (mOverScroll != 0) {
2908                 mOverScroll = 0;
2909                 ViewCompat.postInvalidateOnAnimation(this);
2910             }
2911 
2912             scrollListItemsBy(delta);
2913             mTouchMode = TOUCH_MODE_DRAGGING;
2914 
2915             // We did not scroll the full amount. Treat this essentially like the
2916             // start of a new touch scroll
2917             mMotionPosition = findClosestMotionRowOrColumn((int) mLastTouchPos);
2918             mTouchRemainderPos = 0;
2919         }
2920     }
2921 
2922     /**
2923      * What is the distance between the source and destination rectangles given the direction of
2924      * focus navigation between them? The direction basically helps figure out more quickly what is
2925      * self evident by the relationship between the rects...
2926      *
2927      * @param source the source rectangle
2928      * @param dest the destination rectangle
2929      * @param direction the direction
2930      * @return the distance between the rectangles
2931      */
getDistance(Rect source, Rect dest, int direction)2932     private static int getDistance(Rect source, Rect dest, int direction) {
2933         int sX, sY; // source x, y
2934         int dX, dY; // dest x, y
2935 
2936         switch (direction) {
2937         case View.FOCUS_RIGHT:
2938             sX = source.right;
2939             sY = source.top + source.height() / 2;
2940             dX = dest.left;
2941             dY = dest.top + dest.height() / 2;
2942             break;
2943 
2944         case View.FOCUS_DOWN:
2945             sX = source.left + source.width() / 2;
2946             sY = source.bottom;
2947             dX = dest.left + dest.width() / 2;
2948             dY = dest.top;
2949             break;
2950 
2951         case View.FOCUS_LEFT:
2952             sX = source.left;
2953             sY = source.top + source.height() / 2;
2954             dX = dest.right;
2955             dY = dest.top + dest.height() / 2;
2956             break;
2957 
2958         case View.FOCUS_UP:
2959             sX = source.left + source.width() / 2;
2960             sY = source.top;
2961             dX = dest.left + dest.width() / 2;
2962             dY = dest.bottom;
2963             break;
2964 
2965         case View.FOCUS_FORWARD:
2966         case View.FOCUS_BACKWARD:
2967             sX = source.right + source.width() / 2;
2968             sY = source.top + source.height() / 2;
2969             dX = dest.left + dest.width() / 2;
2970             dY = dest.top + dest.height() / 2;
2971             break;
2972 
2973         default:
2974             throw new IllegalArgumentException("direction must be one of "
2975                     + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT, "
2976                     + "FOCUS_FORWARD, FOCUS_BACKWARD}.");
2977         }
2978 
2979         int deltaX = dX - sX;
2980         int deltaY = dY - sY;
2981 
2982         return deltaY * deltaY + deltaX * deltaX;
2983     }
2984 
findMotionRowOrColumn(int motionPos)2985     private int findMotionRowOrColumn(int motionPos) {
2986         int childCount = getChildCount();
2987         if (childCount == 0) {
2988             return INVALID_POSITION;
2989         }
2990 
2991         for (int i = 0; i < childCount; i++) {
2992             final View v = getChildAt(i);
2993             if (motionPos <= getChildEndEdge(v)) {
2994                 return mFirstPosition + i;
2995             }
2996         }
2997 
2998         return INVALID_POSITION;
2999     }
3000 
findClosestMotionRowOrColumn(int motionPos)3001     private int findClosestMotionRowOrColumn(int motionPos) {
3002         final int childCount = getChildCount();
3003         if (childCount == 0) {
3004             return INVALID_POSITION;
3005         }
3006 
3007         final int motionRow = findMotionRowOrColumn(motionPos);
3008         if (motionRow != INVALID_POSITION) {
3009             return motionRow;
3010         } else {
3011             return mFirstPosition + childCount - 1;
3012         }
3013     }
3014 
3015     @TargetApi(9)
getScaledOverscrollDistance(ViewConfiguration vc)3016     private int getScaledOverscrollDistance(ViewConfiguration vc) {
3017         if (Build.VERSION.SDK_INT < 9) {
3018             return 0;
3019         }
3020 
3021         return vc.getScaledOverscrollDistance();
3022     }
3023 
getStartEdge()3024     private int getStartEdge() {
3025         return (mIsVertical ? getPaddingTop() : getPaddingLeft());
3026     }
3027 
getEndEdge()3028     private int getEndEdge() {
3029         if (mIsVertical) {
3030             return (getHeight() - getPaddingBottom());
3031         } else {
3032             return (getWidth() - getPaddingRight());
3033         }
3034     }
3035 
getSize()3036     private int getSize() {
3037         return (mIsVertical ? getHeight() : getWidth());
3038     }
3039 
getAvailableSize()3040     private int getAvailableSize() {
3041         if (mIsVertical) {
3042             return getHeight() - getPaddingBottom() - getPaddingTop();
3043         } else {
3044             return getWidth() - getPaddingRight() - getPaddingLeft();
3045         }
3046     }
3047 
getChildStartEdge(View child)3048     private int getChildStartEdge(View child) {
3049         return (mIsVertical ? child.getTop() : child.getLeft());
3050     }
3051 
getChildEndEdge(View child)3052     private int getChildEndEdge(View child) {
3053         return (mIsVertical ? child.getBottom() : child.getRight());
3054     }
3055 
getChildSize(View child)3056     private int getChildSize(View child) {
3057         return (mIsVertical ? child.getHeight() : child.getWidth());
3058     }
3059 
getChildMeasuredSize(View child)3060     private int getChildMeasuredSize(View child) {
3061         return (mIsVertical ? child.getMeasuredHeight() : child.getMeasuredWidth());
3062     }
3063 
getFadingEdgeLength()3064     private int getFadingEdgeLength() {
3065         return (mIsVertical ? getVerticalFadingEdgeLength() : getHorizontalFadingEdgeLength());
3066     }
3067 
getMinSelectionPixel(int start, int fadingEdgeLength, int selectedPosition)3068     private int getMinSelectionPixel(int start, int fadingEdgeLength, int selectedPosition) {
3069         // First pixel we can draw the selection into.
3070         int selectionPixelStart = start;
3071         if (selectedPosition > 0) {
3072             selectionPixelStart += fadingEdgeLength;
3073         }
3074 
3075         return selectionPixelStart;
3076     }
3077 
getMaxSelectionPixel(int end, int fadingEdgeLength, int selectedPosition)3078     private int getMaxSelectionPixel(int end, int fadingEdgeLength,
3079                                      int selectedPosition) {
3080         int selectionPixelEnd = end;
3081         if (selectedPosition != mItemCount - 1) {
3082             selectionPixelEnd -= fadingEdgeLength;
3083         }
3084 
3085         return selectionPixelEnd;
3086     }
3087 
contentFits()3088     private boolean contentFits() {
3089         final int childCount = getChildCount();
3090         if (childCount == 0) {
3091             return true;
3092         }
3093 
3094         if (childCount != mItemCount) {
3095             return false;
3096         }
3097 
3098         View first = getChildAt(0);
3099         View last = getChildAt(childCount - 1);
3100 
3101         return (getChildStartEdge(first) >= getStartEdge() &&
3102                 getChildEndEdge(last) <= getEndEdge());
3103     }
3104 
triggerCheckForTap()3105     private void triggerCheckForTap() {
3106         if (mPendingCheckForTap == null) {
3107             mPendingCheckForTap = new CheckForTap();
3108         }
3109 
3110         postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
3111     }
3112 
cancelCheckForTap()3113     private void cancelCheckForTap() {
3114         if (mPendingCheckForTap == null) {
3115             return;
3116         }
3117 
3118         removeCallbacks(mPendingCheckForTap);
3119     }
3120 
triggerCheckForLongPress()3121     private void triggerCheckForLongPress() {
3122         if (mPendingCheckForLongPress == null) {
3123             mPendingCheckForLongPress = new CheckForLongPress();
3124         }
3125 
3126         mPendingCheckForLongPress.rememberWindowAttachCount();
3127 
3128         postDelayed(mPendingCheckForLongPress,
3129                 ViewConfiguration.getLongPressTimeout());
3130     }
3131 
cancelCheckForLongPress()3132     private void cancelCheckForLongPress() {
3133         if (mPendingCheckForLongPress == null) {
3134             return;
3135         }
3136 
3137         removeCallbacks(mPendingCheckForLongPress);
3138     }
3139 
scrollListItemsBy(int incrementalDelta)3140     private boolean scrollListItemsBy(int incrementalDelta) {
3141         final int childCount = getChildCount();
3142         if (childCount == 0) {
3143             return true;
3144         }
3145 
3146         final int firstStart = getChildStartEdge(getChildAt(0));
3147         final int lastEnd = getChildEndEdge(getChildAt(childCount - 1));
3148 
3149         final int paddingTop = getPaddingTop();
3150         final int paddingLeft = getPaddingLeft();
3151 
3152         final int paddingStart = (mIsVertical ? paddingTop : paddingLeft);
3153 
3154         final int spaceBefore = paddingStart - firstStart;
3155         final int end = getEndEdge();
3156         final int spaceAfter = lastEnd - end;
3157 
3158         final int size = getAvailableSize();
3159 
3160         if (incrementalDelta < 0) {
3161             incrementalDelta = Math.max(-(size - 1), incrementalDelta);
3162         } else {
3163             incrementalDelta = Math.min(size - 1, incrementalDelta);
3164         }
3165 
3166         final int firstPosition = mFirstPosition;
3167 
3168         final boolean cannotScrollDown = (firstPosition == 0 &&
3169                 firstStart >= paddingStart && incrementalDelta >= 0);
3170         final boolean cannotScrollUp = (firstPosition + childCount == mItemCount &&
3171                 lastEnd <= end && incrementalDelta <= 0);
3172 
3173         if (cannotScrollDown || cannotScrollUp) {
3174             return incrementalDelta != 0;
3175         }
3176 
3177         final boolean inTouchMode = isInTouchMode();
3178         if (inTouchMode) {
3179             hideSelector();
3180         }
3181 
3182         int start = 0;
3183         int count = 0;
3184 
3185         final boolean down = (incrementalDelta < 0);
3186         if (down) {
3187             int childrenStart = -incrementalDelta + paddingStart;
3188 
3189             for (int i = 0; i < childCount; i++) {
3190                 final View child = getChildAt(i);
3191                 final int childEnd = getChildEndEdge(child);
3192 
3193                 if (childEnd >= childrenStart) {
3194                     break;
3195                 }
3196 
3197                 count++;
3198                 mRecycler.addScrapView(child, firstPosition + i);
3199             }
3200         } else {
3201             int childrenEnd = end - incrementalDelta;
3202 
3203             for (int i = childCount - 1; i >= 0; i--) {
3204                 final View child = getChildAt(i);
3205                 final int childStart = getChildStartEdge(child);
3206 
3207                 if (childStart <= childrenEnd) {
3208                     break;
3209                 }
3210 
3211                 start = i;
3212                 count++;
3213                 mRecycler.addScrapView(child, firstPosition + i);
3214             }
3215         }
3216 
3217         mBlockLayoutRequests = true;
3218 
3219         if (count > 0) {
3220             detachViewsFromParent(start, count);
3221         }
3222 
3223         // invalidate before moving the children to avoid unnecessary invalidate
3224         // calls to bubble up from the children all the way to the top
3225         if (!awakenScrollbarsInternal()) {
3226            invalidate();
3227         }
3228 
3229         offsetChildren(incrementalDelta);
3230 
3231         if (down) {
3232             mFirstPosition += count;
3233         }
3234 
3235         final int absIncrementalDelta = Math.abs(incrementalDelta);
3236         if (spaceBefore < absIncrementalDelta || spaceAfter < absIncrementalDelta) {
3237             fillGap(down);
3238         }
3239 
3240         if (!inTouchMode && mSelectedPosition != INVALID_POSITION) {
3241             final int childIndex = mSelectedPosition - mFirstPosition;
3242             if (childIndex >= 0 && childIndex < getChildCount()) {
3243                 positionSelector(mSelectedPosition, getChildAt(childIndex));
3244             }
3245         } else if (mSelectorPosition != INVALID_POSITION) {
3246             final int childIndex = mSelectorPosition - mFirstPosition;
3247             if (childIndex >= 0 && childIndex < getChildCount()) {
3248                 positionSelector(INVALID_POSITION, getChildAt(childIndex));
3249             }
3250         } else {
3251             mSelectorRect.setEmpty();
3252         }
3253 
3254         mBlockLayoutRequests = false;
3255 
3256         invokeOnItemScrollListener();
3257 
3258         return false;
3259     }
3260 
3261     @TargetApi(14)
getCurrVelocity()3262     private final float getCurrVelocity() {
3263         if (Build.VERSION.SDK_INT >= 14) {
3264             return mScroller.getCurrVelocity();
3265         }
3266 
3267         return 0;
3268     }
3269 
3270     @TargetApi(5)
awakenScrollbarsInternal()3271     private boolean awakenScrollbarsInternal() {
3272         return (Build.VERSION.SDK_INT >= 5) && super.awakenScrollBars();
3273     }
3274 
3275     @Override
computeScroll()3276     public void computeScroll() {
3277         if (!mScroller.computeScrollOffset()) {
3278             return;
3279         }
3280 
3281         final int pos;
3282         if (mIsVertical) {
3283             pos = mScroller.getCurrY();
3284         } else {
3285             pos = mScroller.getCurrX();
3286         }
3287 
3288         final int diff = (int) (pos - mLastTouchPos);
3289         mLastTouchPos = pos;
3290 
3291         final boolean stopped = scrollListItemsBy(diff);
3292 
3293         if (!stopped && !mScroller.isFinished()) {
3294             ViewCompat.postInvalidateOnAnimation(this);
3295         } else {
3296             if (stopped) {
3297                 final int overScrollMode = ViewCompat.getOverScrollMode(this);
3298                 if (overScrollMode != ViewCompat.OVER_SCROLL_NEVER) {
3299                     final EdgeEffectCompat edge =
3300                             (diff > 0 ? mStartEdge : mEndEdge);
3301 
3302                     boolean needsInvalidate =
3303                             edge.onAbsorb(Math.abs((int) getCurrVelocity()));
3304 
3305                     if (needsInvalidate) {
3306                         ViewCompat.postInvalidateOnAnimation(this);
3307                     }
3308                 }
3309 
3310                 finishSmoothScrolling();
3311             }
3312 
3313             mTouchMode = TOUCH_MODE_REST;
3314             reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
3315         }
3316     }
3317 
finishEdgeGlows()3318     private void finishEdgeGlows() {
3319         if (mStartEdge != null) {
3320             mStartEdge.finish();
3321         }
3322 
3323         if (mEndEdge != null) {
3324             mEndEdge.finish();
3325         }
3326     }
3327 
drawStartEdge(Canvas canvas)3328     private boolean drawStartEdge(Canvas canvas) {
3329         if (mStartEdge.isFinished()) {
3330             return false;
3331         }
3332 
3333         if (mIsVertical) {
3334             return mStartEdge.draw(canvas);
3335         }
3336 
3337         final int restoreCount = canvas.save();
3338         final int height = getHeight();
3339 
3340         canvas.translate(0, height);
3341         canvas.rotate(270);
3342 
3343         final boolean needsInvalidate = mStartEdge.draw(canvas);
3344         canvas.restoreToCount(restoreCount);
3345         return needsInvalidate;
3346     }
3347 
drawEndEdge(Canvas canvas)3348     private boolean drawEndEdge(Canvas canvas) {
3349         if (mEndEdge.isFinished()) {
3350             return false;
3351         }
3352 
3353         final int restoreCount = canvas.save();
3354         final int width = getWidth();
3355         final int height = getHeight();
3356 
3357         if (mIsVertical) {
3358             canvas.translate(-width, height);
3359             canvas.rotate(180, width, 0);
3360         } else {
3361             canvas.translate(width, 0);
3362             canvas.rotate(90);
3363         }
3364 
3365         final boolean needsInvalidate = mEndEdge.draw(canvas);
3366         canvas.restoreToCount(restoreCount);
3367         return needsInvalidate;
3368     }
3369 
finishSmoothScrolling()3370     private void finishSmoothScrolling() {
3371         mTouchMode = TOUCH_MODE_REST;
3372         reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
3373 
3374         mScroller.abortAnimation();
3375         if (mPositionScroller != null) {
3376             mPositionScroller.stop();
3377         }
3378     }
3379 
drawSelector(Canvas canvas)3380     private void drawSelector(Canvas canvas) {
3381         if (!mSelectorRect.isEmpty()) {
3382             final Drawable selector = mSelector;
3383             selector.setBounds(mSelectorRect);
3384             selector.draw(canvas);
3385         }
3386     }
3387 
useDefaultSelector()3388     private void useDefaultSelector() {
3389         setSelector(getResources().getDrawable(
3390                 android.R.drawable.list_selector_background));
3391     }
3392 
shouldShowSelector()3393     private boolean shouldShowSelector() {
3394         return (hasFocus() && !isInTouchMode()) || touchModeDrawsInPressedState();
3395     }
3396 
positionSelector(int position, View selected)3397     private void positionSelector(int position, View selected) {
3398         if (position != INVALID_POSITION) {
3399             mSelectorPosition = position;
3400         }
3401 
3402         mSelectorRect.set(selected.getLeft(), selected.getTop(), selected.getRight(),
3403                 selected.getBottom());
3404 
3405         final boolean isChildViewEnabled = mIsChildViewEnabled;
3406         if (selected.isEnabled() != isChildViewEnabled) {
3407             mIsChildViewEnabled = !isChildViewEnabled;
3408 
3409             if (getSelectedItemPosition() != INVALID_POSITION) {
3410                 refreshDrawableState();
3411             }
3412         }
3413     }
3414 
hideSelector()3415     private void hideSelector() {
3416         if (mSelectedPosition != INVALID_POSITION) {
3417             if (mLayoutMode != LAYOUT_SPECIFIC) {
3418                 mResurrectToPosition = mSelectedPosition;
3419             }
3420 
3421             if (mNextSelectedPosition >= 0 && mNextSelectedPosition != mSelectedPosition) {
3422                 mResurrectToPosition = mNextSelectedPosition;
3423             }
3424 
3425             setSelectedPositionInt(INVALID_POSITION);
3426             setNextSelectedPositionInt(INVALID_POSITION);
3427 
3428             mSelectedStart = 0;
3429         }
3430     }
3431 
setSelectedPositionInt(int position)3432     private void setSelectedPositionInt(int position) {
3433         mSelectedPosition = position;
3434         mSelectedRowId = getItemIdAtPosition(position);
3435     }
3436 
setSelectionInt(int position)3437     private void setSelectionInt(int position) {
3438         setNextSelectedPositionInt(position);
3439         boolean awakeScrollbars = false;
3440 
3441         final int selectedPosition = mSelectedPosition;
3442         if (selectedPosition >= 0) {
3443             if (position == selectedPosition - 1) {
3444                 awakeScrollbars = true;
3445             } else if (position == selectedPosition + 1) {
3446                 awakeScrollbars = true;
3447             }
3448         }
3449 
3450         layoutChildren();
3451 
3452         if (awakeScrollbars) {
3453             awakenScrollbarsInternal();
3454         }
3455     }
3456 
setNextSelectedPositionInt(int position)3457     private void setNextSelectedPositionInt(int position) {
3458         mNextSelectedPosition = position;
3459         mNextSelectedRowId = getItemIdAtPosition(position);
3460 
3461         // If we are trying to sync to the selection, update that too
3462         if (mNeedSync && mSyncMode == SYNC_SELECTED_POSITION && position >= 0) {
3463             mSyncPosition = position;
3464             mSyncRowId = mNextSelectedRowId;
3465         }
3466     }
3467 
touchModeDrawsInPressedState()3468     private boolean touchModeDrawsInPressedState() {
3469         switch (mTouchMode) {
3470         case TOUCH_MODE_TAP:
3471         case TOUCH_MODE_DONE_WAITING:
3472             return true;
3473         default:
3474             return false;
3475         }
3476     }
3477 
3478     /**
3479      * Sets the selector state to "pressed" and posts a CheckForKeyLongPress to see if
3480      * this is a long press.
3481      */
keyPressed()3482     private void keyPressed() {
3483         if (!isEnabled() || !isClickable()) {
3484             return;
3485         }
3486 
3487         final Drawable selector = mSelector;
3488         final Rect selectorRect = mSelectorRect;
3489 
3490         if (selector != null && (isFocused() || touchModeDrawsInPressedState())
3491                 && !selectorRect.isEmpty()) {
3492 
3493             final View child = getChildAt(mSelectedPosition - mFirstPosition);
3494 
3495             if (child != null) {
3496                 if (child.hasFocusable()) {
3497                     return;
3498                 }
3499 
3500                 child.setPressed(true);
3501             }
3502 
3503             setPressed(true);
3504 
3505             final boolean longClickable = isLongClickable();
3506             final Drawable d = selector.getCurrent();
3507             if (d != null && d instanceof TransitionDrawable) {
3508                 if (longClickable) {
3509                     ((TransitionDrawable) d).startTransition(
3510                             ViewConfiguration.getLongPressTimeout());
3511                 } else {
3512                     ((TransitionDrawable) d).resetTransition();
3513                 }
3514             }
3515 
3516             if (longClickable && !mDataChanged) {
3517                 if (mPendingCheckForKeyLongPress == null) {
3518                     mPendingCheckForKeyLongPress = new CheckForKeyLongPress();
3519                 }
3520 
3521                 mPendingCheckForKeyLongPress.rememberWindowAttachCount();
3522                 postDelayed(mPendingCheckForKeyLongPress, ViewConfiguration.getLongPressTimeout());
3523             }
3524         }
3525     }
3526 
updateSelectorState()3527     private void updateSelectorState() {
3528         if (mSelector != null) {
3529             if (shouldShowSelector()) {
3530                 mSelector.setState(getDrawableState());
3531             } else {
3532                 mSelector.setState(STATE_NOTHING);
3533             }
3534         }
3535     }
3536 
checkSelectionChanged()3537     private void checkSelectionChanged() {
3538         if ((mSelectedPosition != mOldSelectedPosition) || (mSelectedRowId != mOldSelectedRowId)) {
3539             selectionChanged();
3540             mOldSelectedPosition = mSelectedPosition;
3541             mOldSelectedRowId = mSelectedRowId;
3542         }
3543     }
3544 
selectionChanged()3545     private void selectionChanged() {
3546         OnItemSelectedListener listener = getOnItemSelectedListener();
3547         if (listener == null) {
3548             return;
3549         }
3550 
3551         if (mInLayout || mBlockLayoutRequests) {
3552             // If we are in a layout traversal, defer notification
3553             // by posting. This ensures that the view tree is
3554             // in a consistent state and is able to accommodate
3555             // new layout or invalidate requests.
3556             if (mSelectionNotifier == null) {
3557                 mSelectionNotifier = new SelectionNotifier();
3558             }
3559 
3560             post(mSelectionNotifier);
3561         } else {
3562             fireOnSelected();
3563             performAccessibilityActionsOnSelected();
3564         }
3565     }
3566 
fireOnSelected()3567     private void fireOnSelected() {
3568         OnItemSelectedListener listener = getOnItemSelectedListener();
3569         if (listener == null) {
3570             return;
3571         }
3572 
3573         final int selection = getSelectedItemPosition();
3574         if (selection >= 0) {
3575             View v = getSelectedView();
3576             listener.onItemSelected(this, v, selection,
3577                     mAdapter.getItemId(selection));
3578         } else {
3579             listener.onNothingSelected(this);
3580         }
3581     }
3582 
performAccessibilityActionsOnSelected()3583     private void performAccessibilityActionsOnSelected() {
3584         final int position = getSelectedItemPosition();
3585         if (position >= 0) {
3586             // We fire selection events here not in View
3587             sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
3588         }
3589     }
3590 
lookForSelectablePosition(int position)3591     private int lookForSelectablePosition(int position) {
3592         return lookForSelectablePosition(position, true);
3593     }
3594 
lookForSelectablePosition(int position, boolean lookDown)3595     private int lookForSelectablePosition(int position, boolean lookDown) {
3596         final ListAdapter adapter = mAdapter;
3597         if (adapter == null || isInTouchMode()) {
3598             return INVALID_POSITION;
3599         }
3600 
3601         final int itemCount = mItemCount;
3602         if (!mAreAllItemsSelectable) {
3603             if (lookDown) {
3604                 position = Math.max(0, position);
3605                 while (position < itemCount && !adapter.isEnabled(position)) {
3606                     position++;
3607                 }
3608             } else {
3609                 position = Math.min(position, itemCount - 1);
3610                 while (position >= 0 && !adapter.isEnabled(position)) {
3611                     position--;
3612                 }
3613             }
3614 
3615             if (position < 0 || position >= itemCount) {
3616                 return INVALID_POSITION;
3617             }
3618 
3619             return position;
3620         } else {
3621             if (position < 0 || position >= itemCount) {
3622                 return INVALID_POSITION;
3623             }
3624 
3625             return position;
3626         }
3627     }
3628 
3629     /**
3630      * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or
3631      *        {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the
3632      *        current view orientation.
3633      *
3634      * @return The position of the next selectable position of the views that
3635      *         are currently visible, taking into account the fact that there might
3636      *         be no selection.  Returns {@link #INVALID_POSITION} if there is no
3637      *         selectable view on screen in the given direction.
3638      */
lookForSelectablePositionOnScreen(int direction)3639     private int lookForSelectablePositionOnScreen(int direction) {
3640         forceValidFocusDirection(direction);
3641 
3642         final int firstPosition = mFirstPosition;
3643         final ListAdapter adapter = getAdapter();
3644 
3645         if (direction == View.FOCUS_DOWN || direction == View.FOCUS_RIGHT) {
3646             int startPos = (mSelectedPosition != INVALID_POSITION ?
3647                     mSelectedPosition + 1 : firstPosition);
3648 
3649             if (startPos >= adapter.getCount()) {
3650                 return INVALID_POSITION;
3651             }
3652 
3653             if (startPos < firstPosition) {
3654                 startPos = firstPosition;
3655             }
3656 
3657             final int lastVisiblePos = getLastVisiblePosition();
3658 
3659             for (int pos = startPos; pos <= lastVisiblePos; pos++) {
3660                 if (adapter.isEnabled(pos)
3661                         && getChildAt(pos - firstPosition).getVisibility() == View.VISIBLE) {
3662                     return pos;
3663                 }
3664             }
3665         } else {
3666             final int last = firstPosition + getChildCount() - 1;
3667 
3668             int startPos = (mSelectedPosition != INVALID_POSITION) ?
3669                     mSelectedPosition - 1 : firstPosition + getChildCount() - 1;
3670 
3671             if (startPos < 0 || startPos >= adapter.getCount()) {
3672                 return INVALID_POSITION;
3673             }
3674 
3675             if (startPos > last) {
3676                 startPos = last;
3677             }
3678 
3679             for (int pos = startPos; pos >= firstPosition; pos--) {
3680                 if (adapter.isEnabled(pos)
3681                         && getChildAt(pos - firstPosition).getVisibility() == View.VISIBLE) {
3682                     return pos;
3683                 }
3684             }
3685         }
3686 
3687         return INVALID_POSITION;
3688     }
3689 
3690     @Override
drawableStateChanged()3691     protected void drawableStateChanged() {
3692         super.drawableStateChanged();
3693         updateSelectorState();
3694     }
3695 
3696     @Override
onCreateDrawableState(int extraSpace)3697     protected int[] onCreateDrawableState(int extraSpace) {
3698         // If the child view is enabled then do the default behavior.
3699         if (mIsChildViewEnabled) {
3700             // Common case
3701             return super.onCreateDrawableState(extraSpace);
3702         }
3703 
3704         // The selector uses this View's drawable state. The selected child view
3705         // is disabled, so we need to remove the enabled state from the drawable
3706         // states.
3707         final int enabledState = ENABLED_STATE_SET[0];
3708 
3709         // If we don't have any extra space, it will return one of the static state arrays,
3710         // and clearing the enabled state on those arrays is a bad thing!  If we specify
3711         // we need extra space, it will create+copy into a new array that safely mutable.
3712         int[] state = super.onCreateDrawableState(extraSpace + 1);
3713         int enabledPos = -1;
3714         for (int i = state.length - 1; i >= 0; i--) {
3715             if (state[i] == enabledState) {
3716                 enabledPos = i;
3717                 break;
3718             }
3719         }
3720 
3721         // Remove the enabled state
3722         if (enabledPos >= 0) {
3723             System.arraycopy(state, enabledPos + 1, state, enabledPos,
3724                     state.length - enabledPos - 1);
3725         }
3726 
3727         return state;
3728     }
3729 
3730     @Override
canAnimate()3731     protected boolean canAnimate() {
3732         return (super.canAnimate() && mItemCount > 0);
3733     }
3734 
3735     @Override
dispatchDraw(Canvas canvas)3736     protected void dispatchDraw(Canvas canvas) {
3737         final boolean drawSelectorOnTop = mDrawSelectorOnTop;
3738         if (!drawSelectorOnTop) {
3739             drawSelector(canvas);
3740         }
3741 
3742         super.dispatchDraw(canvas);
3743 
3744         if (drawSelectorOnTop) {
3745             drawSelector(canvas);
3746         }
3747     }
3748 
3749     @Override
draw(Canvas canvas)3750     public void draw(Canvas canvas) {
3751         super.draw(canvas);
3752 
3753         boolean needsInvalidate = false;
3754 
3755         if (mStartEdge != null) {
3756             needsInvalidate |= drawStartEdge(canvas);
3757         }
3758 
3759         if (mEndEdge != null) {
3760             needsInvalidate |= drawEndEdge(canvas);
3761         }
3762 
3763         if (needsInvalidate) {
3764             ViewCompat.postInvalidateOnAnimation(this);
3765         }
3766     }
3767 
3768     @Override
requestLayout()3769     public void requestLayout() {
3770         if (!mInLayout && !mBlockLayoutRequests) {
3771             super.requestLayout();
3772         }
3773     }
3774 
3775     @Override
getSelectedView()3776     public View getSelectedView() {
3777         if (mItemCount > 0 && mSelectedPosition >= 0) {
3778             return getChildAt(mSelectedPosition - mFirstPosition);
3779         } else {
3780             return null;
3781         }
3782     }
3783 
3784     @Override
setSelection(int position)3785     public void setSelection(int position) {
3786         setSelectionFromOffset(position, 0);
3787     }
3788 
setSelectionFromOffset(int position, int offset)3789     public void setSelectionFromOffset(int position, int offset) {
3790         if (mAdapter == null) {
3791             return;
3792         }
3793 
3794         if (!isInTouchMode()) {
3795             position = lookForSelectablePosition(position);
3796             if (position >= 0) {
3797                 setNextSelectedPositionInt(position);
3798             }
3799         } else {
3800             mResurrectToPosition = position;
3801         }
3802 
3803         if (position >= 0) {
3804             mLayoutMode = LAYOUT_SPECIFIC;
3805 
3806             if (mIsVertical) {
3807                 mSpecificStart = getPaddingTop() + offset;
3808             } else {
3809                 mSpecificStart = getPaddingLeft() + offset;
3810             }
3811 
3812             if (mNeedSync) {
3813                 mSyncPosition = position;
3814                 mSyncRowId = mAdapter.getItemId(position);
3815             }
3816 
3817             requestLayout();
3818         }
3819     }
3820 
scrollBy(int offset)3821     public void scrollBy(int offset) {
3822         scrollListItemsBy(-offset);
3823     }
3824 
3825     /**
3826      * Smoothly scroll to the specified adapter position. The view will
3827      * scroll such that the indicated position is displayed.
3828      * @param position Scroll to this adapter position.
3829      */
smoothScrollToPosition(int position)3830     public void smoothScrollToPosition(int position) {
3831         if (mPositionScroller == null) {
3832             mPositionScroller = new PositionScroller();
3833         }
3834         mPositionScroller.start(position);
3835     }
3836 
3837     /**
3838      * Smoothly scroll to the specified adapter position. The view will scroll
3839      * such that the indicated position is displayed <code>offset</code> pixels from
3840      * the top/left edge of the view, according to the orientation. If this is
3841      * impossible, (e.g. the offset would scroll the first or last item beyond the boundaries
3842      * of the list) it will get as close as possible. The scroll will take
3843      * <code>duration</code> milliseconds to complete.
3844      *
3845      * @param position Position to scroll to
3846      * @param offset Desired distance in pixels of <code>position</code> from the top/left
3847      *               of the view when scrolling is finished
3848      * @param duration Number of milliseconds to use for the scroll
3849      */
smoothScrollToPositionFromOffset(int position, int offset, int duration)3850     public void smoothScrollToPositionFromOffset(int position, int offset, int duration) {
3851         if (mPositionScroller == null) {
3852             mPositionScroller = new PositionScroller();
3853         }
3854         mPositionScroller.startWithOffset(position, offset, duration);
3855     }
3856 
3857     /**
3858      * Smoothly scroll to the specified adapter position. The view will scroll
3859      * such that the indicated position is displayed <code>offset</code> pixels from
3860      * the top edge of the view. If this is impossible, (e.g. the offset would scroll
3861      * the first or last item beyond the boundaries of the list) it will get as close
3862      * as possible.
3863      *
3864      * @param position Position to scroll to
3865      * @param offset Desired distance in pixels of <code>position</code> from the top
3866      *               of the view when scrolling is finished
3867      */
smoothScrollToPositionFromOffset(int position, int offset)3868     public void smoothScrollToPositionFromOffset(int position, int offset) {
3869         if (mPositionScroller == null) {
3870             mPositionScroller = new PositionScroller();
3871         }
3872         mPositionScroller.startWithOffset(position, offset);
3873     }
3874 
3875     /**
3876      * Smoothly scroll to the specified adapter position. The view will
3877      * scroll such that the indicated position is displayed, but it will
3878      * stop early if scrolling further would scroll boundPosition out of
3879      * view.
3880      *
3881      * @param position Scroll to this adapter position.
3882      * @param boundPosition Do not scroll if it would move this adapter
3883      *          position out of view.
3884      */
smoothScrollToPosition(int position, int boundPosition)3885     public void smoothScrollToPosition(int position, int boundPosition) {
3886         if (mPositionScroller == null) {
3887             mPositionScroller = new PositionScroller();
3888         }
3889         mPositionScroller.start(position, boundPosition);
3890     }
3891 
3892     /**
3893      * Smoothly scroll by distance pixels over duration milliseconds.
3894      * @param distance Distance to scroll in pixels.
3895      * @param duration Duration of the scroll animation in milliseconds.
3896      */
smoothScrollBy(int distance, int duration)3897     public void smoothScrollBy(int distance, int duration) {
3898         // No sense starting to scroll if we're not going anywhere
3899         final int firstPosition = mFirstPosition;
3900         final int childCount = getChildCount();
3901         final int lastPosition = firstPosition + childCount;
3902         final int start = getStartEdge();
3903         final int end = getEndEdge();
3904 
3905         if (distance == 0 || mItemCount == 0 || childCount == 0 ||
3906                 (firstPosition == 0 && getChildStartEdge(getChildAt(0)) == start && distance < 0) ||
3907                 (lastPosition == mItemCount &&
3908                             getChildEndEdge(getChildAt(childCount - 1)) == end && distance > 0)) {
3909             finishSmoothScrolling();
3910         } else {
3911             mScroller.startScroll(0, 0,
3912                                   mIsVertical ? 0 : -distance,
3913                                   mIsVertical ? -distance : 0,
3914                                   duration);
3915 
3916             mLastTouchPos = 0;
3917 
3918             mTouchMode = TOUCH_MODE_FLINGING;
3919             reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
3920 
3921             ViewCompat.postInvalidateOnAnimation(this);
3922         }
3923     }
3924 
3925     @Override
dispatchKeyEvent(KeyEvent event)3926     public boolean dispatchKeyEvent(KeyEvent event) {
3927         // Dispatch in the normal way
3928         boolean handled = super.dispatchKeyEvent(event);
3929         if (!handled) {
3930             // If we didn't handle it...
3931             final View focused = getFocusedChild();
3932             if (focused != null && event.getAction() == KeyEvent.ACTION_DOWN) {
3933                 // ... and our focused child didn't handle it
3934                 // ... give it to ourselves so we can scroll if necessary
3935                 handled = onKeyDown(event.getKeyCode(), event);
3936             }
3937         }
3938 
3939         return handled;
3940     }
3941 
3942     @Override
dispatchSetPressed(boolean pressed)3943     protected void dispatchSetPressed(boolean pressed) {
3944         // Don't dispatch setPressed to our children. We call setPressed on ourselves to
3945         // get the selector in the right state, but we don't want to press each child.
3946     }
3947 
3948     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)3949     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
3950         if (mSelector == null) {
3951             useDefaultSelector();
3952         }
3953 
3954         int widthMode = MeasureSpec.getMode(widthMeasureSpec);
3955         int heightMode = MeasureSpec.getMode(heightMeasureSpec);
3956         int widthSize = MeasureSpec.getSize(widthMeasureSpec);
3957         int heightSize = MeasureSpec.getSize(heightMeasureSpec);
3958 
3959         int childWidth = 0;
3960         int childHeight = 0;
3961 
3962         mItemCount = (mAdapter == null ? 0 : mAdapter.getCount());
3963         if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED ||
3964                 heightMode == MeasureSpec.UNSPECIFIED)) {
3965             final View child = obtainView(0, mIsScrap);
3966 
3967             final int secondaryMeasureSpec =
3968                     (mIsVertical ? widthMeasureSpec : heightMeasureSpec);
3969 
3970             measureScrapChild(child, 0, secondaryMeasureSpec);
3971 
3972             childWidth = child.getMeasuredWidth();
3973             childHeight = child.getMeasuredHeight();
3974 
3975             if (recycleOnMeasure()) {
3976                 mRecycler.addScrapView(child, -1);
3977             }
3978         }
3979 
3980         if (widthMode == MeasureSpec.UNSPECIFIED) {
3981             widthSize = getPaddingLeft() + getPaddingRight() + childWidth;
3982             if (mIsVertical) {
3983                 widthSize += getVerticalScrollbarWidth();
3984             }
3985         }
3986 
3987         if (heightMode == MeasureSpec.UNSPECIFIED) {
3988             heightSize = getPaddingTop() + getPaddingBottom() + childHeight;
3989             if (!mIsVertical) {
3990                 heightSize += getHorizontalScrollbarHeight();
3991             }
3992         }
3993 
3994         if (mIsVertical && heightMode == MeasureSpec.AT_MOST) {
3995             heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
3996         }
3997 
3998         if (!mIsVertical && widthMode == MeasureSpec.AT_MOST) {
3999             widthSize = measureWidthOfChildren(heightMeasureSpec, 0, NO_POSITION, widthSize, -1);
4000         }
4001 
4002         setMeasuredDimension(widthSize, heightSize);
4003     }
4004 
4005     @Override
onLayout(boolean changed, int l, int t, int r, int b)4006     protected void onLayout(boolean changed, int l, int t, int r, int b) {
4007         mInLayout = true;
4008 
4009         if (changed) {
4010             final int childCount = getChildCount();
4011             for (int i = 0; i < childCount; i++) {
4012                 getChildAt(i).forceLayout();
4013             }
4014 
4015             mRecycler.markChildrenDirty();
4016         }
4017 
4018         layoutChildren();
4019 
4020         mInLayout = false;
4021 
4022         final int width = r - l - getPaddingLeft() - getPaddingRight();
4023         final int height = b - t - getPaddingTop() - getPaddingBottom();
4024 
4025         if (mStartEdge != null && mEndEdge != null) {
4026             if (mIsVertical) {
4027                 mStartEdge.setSize(width, height);
4028                 mEndEdge.setSize(width, height);
4029             } else {
4030                 mStartEdge.setSize(height, width);
4031                 mEndEdge.setSize(height, width);
4032             }
4033         }
4034     }
4035 
layoutChildren()4036     private void layoutChildren() {
4037         if (getWidth() == 0 || getHeight() == 0) {
4038             return;
4039         }
4040 
4041         final boolean blockLayoutRequests = mBlockLayoutRequests;
4042         if (!blockLayoutRequests) {
4043             mBlockLayoutRequests = true;
4044         } else {
4045             return;
4046         }
4047 
4048         try {
4049             invalidate();
4050 
4051             if (mAdapter == null) {
4052                 resetState();
4053                 return;
4054             }
4055 
4056             final int start = getStartEdge();
4057             final int end = getEndEdge();
4058 
4059             int childCount = getChildCount();
4060             int index = 0;
4061             int delta = 0;
4062 
4063             View focusLayoutRestoreView = null;
4064 
4065             View selected = null;
4066             View oldSelected = null;
4067             View newSelected = null;
4068             View oldFirstChild = null;
4069 
4070             switch (mLayoutMode) {
4071             case LAYOUT_SET_SELECTION:
4072                 index = mNextSelectedPosition - mFirstPosition;
4073                 if (index >= 0 && index < childCount) {
4074                     newSelected = getChildAt(index);
4075                 }
4076 
4077                 break;
4078 
4079             case LAYOUT_FORCE_TOP:
4080             case LAYOUT_FORCE_BOTTOM:
4081             case LAYOUT_SPECIFIC:
4082             case LAYOUT_SYNC:
4083                 break;
4084 
4085             case LAYOUT_MOVE_SELECTION:
4086             default:
4087                 // Remember the previously selected view
4088                 index = mSelectedPosition - mFirstPosition;
4089                 if (index >= 0 && index < childCount) {
4090                     oldSelected = getChildAt(index);
4091                 }
4092 
4093                 // Remember the previous first child
4094                 oldFirstChild = getChildAt(0);
4095 
4096                 if (mNextSelectedPosition >= 0) {
4097                     delta = mNextSelectedPosition - mSelectedPosition;
4098                 }
4099 
4100                 // Caution: newSelected might be null
4101                 newSelected = getChildAt(index + delta);
4102             }
4103 
4104             final boolean dataChanged = mDataChanged;
4105             if (dataChanged) {
4106                 handleDataChanged();
4107             }
4108 
4109             // Handle the empty set by removing all views that are visible
4110             // and calling it a day
4111             if (mItemCount == 0) {
4112                 resetState();
4113                 return;
4114             } else if (mItemCount != mAdapter.getCount()) {
4115                 throw new IllegalStateException("The content of the adapter has changed but "
4116                         + "TwoWayView did not receive a notification. Make sure the content of "
4117                         + "your adapter is not modified from a background thread, but only "
4118                         + "from the UI thread. [in TwoWayView(" + getId() + ", " + getClass()
4119                         + ") with Adapter(" + mAdapter.getClass() + ")]");
4120             }
4121 
4122             setSelectedPositionInt(mNextSelectedPosition);
4123 
4124             // Reset the focus restoration
4125             View focusLayoutRestoreDirectChild = null;
4126 
4127             // Pull all children into the RecycleBin.
4128             // These views will be reused if possible
4129             final int firstPosition = mFirstPosition;
4130             final RecycleBin recycleBin = mRecycler;
4131 
4132             if (dataChanged) {
4133                 for (int i = 0; i < childCount; i++) {
4134                     recycleBin.addScrapView(getChildAt(i), firstPosition + i);
4135                 }
4136             } else {
4137                 recycleBin.fillActiveViews(childCount, firstPosition);
4138             }
4139 
4140             // Take focus back to us temporarily to avoid the eventual
4141             // call to clear focus when removing the focused child below
4142             // from messing things up when ViewAncestor assigns focus back
4143             // to someone else.
4144             final View focusedChild = getFocusedChild();
4145             if (focusedChild != null) {
4146                 // We can remember the focused view to restore after relayout if the
4147                 // data hasn't changed, or if the focused position is a header or footer.
4148                 if (!dataChanged) {
4149                     focusLayoutRestoreDirectChild = focusedChild;
4150 
4151                     // Remember the specific view that had focus
4152                     focusLayoutRestoreView = findFocus();
4153                     if (focusLayoutRestoreView != null) {
4154                         // Tell it we are going to mess with it
4155                         focusLayoutRestoreView.onStartTemporaryDetach();
4156                     }
4157                 }
4158 
4159                 requestFocus();
4160             }
4161 
4162             // FIXME: We need a way to save current accessibility focus here
4163             // so that it can be restored after we re-attach the children on each
4164             // layout round.
4165 
4166             detachAllViewsFromParent();
4167 
4168             switch (mLayoutMode) {
4169             case LAYOUT_SET_SELECTION:
4170                 if (newSelected != null) {
4171                     final int newSelectedStart = getChildStartEdge(newSelected);
4172                     selected = fillFromSelection(newSelectedStart, start, end);
4173                 } else {
4174                     selected = fillFromMiddle(start, end);
4175                 }
4176 
4177                 break;
4178 
4179             case LAYOUT_SYNC:
4180                 selected = fillSpecific(mSyncPosition, mSpecificStart);
4181                 break;
4182 
4183             case LAYOUT_FORCE_BOTTOM:
4184                 selected = fillBefore(mItemCount - 1, end);
4185                 adjustViewsStartOrEnd();
4186                 break;
4187 
4188             case LAYOUT_FORCE_TOP:
4189                 mFirstPosition = 0;
4190                 selected = fillFromOffset(start);
4191                 adjustViewsStartOrEnd();
4192                 break;
4193 
4194             case LAYOUT_SPECIFIC:
4195                 selected = fillSpecific(reconcileSelectedPosition(), mSpecificStart);
4196                 break;
4197 
4198             case LAYOUT_MOVE_SELECTION:
4199                 selected = moveSelection(oldSelected, newSelected, delta, start, end);
4200                 break;
4201 
4202             default:
4203                 if (childCount == 0) {
4204                     final int position = lookForSelectablePosition(0);
4205                     setSelectedPositionInt(position);
4206                     selected = fillFromOffset(start);
4207                 } else {
4208                     if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) {
4209                         int offset = start;
4210                         if (oldSelected != null) {
4211                             offset = getChildStartEdge(oldSelected);
4212                         }
4213                         selected = fillSpecific(mSelectedPosition, offset);
4214                     } else if (mFirstPosition < mItemCount) {
4215                         int offset = start;
4216                         if (oldFirstChild != null) {
4217                             offset = getChildStartEdge(oldFirstChild);
4218                         }
4219 
4220                         selected = fillSpecific(mFirstPosition, offset);
4221                     } else {
4222                         selected = fillSpecific(0, start);
4223                     }
4224                 }
4225 
4226                 break;
4227 
4228             }
4229 
4230             recycleBin.scrapActiveViews();
4231 
4232             if (selected != null) {
4233                 if (mItemsCanFocus && hasFocus() && !selected.hasFocus()) {
4234                     final boolean focusWasTaken = (selected == focusLayoutRestoreDirectChild &&
4235                             focusLayoutRestoreView != null &&
4236                             focusLayoutRestoreView.requestFocus()) || selected.requestFocus();
4237 
4238                     if (!focusWasTaken) {
4239                         // Selected item didn't take focus, fine, but still want
4240                         // to make sure something else outside of the selected view
4241                         // has focus
4242                         final View focused = getFocusedChild();
4243                         if (focused != null) {
4244                             focused.clearFocus();
4245                         }
4246 
4247                         positionSelector(INVALID_POSITION, selected);
4248                     } else {
4249                         selected.setSelected(false);
4250                         mSelectorRect.setEmpty();
4251                     }
4252                 } else {
4253                     positionSelector(INVALID_POSITION, selected);
4254                 }
4255 
4256                 mSelectedStart = getChildStartEdge(selected);
4257             } else {
4258                 if (mTouchMode > TOUCH_MODE_DOWN && mTouchMode < TOUCH_MODE_DRAGGING) {
4259                     View child = getChildAt(mMotionPosition - mFirstPosition);
4260 
4261                     if (child != null) {
4262                         positionSelector(mMotionPosition, child);
4263                     }
4264                 } else {
4265                     mSelectedStart = 0;
4266                     mSelectorRect.setEmpty();
4267                 }
4268 
4269                 // Even if there is not selected position, we may need to restore
4270                 // focus (i.e. something focusable in touch mode)
4271                 if (hasFocus() && focusLayoutRestoreView != null) {
4272                     focusLayoutRestoreView.requestFocus();
4273                 }
4274             }
4275 
4276             // Tell focus view we are done mucking with it, if it is still in
4277             // our view hierarchy.
4278             if (focusLayoutRestoreView != null
4279                     && focusLayoutRestoreView.getWindowToken() != null) {
4280                 focusLayoutRestoreView.onFinishTemporaryDetach();
4281             }
4282 
4283             mLayoutMode = LAYOUT_NORMAL;
4284             mDataChanged = false;
4285             mNeedSync = false;
4286 
4287             setNextSelectedPositionInt(mSelectedPosition);
4288             if (mItemCount > 0) {
4289                 checkSelectionChanged();
4290             }
4291 
4292             invokeOnItemScrollListener();
4293         } finally {
4294             if (!blockLayoutRequests) {
4295                 mBlockLayoutRequests = false;
4296                 mDataChanged = false;
4297             }
4298         }
4299     }
4300 
recycleOnMeasure()4301     protected boolean recycleOnMeasure() {
4302         return true;
4303     }
4304 
offsetChildren(int offset)4305     private void offsetChildren(int offset) {
4306         final int childCount = getChildCount();
4307 
4308         for (int i = 0; i < childCount; i++) {
4309             final View child = getChildAt(i);
4310 
4311             if (mIsVertical) {
4312                 child.offsetTopAndBottom(offset);
4313             } else {
4314                 child.offsetLeftAndRight(offset);
4315             }
4316         }
4317     }
4318 
moveSelection(View oldSelected, View newSelected, int delta, int start, int end)4319     private View moveSelection(View oldSelected, View newSelected, int delta, int start,
4320             int end) {
4321         final int fadingEdgeLength = getFadingEdgeLength();
4322         final int selectedPosition = mSelectedPosition;
4323 
4324         final int oldSelectedStart = getChildStartEdge(oldSelected);
4325         final int oldSelectedEnd = getChildEndEdge(oldSelected);
4326 
4327         final int minStart = getMinSelectionPixel(start, fadingEdgeLength, selectedPosition);
4328         final int maxEnd = getMaxSelectionPixel(end, fadingEdgeLength, selectedPosition);
4329 
4330         View selected = null;
4331 
4332         if (delta > 0) {
4333             /*
4334              * Case 1: Scrolling down.
4335              */
4336 
4337             /*
4338              *     Before           After
4339              *    |       |        |       |
4340              *    +-------+        +-------+
4341              *    |   A   |        |   A   |
4342              *    |   1   |   =>   +-------+
4343              *    +-------+        |   B   |
4344              *    |   B   |        |   2   |
4345              *    +-------+        +-------+
4346              *    |       |        |       |
4347              *
4348              *    Try to keep the top of the previously selected item where it was.
4349              *    oldSelected = A
4350              *    selected = B
4351              */
4352 
4353             // Put oldSelected (A) where it belongs
4354             oldSelected = makeAndAddView(selectedPosition - 1, oldSelectedStart, true, false);
4355 
4356             final int itemMargin = mItemMargin;
4357 
4358             // Now put the new selection (B) below that
4359             selected = makeAndAddView(selectedPosition, oldSelectedEnd + itemMargin, true, true);
4360 
4361             final int selectedStart = getChildStartEdge(selected);
4362             final int selectedEnd = getChildEndEdge(selected);
4363 
4364             // Some of the newly selected item extends below the bottom of the list
4365             if (selectedEnd > end) {
4366                 // Find space available above the selection into which we can scroll upwards
4367                 final int spaceBefore = selectedStart - minStart;
4368 
4369                 // Find space required to bring the bottom of the selected item fully into view
4370                 final int spaceAfter = selectedEnd - maxEnd;
4371 
4372                 // Don't scroll more than half the size of the list
4373                 final int halfSpace = (end - start) / 2;
4374                 int offset = Math.min(spaceBefore, spaceAfter);
4375                 offset = Math.min(offset, halfSpace);
4376 
4377                 if (mIsVertical) {
4378                     oldSelected.offsetTopAndBottom(-offset);
4379                     selected.offsetTopAndBottom(-offset);
4380                 } else {
4381                     oldSelected.offsetLeftAndRight(-offset);
4382                     selected.offsetLeftAndRight(-offset);
4383                 }
4384             }
4385 
4386             // Fill in views before and after
4387             fillBefore(mSelectedPosition - 2, selectedStart - itemMargin);
4388             adjustViewsStartOrEnd();
4389             fillAfter(mSelectedPosition + 1, selectedEnd + itemMargin);
4390         } else if (delta < 0) {
4391             /*
4392              * Case 2: Scrolling up.
4393              */
4394 
4395             /*
4396              *     Before           After
4397              *    |       |        |       |
4398              *    +-------+        +-------+
4399              *    |   A   |        |   A   |
4400              *    +-------+   =>   |   1   |
4401              *    |   B   |        +-------+
4402              *    |   2   |        |   B   |
4403              *    +-------+        +-------+
4404              *    |       |        |       |
4405              *
4406              *    Try to keep the top of the item about to become selected where it was.
4407              *    newSelected = A
4408              *    olSelected = B
4409              */
4410 
4411             if (newSelected != null) {
4412                 // Try to position the top of newSel (A) where it was before it was selected
4413                 final int newSelectedStart = getChildStartEdge(newSelected);
4414                 selected = makeAndAddView(selectedPosition, newSelectedStart, true, true);
4415             } else {
4416                 // If (A) was not on screen and so did not have a view, position
4417                 // it above the oldSelected (B)
4418                 selected = makeAndAddView(selectedPosition, oldSelectedStart, false, true);
4419             }
4420 
4421             final int selectedStart = getChildStartEdge(selected);
4422             final int selectedEnd = getChildEndEdge(selected);
4423 
4424             // Some of the newly selected item extends above the top of the list
4425             if (selectedStart < minStart) {
4426                 // Find space required to bring the top of the selected item fully into view
4427                 final int spaceBefore = minStart - selectedStart;
4428 
4429                // Find space available below the selection into which we can scroll downwards
4430                 final int spaceAfter = maxEnd - selectedEnd;
4431 
4432                 // Don't scroll more than half the height of the list
4433                 final int halfSpace = (end - start) / 2;
4434                 int offset = Math.min(spaceBefore, spaceAfter);
4435                 offset = Math.min(offset, halfSpace);
4436 
4437                 if (mIsVertical) {
4438                     selected.offsetTopAndBottom(offset);
4439                 } else {
4440                     selected.offsetLeftAndRight(offset);
4441                 }
4442             }
4443 
4444             // Fill in views above and below
4445             fillBeforeAndAfter(selected, selectedPosition);
4446         } else {
4447             /*
4448              * Case 3: Staying still
4449              */
4450 
4451             selected = makeAndAddView(selectedPosition, oldSelectedStart, true, true);
4452 
4453             final int selectedStart = getChildStartEdge(selected);
4454             final int selectedEnd = getChildEndEdge(selected);
4455 
4456             // We're staying still...
4457             if (oldSelectedStart < start) {
4458                 // ... but the top of the old selection was off screen.
4459                 // (This can happen if the data changes size out from under us)
4460                 int newEnd = selectedEnd;
4461                 if (newEnd < start + 20) {
4462                     // Not enough visible -- bring it onscreen
4463                     if (mIsVertical) {
4464                         selected.offsetTopAndBottom(start - selectedStart);
4465                     } else {
4466                         selected.offsetLeftAndRight(start - selectedStart);
4467                     }
4468                 }
4469             }
4470 
4471             // Fill in views above and below
4472             fillBeforeAndAfter(selected, selectedPosition);
4473         }
4474 
4475         return selected;
4476     }
4477 
confirmCheckedPositionsById()4478     void confirmCheckedPositionsById() {
4479         // Clear out the positional check states, we'll rebuild it below from IDs.
4480         mCheckStates.clear();
4481 
4482         for (int checkedIndex = 0; checkedIndex < mCheckedIdStates.size(); checkedIndex++) {
4483             final long id = mCheckedIdStates.keyAt(checkedIndex);
4484             final int lastPos = mCheckedIdStates.valueAt(checkedIndex);
4485 
4486             final long lastPosId = mAdapter.getItemId(lastPos);
4487             if (id != lastPosId) {
4488                 // Look around to see if the ID is nearby. If not, uncheck it.
4489                 final int start = Math.max(0, lastPos - CHECK_POSITION_SEARCH_DISTANCE);
4490                 final int end = Math.min(lastPos + CHECK_POSITION_SEARCH_DISTANCE, mItemCount);
4491                 boolean found = false;
4492 
4493                 for (int searchPos = start; searchPos < end; searchPos++) {
4494                     final long searchId = mAdapter.getItemId(searchPos);
4495                     if (id == searchId) {
4496                         found = true;
4497                         mCheckStates.put(searchPos, true);
4498                         mCheckedIdStates.setValueAt(checkedIndex, searchPos);
4499                         break;
4500                     }
4501                 }
4502 
4503                 if (!found) {
4504                     mCheckedIdStates.delete(id);
4505                     checkedIndex--;
4506                     mCheckedItemCount--;
4507                 }
4508             } else {
4509                 mCheckStates.put(lastPos, true);
4510             }
4511         }
4512     }
4513 
handleDataChanged()4514     private void handleDataChanged() {
4515         if (mChoiceMode != ChoiceMode.NONE && mAdapter != null && mAdapter.hasStableIds()) {
4516             confirmCheckedPositionsById();
4517         }
4518 
4519         mRecycler.clearTransientStateViews();
4520 
4521         final int itemCount = mItemCount;
4522         if (itemCount > 0) {
4523             int newPos;
4524             int selectablePos;
4525 
4526             // Find the row we are supposed to sync to
4527             if (mNeedSync) {
4528                 // Update this first, since setNextSelectedPositionInt inspects it
4529                 mNeedSync = false;
4530                 mPendingSync = null;
4531 
4532                 switch (mSyncMode) {
4533                 case SYNC_SELECTED_POSITION:
4534                     if (isInTouchMode()) {
4535                         // We saved our state when not in touch mode. (We know this because
4536                         // mSyncMode is SYNC_SELECTED_POSITION.) Now we are trying to
4537                         // restore in touch mode. Just leave mSyncPosition as it is (possibly
4538                         // adjusting if the available range changed) and return.
4539                         mLayoutMode = LAYOUT_SYNC;
4540                         mSyncPosition = Math.min(Math.max(0, mSyncPosition), itemCount - 1);
4541 
4542                         return;
4543                     } else {
4544                         // See if we can find a position in the new data with the same
4545                         // id as the old selection. This will change mSyncPosition.
4546                         newPos = findSyncPosition();
4547                         if (newPos >= 0) {
4548                             // Found it. Now verify that new selection is still selectable
4549                             selectablePos = lookForSelectablePosition(newPos, true);
4550                             if (selectablePos == newPos) {
4551                                 // Same row id is selected
4552                                 mSyncPosition = newPos;
4553 
4554                                 if (mSyncSize == getSize()) {
4555                                     // If we are at the same height as when we saved state, try
4556                                     // to restore the scroll position too.
4557                                     mLayoutMode = LAYOUT_SYNC;
4558                                 } else {
4559                                     // We are not the same height as when the selection was saved, so
4560                                     // don't try to restore the exact position
4561                                     mLayoutMode = LAYOUT_SET_SELECTION;
4562                                 }
4563 
4564                                 // Restore selection
4565                                 setNextSelectedPositionInt(newPos);
4566                                 return;
4567                             }
4568                         }
4569                     }
4570                     break;
4571 
4572                 case SYNC_FIRST_POSITION:
4573                     // Leave mSyncPosition as it is -- just pin to available range
4574                     mLayoutMode = LAYOUT_SYNC;
4575                     mSyncPosition = Math.min(Math.max(0, mSyncPosition), itemCount - 1);
4576 
4577                     return;
4578                 }
4579             }
4580 
4581             if (!isInTouchMode()) {
4582                 // We couldn't find matching data -- try to use the same position
4583                 newPos = getSelectedItemPosition();
4584 
4585                 // Pin position to the available range
4586                 if (newPos >= itemCount) {
4587                     newPos = itemCount - 1;
4588                 }
4589                 if (newPos < 0) {
4590                     newPos = 0;
4591                 }
4592 
4593                 // Make sure we select something selectable -- first look down
4594                 selectablePos = lookForSelectablePosition(newPos, true);
4595 
4596                 if (selectablePos >= 0) {
4597                     setNextSelectedPositionInt(selectablePos);
4598                     return;
4599                 } else {
4600                     // Looking down didn't work -- try looking up
4601                     selectablePos = lookForSelectablePosition(newPos, false);
4602                     if (selectablePos >= 0) {
4603                         setNextSelectedPositionInt(selectablePos);
4604                         return;
4605                     }
4606                 }
4607             } else {
4608                 // We already know where we want to resurrect the selection
4609                 if (mResurrectToPosition >= 0) {
4610                     return;
4611                 }
4612             }
4613         }
4614 
4615         // Nothing is selected. Give up and reset everything.
4616         mLayoutMode = LAYOUT_FORCE_TOP;
4617         mSelectedPosition = INVALID_POSITION;
4618         mSelectedRowId = INVALID_ROW_ID;
4619         mNextSelectedPosition = INVALID_POSITION;
4620         mNextSelectedRowId = INVALID_ROW_ID;
4621         mNeedSync = false;
4622         mPendingSync = null;
4623         mSelectorPosition = INVALID_POSITION;
4624 
4625         checkSelectionChanged();
4626     }
4627 
reconcileSelectedPosition()4628     private int reconcileSelectedPosition() {
4629         int position = mSelectedPosition;
4630         if (position < 0) {
4631             position = mResurrectToPosition;
4632         }
4633 
4634         position = Math.max(0, position);
4635         position = Math.min(position, mItemCount - 1);
4636 
4637         return position;
4638     }
4639 
resurrectSelection()4640     boolean resurrectSelection() {
4641         final int childCount = getChildCount();
4642         if (childCount <= 0) {
4643             return false;
4644         }
4645 
4646         int selectedStart = 0;
4647         int selectedPosition;
4648 
4649         int start = getStartEdge();
4650         int end = getEndEdge();
4651 
4652         final int firstPosition = mFirstPosition;
4653         final int toPosition = mResurrectToPosition;
4654         boolean down = true;
4655 
4656         if (toPosition >= firstPosition && toPosition < firstPosition + childCount) {
4657             selectedPosition = toPosition;
4658 
4659             final View selected = getChildAt(selectedPosition - mFirstPosition);
4660             selectedStart = getChildStartEdge(selected);
4661 
4662             final int selectedEnd = getChildEndEdge(selected);
4663 
4664             // We are scrolled, don't get in the fade
4665             if (selectedStart < start) {
4666                 selectedStart = start + getFadingEdgeLength();
4667             } else if (selectedEnd > end) {
4668                 selectedStart = end - getChildMeasuredSize(selected) - getFadingEdgeLength();
4669             }
4670         } else if (toPosition < firstPosition) {
4671             // Default to selecting whatever is first
4672             selectedPosition = firstPosition;
4673 
4674             for (int i = 0; i < childCount; i++) {
4675                 final View child = getChildAt(i);
4676                 final int childStart = getChildStartEdge(child);
4677 
4678                 if (i == 0) {
4679                     // Remember the position of the first item
4680                     selectedStart = childStart;
4681 
4682                     // See if we are scrolled at all
4683                     if (firstPosition > 0 || childStart < start) {
4684                         // If we are scrolled, don't select anything that is
4685                         // in the fade region
4686                         start += getFadingEdgeLength();
4687                     }
4688                 }
4689 
4690                 if (childStart >= start) {
4691                     // Found a view whose top is fully visible
4692                     selectedPosition = firstPosition + i;
4693                     selectedStart = childStart;
4694                     break;
4695                 }
4696             }
4697         } else {
4698             final int itemCount = mItemCount;
4699             selectedPosition = firstPosition + childCount - 1;
4700             down = false;
4701 
4702             for (int i = childCount - 1; i >= 0; i--) {
4703                 final View child = getChildAt(i);
4704                 final int childStart = getChildStartEdge(child);
4705                 final int childEnd = getChildEndEdge(child);
4706 
4707                 if (i == childCount - 1) {
4708                     selectedStart = childStart;
4709 
4710                     if (firstPosition + childCount < itemCount || childEnd > end) {
4711                         end -= getFadingEdgeLength();
4712                     }
4713                 }
4714 
4715                 if (childEnd <= end) {
4716                     selectedPosition = firstPosition + i;
4717                     selectedStart = childStart;
4718                     break;
4719                 }
4720             }
4721         }
4722 
4723         mResurrectToPosition = INVALID_POSITION;
4724 
4725         finishSmoothScrolling();
4726 
4727         mTouchMode = TOUCH_MODE_REST;
4728         reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
4729 
4730         mSpecificStart = selectedStart;
4731 
4732         selectedPosition = lookForSelectablePosition(selectedPosition, down);
4733         if (selectedPosition >= firstPosition && selectedPosition <= getLastVisiblePosition()) {
4734             mLayoutMode = LAYOUT_SPECIFIC;
4735             updateSelectorState();
4736             setSelectionInt(selectedPosition);
4737             invokeOnItemScrollListener();
4738         } else {
4739             selectedPosition = INVALID_POSITION;
4740         }
4741 
4742         return selectedPosition >= 0;
4743     }
4744 
4745     /**
4746      * If there is a selection returns false.
4747      * Otherwise resurrects the selection and returns true if resurrected.
4748      */
resurrectSelectionIfNeeded()4749     boolean resurrectSelectionIfNeeded() {
4750         if (mSelectedPosition < 0 && resurrectSelection()) {
4751             updateSelectorState();
4752             return true;
4753         }
4754 
4755         return false;
4756     }
4757 
getChildWidthMeasureSpec(LayoutParams lp)4758     private int getChildWidthMeasureSpec(LayoutParams lp) {
4759         if (!mIsVertical && lp.width == LayoutParams.WRAP_CONTENT) {
4760             return MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
4761         } else if (mIsVertical) {
4762             final int maxWidth = getWidth() - getPaddingLeft() - getPaddingRight();
4763             return MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.EXACTLY);
4764         } else {
4765             return MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY);
4766         }
4767     }
4768 
getChildHeightMeasureSpec(LayoutParams lp)4769     private int getChildHeightMeasureSpec(LayoutParams lp) {
4770         if (mIsVertical && lp.height == LayoutParams.WRAP_CONTENT) {
4771             return MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
4772         } else if (!mIsVertical) {
4773             final int maxHeight = getHeight() - getPaddingTop() - getPaddingBottom();
4774             return MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.EXACTLY);
4775         } else {
4776             return MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY);
4777         }
4778     }
4779 
measureChild(View child)4780     private void measureChild(View child) {
4781         measureChild(child, (LayoutParams) child.getLayoutParams());
4782     }
4783 
measureChild(View child, LayoutParams lp)4784     private void measureChild(View child, LayoutParams lp) {
4785         final int widthSpec = getChildWidthMeasureSpec(lp);
4786         final int heightSpec = getChildHeightMeasureSpec(lp);
4787         child.measure(widthSpec, heightSpec);
4788     }
4789 
relayoutMeasuredChild(View child)4790     private void relayoutMeasuredChild(View child) {
4791         final int w = child.getMeasuredWidth();
4792         final int h = child.getMeasuredHeight();
4793 
4794         final int childLeft = getPaddingLeft();
4795         final int childRight = childLeft + w;
4796         final int childTop = child.getTop();
4797         final int childBottom = childTop + h;
4798 
4799         child.layout(childLeft, childTop, childRight, childBottom);
4800     }
4801 
measureScrapChild(View scrapChild, int position, int secondaryMeasureSpec)4802     private void measureScrapChild(View scrapChild, int position, int secondaryMeasureSpec) {
4803         LayoutParams lp = (LayoutParams) scrapChild.getLayoutParams();
4804         if (lp == null) {
4805             lp = generateDefaultLayoutParams();
4806             scrapChild.setLayoutParams(lp);
4807         }
4808 
4809         lp.viewType = mAdapter.getItemViewType(position);
4810         lp.forceAdd = true;
4811 
4812         final int widthMeasureSpec;
4813         final int heightMeasureSpec;
4814         if (mIsVertical) {
4815             widthMeasureSpec = secondaryMeasureSpec;
4816             heightMeasureSpec = getChildHeightMeasureSpec(lp);
4817         } else {
4818             widthMeasureSpec = getChildWidthMeasureSpec(lp);
4819             heightMeasureSpec = secondaryMeasureSpec;
4820         }
4821 
4822         scrapChild.measure(widthMeasureSpec, heightMeasureSpec);
4823     }
4824 
4825     /**
4826      * Measures the height of the given range of children (inclusive) and
4827      * returns the height with this TwoWayView's padding and item margin heights
4828      * included. If maxHeight is provided, the measuring will stop when the
4829      * current height reaches maxHeight.
4830      *
4831      * @param widthMeasureSpec The width measure spec to be given to a child's
4832      *            {@link View#measure(int, int)}.
4833      * @param startPosition The position of the first child to be shown.
4834      * @param endPosition The (inclusive) position of the last child to be
4835      *            shown. Specify {@link #NO_POSITION} if the last child should be
4836      *            the last available child from the adapter.
4837      * @param maxHeight The maximum height that will be returned (if all the
4838      *            children don't fit in this value, this value will be
4839      *            returned).
4840      * @param disallowPartialChildPosition In general, whether the returned
4841      *            height should only contain entire children. This is more
4842      *            powerful--it is the first inclusive position at which partial
4843      *            children will not be allowed. Example: it looks nice to have
4844      *            at least 3 completely visible children, and in portrait this
4845      *            will most likely fit; but in landscape there could be times
4846      *            when even 2 children can not be completely shown, so a value
4847      *            of 2 (remember, inclusive) would be good (assuming
4848      *            startPosition is 0).
4849      * @return The height of this TwoWayView with the given children.
4850      */
measureHeightOfChildren(int widthMeasureSpec, int startPosition, int endPosition, final int maxHeight, int disallowPartialChildPosition)4851     private int measureHeightOfChildren(int widthMeasureSpec, int startPosition, int endPosition,
4852             final int maxHeight, int disallowPartialChildPosition) {
4853 
4854         final int paddingTop = getPaddingTop();
4855         final int paddingBottom = getPaddingBottom();
4856 
4857         final ListAdapter adapter = mAdapter;
4858         if (adapter == null) {
4859             return paddingTop + paddingBottom;
4860         }
4861 
4862         // Include the padding of the list
4863         int returnedHeight = paddingTop + paddingBottom;
4864         final int itemMargin = mItemMargin;
4865 
4866         // The previous height value that was less than maxHeight and contained
4867         // no partial children
4868         int prevHeightWithoutPartialChild = 0;
4869         int i;
4870         View child;
4871 
4872         // mItemCount - 1 since endPosition parameter is inclusive
4873         endPosition = (endPosition == NO_POSITION) ? adapter.getCount() - 1 : endPosition;
4874         final RecycleBin recycleBin = mRecycler;
4875         final boolean shouldRecycle = recycleOnMeasure();
4876         final boolean[] isScrap = mIsScrap;
4877 
4878         for (i = startPosition; i <= endPosition; ++i) {
4879             child = obtainView(i, isScrap);
4880 
4881             measureScrapChild(child, i, widthMeasureSpec);
4882 
4883             if (i > 0) {
4884                 // Count the item margin for all but one child
4885                 returnedHeight += itemMargin;
4886             }
4887 
4888             // Recycle the view before we possibly return from the method
4889             if (shouldRecycle) {
4890                 recycleBin.addScrapView(child, -1);
4891             }
4892 
4893             returnedHeight += child.getMeasuredHeight();
4894 
4895             if (returnedHeight >= maxHeight) {
4896                 // We went over, figure out which height to return.  If returnedHeight > maxHeight,
4897                 // then the i'th position did not fit completely.
4898                 return (disallowPartialChildPosition >= 0) // Disallowing is enabled (> -1)
4899                             && (i > disallowPartialChildPosition) // We've past the min pos
4900                             && (prevHeightWithoutPartialChild > 0) // We have a prev height
4901                             && (returnedHeight != maxHeight) // i'th child did not fit completely
4902                         ? prevHeightWithoutPartialChild
4903                         : maxHeight;
4904             }
4905 
4906             if ((disallowPartialChildPosition >= 0) && (i >= disallowPartialChildPosition)) {
4907                 prevHeightWithoutPartialChild = returnedHeight;
4908             }
4909         }
4910 
4911         // At this point, we went through the range of children, and they each
4912         // completely fit, so return the returnedHeight
4913         return returnedHeight;
4914     }
4915 
4916     /**
4917      * Measures the width of the given range of children (inclusive) and
4918      * returns the width with this TwoWayView's padding and item margin widths
4919      * included. If maxWidth is provided, the measuring will stop when the
4920      * current width reaches maxWidth.
4921      *
4922      * @param heightMeasureSpec The height measure spec to be given to a child's
4923      *            {@link View#measure(int, int)}.
4924      * @param startPosition The position of the first child to be shown.
4925      * @param endPosition The (inclusive) position of the last child to be
4926      *            shown. Specify {@link #NO_POSITION} if the last child should be
4927      *            the last available child from the adapter.
4928      * @param maxWidth The maximum width that will be returned (if all the
4929      *            children don't fit in this value, this value will be
4930      *            returned).
4931      * @param disallowPartialChildPosition In general, whether the returned
4932      *            width should only contain entire children. This is more
4933      *            powerful--it is the first inclusive position at which partial
4934      *            children will not be allowed. Example: it looks nice to have
4935      *            at least 3 completely visible children, and in portrait this
4936      *            will most likely fit; but in landscape there could be times
4937      *            when even 2 children can not be completely shown, so a value
4938      *            of 2 (remember, inclusive) would be good (assuming
4939      *            startPosition is 0).
4940      * @return The width of this TwoWayView with the given children.
4941      */
measureWidthOfChildren(int heightMeasureSpec, int startPosition, int endPosition, final int maxWidth, int disallowPartialChildPosition)4942     private int measureWidthOfChildren(int heightMeasureSpec, int startPosition, int endPosition,
4943             final int maxWidth, int disallowPartialChildPosition) {
4944 
4945         final int paddingLeft = getPaddingLeft();
4946         final int paddingRight = getPaddingRight();
4947 
4948         final ListAdapter adapter = mAdapter;
4949         if (adapter == null) {
4950             return paddingLeft + paddingRight;
4951         }
4952 
4953         // Include the padding of the list
4954         int returnedWidth = paddingLeft + paddingRight;
4955         final int itemMargin = mItemMargin;
4956 
4957         // The previous height value that was less than maxHeight and contained
4958         // no partial children
4959         int prevWidthWithoutPartialChild = 0;
4960         int i;
4961         View child;
4962 
4963         // mItemCount - 1 since endPosition parameter is inclusive
4964         endPosition = (endPosition == NO_POSITION) ? adapter.getCount() - 1 : endPosition;
4965         final RecycleBin recycleBin = mRecycler;
4966         final boolean shouldRecycle = recycleOnMeasure();
4967         final boolean[] isScrap = mIsScrap;
4968 
4969         for (i = startPosition; i <= endPosition; ++i) {
4970             child = obtainView(i, isScrap);
4971 
4972             measureScrapChild(child, i, heightMeasureSpec);
4973 
4974             if (i > 0) {
4975                 // Count the item margin for all but one child
4976                 returnedWidth += itemMargin;
4977             }
4978 
4979             // Recycle the view before we possibly return from the method
4980             if (shouldRecycle) {
4981                 recycleBin.addScrapView(child, -1);
4982             }
4983 
4984             returnedWidth += child.getMeasuredWidth();
4985 
4986             if (returnedWidth >= maxWidth) {
4987                 // We went over, figure out which width to return.  If returnedWidth > maxWidth,
4988                 // then the i'th position did not fit completely.
4989                 return (disallowPartialChildPosition >= 0) // Disallowing is enabled (> -1)
4990                             && (i > disallowPartialChildPosition) // We've past the min pos
4991                             && (prevWidthWithoutPartialChild > 0) // We have a prev width
4992                             && (returnedWidth != maxWidth) // i'th child did not fit completely
4993                         ? prevWidthWithoutPartialChild
4994                         : maxWidth;
4995             }
4996 
4997             if ((disallowPartialChildPosition >= 0) && (i >= disallowPartialChildPosition)) {
4998                 prevWidthWithoutPartialChild = returnedWidth;
4999             }
5000         }
5001 
5002         // At this point, we went through the range of children, and they each
5003         // completely fit, so return the returnedWidth
5004         return returnedWidth;
5005     }
5006 
makeAndAddView(int position, int offset, boolean flow, boolean selected)5007     private View makeAndAddView(int position, int offset, boolean flow, boolean selected) {
5008         final int top;
5009         final int left;
5010 
5011         // Compensate item margin on the first item of the list if the item margin
5012         // is negative to avoid incorrect offset for the very first child.
5013         if (mIsVertical) {
5014             top = offset;
5015             left = getPaddingLeft();
5016         } else {
5017             top = getPaddingTop();
5018             left = offset;
5019         }
5020 
5021         if (!mDataChanged) {
5022             // Try to use an existing view for this position
5023             final View activeChild = mRecycler.getActiveView(position);
5024             if (activeChild != null) {
5025                 // Found it -- we're using an existing child
5026                 // This just needs to be positioned
5027                 setupChild(activeChild, position, top, left, flow, selected, true);
5028 
5029                 return activeChild;
5030             }
5031         }
5032 
5033         // Make a new view for this position, or convert an unused view if possible
5034         final View child = obtainView(position, mIsScrap);
5035 
5036         // This needs to be positioned and measured
5037         setupChild(child, position, top, left, flow, selected, mIsScrap[0]);
5038 
5039         return child;
5040     }
5041 
5042     @TargetApi(11)
setupChild(View child, int position, int top, int left, boolean flow, boolean selected, boolean recycled)5043     private void setupChild(View child, int position, int top, int left,
5044             boolean flow, boolean selected, boolean recycled) {
5045         final boolean isSelected = selected && shouldShowSelector();
5046         final boolean updateChildSelected = isSelected != child.isSelected();
5047         final int touchMode = mTouchMode;
5048 
5049         final boolean isPressed = touchMode > TOUCH_MODE_DOWN && touchMode < TOUCH_MODE_DRAGGING &&
5050                 mMotionPosition == position;
5051 
5052         final boolean updateChildPressed = isPressed != child.isPressed();
5053         final boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested();
5054 
5055         // Respect layout params that are already in the view. Otherwise make some up...
5056         LayoutParams lp = (LayoutParams) child.getLayoutParams();
5057         if (lp == null) {
5058             lp = generateDefaultLayoutParams();
5059         }
5060 
5061         lp.viewType = mAdapter.getItemViewType(position);
5062 
5063         if (recycled && !lp.forceAdd) {
5064             attachViewToParent(child, (flow ? -1 : 0), lp);
5065         } else {
5066             lp.forceAdd = false;
5067             addViewInLayout(child, (flow ? -1 : 0), lp, true);
5068         }
5069 
5070         if (updateChildSelected) {
5071             child.setSelected(isSelected);
5072         }
5073 
5074         if (updateChildPressed) {
5075             child.setPressed(isPressed);
5076         }
5077 
5078         if (mChoiceMode != ChoiceMode.NONE && mCheckStates != null) {
5079             if (child instanceof Checkable) {
5080                 ((Checkable) child).setChecked(mCheckStates.get(position));
5081             } else if (Build.VERSION.SDK_INT >= HONEYCOMB) {
5082                 child.setActivated(mCheckStates.get(position));
5083             }
5084         }
5085 
5086         if (needToMeasure) {
5087             measureChild(child, lp);
5088         } else {
5089             cleanupLayoutState(child);
5090         }
5091 
5092         final int w = child.getMeasuredWidth();
5093         final int h = child.getMeasuredHeight();
5094 
5095         final int childTop = (mIsVertical && !flow ? top - h : top);
5096         final int childLeft = (!mIsVertical && !flow ? left - w : left);
5097 
5098         if (needToMeasure) {
5099             final int childRight = childLeft + w;
5100             final int childBottom = childTop + h;
5101 
5102             child.layout(childLeft, childTop, childRight, childBottom);
5103         } else {
5104             child.offsetLeftAndRight(childLeft - child.getLeft());
5105             child.offsetTopAndBottom(childTop - child.getTop());
5106         }
5107     }
5108 
fillGap(boolean down)5109     void fillGap(boolean down) {
5110         final int childCount = getChildCount();
5111 
5112         if (down) {
5113             final int start = getStartEdge();
5114             final int lastEnd = getChildEndEdge(getChildAt(childCount - 1));
5115             final int offset = (childCount > 0 ? lastEnd + mItemMargin : start);
5116             fillAfter(mFirstPosition + childCount, offset);
5117             correctTooHigh(getChildCount());
5118         } else {
5119             final int end = getEndEdge();
5120             final int firstStart = getChildStartEdge(getChildAt(0));
5121             final int offset = (childCount > 0 ? firstStart - mItemMargin : end);
5122             fillBefore(mFirstPosition - 1, offset);
5123             correctTooLow(getChildCount());
5124         }
5125     }
5126 
fillBefore(int pos, int nextOffset)5127     private View fillBefore(int pos, int nextOffset) {
5128         View selectedView = null;
5129 
5130         final int start = getStartEdge();
5131 
5132         while (nextOffset > start && pos >= 0) {
5133             boolean isSelected = (pos == mSelectedPosition);
5134 
5135             View child = makeAndAddView(pos, nextOffset, false, isSelected);
5136             nextOffset = getChildStartEdge(child) - mItemMargin;
5137 
5138             if (isSelected) {
5139                 selectedView = child;
5140             }
5141 
5142             pos--;
5143         }
5144 
5145         mFirstPosition = pos + 1;
5146 
5147         return selectedView;
5148     }
5149 
fillAfter(int pos, int nextOffset)5150     private View fillAfter(int pos, int nextOffset) {
5151         View selectedView = null;
5152 
5153         final int end = getEndEdge();
5154 
5155         while (nextOffset < end && pos < mItemCount) {
5156             boolean selected = (pos == mSelectedPosition);
5157 
5158             View child = makeAndAddView(pos, nextOffset, true, selected);
5159             nextOffset = getChildEndEdge(child) + mItemMargin;
5160 
5161             if (selected) {
5162                 selectedView = child;
5163             }
5164 
5165             pos++;
5166         }
5167 
5168         return selectedView;
5169     }
5170 
fillSpecific(int position, int offset)5171     private View fillSpecific(int position, int offset) {
5172         final boolean tempIsSelected = (position == mSelectedPosition);
5173         View temp = makeAndAddView(position, offset, true, tempIsSelected);
5174 
5175         // Possibly changed again in fillBefore if we add rows above this one.
5176         mFirstPosition = position;
5177 
5178         final int offsetBefore = getChildStartEdge(temp) - mItemMargin;
5179         final View before = fillBefore(position - 1, offsetBefore);
5180 
5181         // This will correct for the top of the first view not touching the top of the list
5182         adjustViewsStartOrEnd();
5183 
5184         final int offsetAfter = getChildEndEdge(temp) + mItemMargin;
5185         final View after = fillAfter(position + 1, offsetAfter);
5186 
5187         final int childCount = getChildCount();
5188         if (childCount > 0) {
5189             correctTooHigh(childCount);
5190         }
5191 
5192         if (tempIsSelected) {
5193             return temp;
5194         } else if (before != null) {
5195             return before;
5196         } else {
5197             return after;
5198         }
5199     }
5200 
fillFromOffset(int nextOffset)5201     private View fillFromOffset(int nextOffset) {
5202         mFirstPosition = Math.min(mFirstPosition, mSelectedPosition);
5203         mFirstPosition = Math.min(mFirstPosition, mItemCount - 1);
5204 
5205         if (mFirstPosition < 0) {
5206             mFirstPosition = 0;
5207         }
5208 
5209         return fillAfter(mFirstPosition, nextOffset);
5210     }
5211 
fillFromMiddle(int start, int end)5212     private View fillFromMiddle(int start, int end) {
5213         final int size = end - start;
5214         int position = reconcileSelectedPosition();
5215 
5216         View selected = makeAndAddView(position, start, true, true);
5217         mFirstPosition = position;
5218 
5219         if (mIsVertical) {
5220             int selectedHeight = selected.getMeasuredHeight();
5221             if (selectedHeight <= size) {
5222                 selected.offsetTopAndBottom((size - selectedHeight) / 2);
5223             }
5224         } else {
5225             int selectedWidth = selected.getMeasuredWidth();
5226             if (selectedWidth <= size) {
5227                 selected.offsetLeftAndRight((size - selectedWidth) / 2);
5228             }
5229         }
5230 
5231         fillBeforeAndAfter(selected, position);
5232         correctTooHigh(getChildCount());
5233 
5234         return selected;
5235     }
5236 
fillBeforeAndAfter(View selected, int position)5237     private void fillBeforeAndAfter(View selected, int position) {
5238         final int offsetBefore = getChildStartEdge(selected) + mItemMargin;
5239         fillBefore(position - 1, offsetBefore);
5240 
5241         adjustViewsStartOrEnd();
5242 
5243         final int offsetAfter = getChildEndEdge(selected) + mItemMargin;
5244         fillAfter(position + 1, offsetAfter);
5245     }
5246 
fillFromSelection(int selectedTop, int start, int end)5247     private View fillFromSelection(int selectedTop, int start, int end) {
5248         int fadingEdgeLength = getFadingEdgeLength();
5249         final int selectedPosition = mSelectedPosition;
5250 
5251         final int minStart = getMinSelectionPixel(start, fadingEdgeLength, selectedPosition);
5252         final int maxEnd = getMaxSelectionPixel(end, fadingEdgeLength, selectedPosition);
5253 
5254         View selected = makeAndAddView(selectedPosition, selectedTop, true, true);
5255 
5256         final int selectedStart = getChildStartEdge(selected);
5257         final int selectedEnd = getChildEndEdge(selected);
5258 
5259         // Some of the newly selected item extends below the bottom of the list
5260         if (selectedEnd > maxEnd) {
5261             // Find space available above the selection into which we can scroll
5262             // upwards
5263             final int spaceAbove = selectedStart - minStart;
5264 
5265             // Find space required to bring the bottom of the selected item
5266             // fully into view
5267             final int spaceBelow = selectedEnd - maxEnd;
5268 
5269             final int offset = Math.min(spaceAbove, spaceBelow);
5270 
5271             // Now offset the selected item to get it into view
5272             selected.offsetTopAndBottom(-offset);
5273         } else if (selectedStart < minStart) {
5274             // Find space required to bring the top of the selected item fully
5275             // into view
5276             final int spaceAbove = minStart - selectedStart;
5277 
5278             // Find space available below the selection into which we can scroll
5279             // downwards
5280             final int spaceBelow = maxEnd - selectedEnd;
5281 
5282             final int offset = Math.min(spaceAbove, spaceBelow);
5283 
5284             // Offset the selected item to get it into view
5285             selected.offsetTopAndBottom(offset);
5286         }
5287 
5288         // Fill in views above and below
5289         fillBeforeAndAfter(selected, selectedPosition);
5290         correctTooHigh(getChildCount());
5291 
5292         return selected;
5293     }
5294 
correctTooHigh(int childCount)5295     private void correctTooHigh(int childCount) {
5296         // First see if the last item is visible. If it is not, it is OK for the
5297         // top of the list to be pushed up.
5298         final int lastPosition = mFirstPosition + childCount - 1;
5299         if (lastPosition != mItemCount - 1 || childCount == 0) {
5300             return;
5301         }
5302 
5303         // Get the last child end edge
5304         final int lastEnd = getChildEndEdge(getChildAt(childCount - 1));
5305 
5306         // This is bottom of our drawable area
5307         final int start = getStartEdge();
5308         final int end = getEndEdge();
5309 
5310         // This is how far the end edge of the last view is from the end of the
5311         // drawable area
5312         int endOffset = end - lastEnd;
5313 
5314         View firstChild = getChildAt(0);
5315         int firstStart = getChildStartEdge(firstChild);
5316 
5317         // Make sure we are 1) Too high, and 2) Either there are more rows above the
5318         // first row or the first row is scrolled off the top of the drawable area
5319         if (endOffset > 0 && (mFirstPosition > 0 || firstStart < start))  {
5320             if (mFirstPosition == 0) {
5321                 // Don't pull the top too far down
5322                 endOffset = Math.min(endOffset, start - firstStart);
5323             }
5324 
5325             // Move everything down
5326             offsetChildren(endOffset);
5327 
5328             if (mFirstPosition > 0) {
5329                 firstStart = getChildStartEdge(firstChild);
5330 
5331                 // Fill the gap that was opened above mFirstPosition with more rows, if
5332                 // possible
5333                 fillBefore(mFirstPosition - 1, firstStart - mItemMargin);
5334 
5335                 // Close up the remaining gap
5336                 adjustViewsStartOrEnd();
5337             }
5338         }
5339     }
5340 
correctTooLow(int childCount)5341     private void correctTooLow(int childCount) {
5342         // First see if the first item is visible. If it is not, it is OK for the
5343         // bottom of the list to be pushed down.
5344         if (mFirstPosition != 0 || childCount == 0) {
5345             return;
5346         }
5347 
5348         final int firstStart = getChildStartEdge(getChildAt(0));
5349 
5350         final int start = getStartEdge();
5351         final int end = getEndEdge();
5352 
5353         // This is how far the start edge of the first view is from the start of the
5354         // drawable area
5355         int startOffset = firstStart - start;
5356 
5357         View last = getChildAt(childCount - 1);
5358         int lastEnd = getChildEndEdge(last);
5359 
5360         int lastPosition = mFirstPosition + childCount - 1;
5361 
5362         // Make sure we are 1) Too low, and 2) Either there are more columns/rows below the
5363         // last column/row or the last column/row is scrolled off the end of the
5364         // drawable area
5365         if (startOffset > 0) {
5366             if (lastPosition < mItemCount - 1 || lastEnd > end)  {
5367                 if (lastPosition == mItemCount - 1) {
5368                     // Don't pull the bottom too far up
5369                     startOffset = Math.min(startOffset, lastEnd - end);
5370                 }
5371 
5372                 // Move everything up
5373                 offsetChildren(-startOffset);
5374 
5375                 if (lastPosition < mItemCount - 1) {
5376                     lastEnd = getChildEndEdge(last);
5377 
5378                     // Fill the gap that was opened below the last position with more rows, if
5379                     // possible
5380                     fillAfter(lastPosition + 1, lastEnd + mItemMargin);
5381 
5382                     // Close up the remaining gap
5383                     adjustViewsStartOrEnd();
5384                 }
5385             } else if (lastPosition == mItemCount - 1) {
5386                 adjustViewsStartOrEnd();
5387             }
5388         }
5389     }
5390 
adjustViewsStartOrEnd()5391     private void adjustViewsStartOrEnd() {
5392         if (getChildCount() == 0) {
5393             return;
5394         }
5395 
5396         int delta = getChildStartEdge(getChildAt(0)) - getStartEdge();
5397 
5398         // If item margin is negative we shouldn't apply it in the
5399         // first item of the list to avoid offsetting it incorrectly.
5400         if (mItemMargin >= 0 || mFirstPosition != 0) {
5401             delta -= mItemMargin;
5402         }
5403 
5404         if (delta < 0) {
5405             // We only are looking to see if we are too low, not too high
5406             delta = 0;
5407         }
5408 
5409         if (delta != 0) {
5410             offsetChildren(-delta);
5411         }
5412     }
5413 
5414     @TargetApi(14)
cloneCheckStates()5415     private SparseBooleanArray cloneCheckStates() {
5416         if (mCheckStates == null) {
5417             return null;
5418         }
5419 
5420         SparseBooleanArray checkedStates;
5421 
5422         if (Build.VERSION.SDK_INT >= 14) {
5423             checkedStates = mCheckStates.clone();
5424         } else {
5425             checkedStates = new SparseBooleanArray();
5426 
5427             for (int i = 0; i < mCheckStates.size(); i++) {
5428                 checkedStates.put(mCheckStates.keyAt(i), mCheckStates.valueAt(i));
5429             }
5430         }
5431 
5432         return checkedStates;
5433     }
5434 
findSyncPosition()5435     private int findSyncPosition() {
5436         int itemCount = mItemCount;
5437 
5438         if (itemCount == 0) {
5439             return INVALID_POSITION;
5440         }
5441 
5442         final long idToMatch = mSyncRowId;
5443 
5444         // If there isn't a selection don't hunt for it
5445         if (idToMatch == INVALID_ROW_ID) {
5446             return INVALID_POSITION;
5447         }
5448 
5449         // Pin seed to reasonable values
5450         int seed = mSyncPosition;
5451         seed = Math.max(0, seed);
5452         seed = Math.min(itemCount - 1, seed);
5453 
5454         long endTime = SystemClock.uptimeMillis() + SYNC_MAX_DURATION_MILLIS;
5455 
5456         long rowId;
5457 
5458         // first position scanned so far
5459         int first = seed;
5460 
5461         // last position scanned so far
5462         int last = seed;
5463 
5464         // True if we should move down on the next iteration
5465         boolean next = false;
5466 
5467         // True when we have looked at the first item in the data
5468         boolean hitFirst;
5469 
5470         // True when we have looked at the last item in the data
5471         boolean hitLast;
5472 
5473         // Get the item ID locally (instead of getItemIdAtPosition), so
5474         // we need the adapter
5475         final ListAdapter adapter = mAdapter;
5476         if (adapter == null) {
5477             return INVALID_POSITION;
5478         }
5479 
5480         while (SystemClock.uptimeMillis() <= endTime) {
5481             rowId = adapter.getItemId(seed);
5482             if (rowId == idToMatch) {
5483                 // Found it!
5484                 return seed;
5485             }
5486 
5487             hitLast = (last == itemCount - 1);
5488             hitFirst = (first == 0);
5489 
5490             if (hitLast && hitFirst) {
5491                 // Looked at everything
5492                 break;
5493             }
5494 
5495             if (hitFirst || (next && !hitLast)) {
5496                 // Either we hit the top, or we are trying to move down
5497                 last++;
5498                 seed = last;
5499 
5500                 // Try going up next time
5501                 next = false;
5502             } else if (hitLast || (!next && !hitFirst)) {
5503                 // Either we hit the bottom, or we are trying to move up
5504                 first--;
5505                 seed = first;
5506 
5507                 // Try going down next time
5508                 next = true;
5509             }
5510         }
5511 
5512         return INVALID_POSITION;
5513     }
5514 
5515     @TargetApi(16)
obtainView(int position, boolean[] isScrap)5516     private View obtainView(int position, boolean[] isScrap) {
5517         isScrap[0] = false;
5518 
5519         View scrapView = mRecycler.getTransientStateView(position);
5520         if (scrapView != null) {
5521             return scrapView;
5522         }
5523 
5524         scrapView = mRecycler.getScrapView(position);
5525 
5526         final View child;
5527         if (scrapView != null) {
5528             child = mAdapter.getView(position, scrapView, this);
5529 
5530             if (child != scrapView) {
5531                 mRecycler.addScrapView(scrapView, position);
5532             } else {
5533                 isScrap[0] = true;
5534             }
5535         } else {
5536             child = mAdapter.getView(position, null, this);
5537         }
5538 
5539         if (ViewCompat.getImportantForAccessibility(child) == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
5540             ViewCompat.setImportantForAccessibility(child, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
5541         }
5542 
5543         if (mHasStableIds) {
5544             LayoutParams lp = (LayoutParams) child.getLayoutParams();
5545 
5546             if (lp == null) {
5547                 lp = generateDefaultLayoutParams();
5548             } else if (!checkLayoutParams(lp)) {
5549                 lp = generateLayoutParams(lp);
5550             }
5551 
5552             lp.id = mAdapter.getItemId(position);
5553 
5554             child.setLayoutParams(lp);
5555         }
5556 
5557         if (mAccessibilityDelegate == null) {
5558             mAccessibilityDelegate = new ListItemAccessibilityDelegate();
5559         }
5560 
5561         ViewCompat.setAccessibilityDelegate(child, mAccessibilityDelegate);
5562 
5563         return child;
5564     }
5565 
resetState()5566     void resetState() {
5567         mScroller.forceFinished(true);
5568 
5569         removeAllViewsInLayout();
5570 
5571         mSelectedStart = 0;
5572         mFirstPosition = 0;
5573         mDataChanged = false;
5574         mNeedSync = false;
5575         mPendingSync = null;
5576         mOldSelectedPosition = INVALID_POSITION;
5577         mOldSelectedRowId = INVALID_ROW_ID;
5578 
5579         mOverScroll = 0;
5580 
5581         setSelectedPositionInt(INVALID_POSITION);
5582         setNextSelectedPositionInt(INVALID_POSITION);
5583 
5584         mSelectorPosition = INVALID_POSITION;
5585         mSelectorRect.setEmpty();
5586 
5587         invalidate();
5588     }
5589 
rememberSyncState()5590     private void rememberSyncState() {
5591         if (getChildCount() == 0) {
5592             return;
5593         }
5594 
5595         mNeedSync = true;
5596 
5597         if (mSelectedPosition >= 0) {
5598             View child = getChildAt(mSelectedPosition - mFirstPosition);
5599 
5600             mSyncRowId = mNextSelectedRowId;
5601             mSyncPosition = mNextSelectedPosition;
5602 
5603             if (child != null) {
5604                 mSpecificStart = getChildStartEdge(child);
5605             }
5606 
5607             mSyncMode = SYNC_SELECTED_POSITION;
5608         } else {
5609             // Sync the based on the offset of the first view
5610             View child = getChildAt(0);
5611             ListAdapter adapter = getAdapter();
5612 
5613             if (mFirstPosition >= 0 && mFirstPosition < adapter.getCount()) {
5614                 mSyncRowId = adapter.getItemId(mFirstPosition);
5615             } else {
5616                 mSyncRowId = NO_ID;
5617             }
5618 
5619             mSyncPosition = mFirstPosition;
5620 
5621             if (child != null) {
5622                 mSpecificStart = getChildStartEdge(child);
5623             }
5624 
5625             mSyncMode = SYNC_FIRST_POSITION;
5626         }
5627     }
5628 
createContextMenuInfo(View view, int position, long id)5629     private ContextMenuInfo createContextMenuInfo(View view, int position, long id) {
5630         return new AdapterContextMenuInfo(view, position, id);
5631     }
5632 
5633     @TargetApi(11)
updateOnScreenCheckedViews()5634     private void updateOnScreenCheckedViews() {
5635         final int firstPos = mFirstPosition;
5636         final int count = getChildCount();
5637 
5638         for (int i = 0; i < count; i++) {
5639             final View child = getChildAt(i);
5640             final int position = firstPos + i;
5641 
5642             if (child instanceof Checkable) {
5643                 ((Checkable) child).setChecked(mCheckStates.get(position));
5644             } else if (Build.VERSION.SDK_INT >= HONEYCOMB) {
5645                 child.setActivated(mCheckStates.get(position));
5646             }
5647         }
5648     }
5649 
5650     @Override
performItemClick(View view, int position, long id)5651     public boolean performItemClick(View view, int position, long id) {
5652         boolean checkedStateChanged = false;
5653 
5654         if (mChoiceMode == ChoiceMode.MULTIPLE) {
5655             boolean checked = !mCheckStates.get(position, false);
5656             mCheckStates.put(position, checked);
5657 
5658             if (mCheckedIdStates != null && mAdapter.hasStableIds()) {
5659                 if (checked) {
5660                     mCheckedIdStates.put(mAdapter.getItemId(position), position);
5661                 } else {
5662                     mCheckedIdStates.delete(mAdapter.getItemId(position));
5663                 }
5664             }
5665 
5666             if (checked) {
5667                 mCheckedItemCount++;
5668             } else {
5669                 mCheckedItemCount--;
5670             }
5671 
5672             checkedStateChanged = true;
5673         } else if (mChoiceMode == ChoiceMode.SINGLE) {
5674             boolean checked = !mCheckStates.get(position, false);
5675             if (checked) {
5676                 mCheckStates.clear();
5677                 mCheckStates.put(position, true);
5678 
5679                 if (mCheckedIdStates != null && mAdapter.hasStableIds()) {
5680                     mCheckedIdStates.clear();
5681                     mCheckedIdStates.put(mAdapter.getItemId(position), position);
5682                 }
5683 
5684                 mCheckedItemCount = 1;
5685             } else if (mCheckStates.size() == 0 || !mCheckStates.valueAt(0)) {
5686                 mCheckedItemCount = 0;
5687             }
5688 
5689             checkedStateChanged = true;
5690         }
5691 
5692         if (checkedStateChanged) {
5693             updateOnScreenCheckedViews();
5694         }
5695 
5696         return super.performItemClick(view, position, id);
5697     }
5698 
performLongPress(final View child, final int longPressPosition, final long longPressId)5699     private boolean performLongPress(final View child,
5700             final int longPressPosition, final long longPressId) {
5701         // CHOICE_MODE_MULTIPLE_MODAL takes over long press.
5702         boolean handled = false;
5703 
5704         OnItemLongClickListener listener = getOnItemLongClickListener();
5705         if (listener != null) {
5706             handled = listener.onItemLongClick(TwoWayView.this, child,
5707                     longPressPosition, longPressId);
5708         }
5709 
5710         if (!handled) {
5711             mContextMenuInfo = createContextMenuInfo(child, longPressPosition, longPressId);
5712             handled = super.showContextMenuForChild(TwoWayView.this);
5713         }
5714 
5715         if (handled) {
5716             performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
5717         }
5718 
5719         return handled;
5720     }
5721 
5722     @Override
generateDefaultLayoutParams()5723     protected LayoutParams generateDefaultLayoutParams() {
5724         if (mIsVertical) {
5725             return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
5726         } else {
5727             return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
5728         }
5729     }
5730 
5731     @Override
generateLayoutParams(ViewGroup.LayoutParams lp)5732     protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
5733         return new LayoutParams(lp);
5734     }
5735 
5736     @Override
checkLayoutParams(ViewGroup.LayoutParams lp)5737     protected boolean checkLayoutParams(ViewGroup.LayoutParams lp) {
5738         return lp instanceof LayoutParams;
5739     }
5740 
5741     @Override
generateLayoutParams(AttributeSet attrs)5742     public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
5743         return new LayoutParams(getContext(), attrs);
5744     }
5745 
5746     @Override
getContextMenuInfo()5747     protected ContextMenuInfo getContextMenuInfo() {
5748         return mContextMenuInfo;
5749     }
5750 
5751     @Override
onSaveInstanceState()5752     public Parcelable onSaveInstanceState() {
5753         Parcelable superState = super.onSaveInstanceState();
5754         SavedState ss = new SavedState(superState);
5755 
5756         if (mPendingSync != null) {
5757             ss.selectedId = mPendingSync.selectedId;
5758             ss.firstId = mPendingSync.firstId;
5759             ss.viewStart = mPendingSync.viewStart;
5760             ss.position = mPendingSync.position;
5761             ss.size = mPendingSync.size;
5762 
5763             return ss;
5764         }
5765 
5766         boolean haveChildren = (getChildCount() > 0 && mItemCount > 0);
5767         long selectedId = getSelectedItemId();
5768         ss.selectedId = selectedId;
5769         ss.size = getSize();
5770 
5771         if (selectedId >= 0) {
5772             ss.viewStart = mSelectedStart;
5773             ss.position = getSelectedItemPosition();
5774             ss.firstId = INVALID_POSITION;
5775         } else if (haveChildren && mFirstPosition > 0) {
5776             // Remember the position of the first child.
5777             // We only do this if we are not currently at the top of
5778             // the list, for two reasons:
5779             //
5780             // (1) The list may be in the process of becoming empty, in
5781             // which case mItemCount may not be 0, but if we try to
5782             // ask for any information about position 0 we will crash.
5783             //
5784             // (2) Being "at the top" seems like a special case, anyway,
5785             // and the user wouldn't expect to end up somewhere else when
5786             // they revisit the list even if its content has changed.
5787 
5788             ss.viewStart = getChildStartEdge(getChildAt(0));
5789 
5790             int firstPos = mFirstPosition;
5791             if (firstPos >= mItemCount) {
5792                 firstPos = mItemCount - 1;
5793             }
5794 
5795             ss.position = firstPos;
5796             ss.firstId = mAdapter.getItemId(firstPos);
5797         } else {
5798             ss.viewStart = 0;
5799             ss.firstId = INVALID_POSITION;
5800             ss.position = 0;
5801         }
5802 
5803         if (mCheckStates != null) {
5804             ss.checkState = cloneCheckStates();
5805         }
5806 
5807         if (mCheckedIdStates != null) {
5808             final LongSparseArray<Integer> idState = new LongSparseArray<Integer>();
5809 
5810             final int count = mCheckedIdStates.size();
5811             for (int i = 0; i < count; i++) {
5812                 idState.put(mCheckedIdStates.keyAt(i), mCheckedIdStates.valueAt(i));
5813             }
5814 
5815             ss.checkIdState = idState;
5816         }
5817 
5818         ss.checkedItemCount = mCheckedItemCount;
5819 
5820         return ss;
5821     }
5822 
5823     @Override
onRestoreInstanceState(Parcelable state)5824     public void onRestoreInstanceState(Parcelable state) {
5825         SavedState ss = (SavedState) state;
5826         super.onRestoreInstanceState(ss.getSuperState());
5827 
5828         mDataChanged = true;
5829         mSyncSize = ss.size;
5830 
5831         if (ss.selectedId >= 0) {
5832             mNeedSync = true;
5833             mPendingSync = ss;
5834             mSyncRowId = ss.selectedId;
5835             mSyncPosition = ss.position;
5836             mSpecificStart = ss.viewStart;
5837             mSyncMode = SYNC_SELECTED_POSITION;
5838         } else if (ss.firstId >= 0) {
5839             setSelectedPositionInt(INVALID_POSITION);
5840 
5841             // Do this before setting mNeedSync since setNextSelectedPosition looks at mNeedSync
5842             setNextSelectedPositionInt(INVALID_POSITION);
5843 
5844             mSelectorPosition = INVALID_POSITION;
5845             mNeedSync = true;
5846             mPendingSync = ss;
5847             mSyncRowId = ss.firstId;
5848             mSyncPosition = ss.position;
5849             mSpecificStart = ss.viewStart;
5850             mSyncMode = SYNC_FIRST_POSITION;
5851         }
5852 
5853         if (ss.checkState != null) {
5854             mCheckStates = ss.checkState;
5855         }
5856 
5857         if (ss.checkIdState != null) {
5858             mCheckedIdStates = ss.checkIdState;
5859         }
5860 
5861         mCheckedItemCount = ss.checkedItemCount;
5862 
5863         requestLayout();
5864     }
5865 
5866     public static class LayoutParams extends ViewGroup.LayoutParams {
5867         /**
5868          * Type of this view as reported by the adapter
5869          */
5870         int viewType;
5871 
5872         /**
5873          * The stable ID of the item this view displays
5874          */
5875         long id = -1;
5876 
5877         /**
5878          * The position the view was removed from when pulled out of the
5879          * scrap heap.
5880          * @hide
5881          */
5882         int scrappedFromPosition;
5883 
5884         /**
5885          * When a TwoWayView is measured with an AT_MOST measure spec, it needs
5886          * to obtain children views to measure itself. When doing so, the children
5887          * are not attached to the window, but put in the recycler which assumes
5888          * they've been attached before. Setting this flag will force the reused
5889          * view to be attached to the window rather than just attached to the
5890          * parent.
5891          */
5892         boolean forceAdd;
5893 
LayoutParams(int width, int height)5894         public LayoutParams(int width, int height) {
5895             super(width, height);
5896 
5897             if (this.width == MATCH_PARENT) {
5898                 Log.w(LOGTAG, "Constructing LayoutParams with width FILL_PARENT " +
5899                         "does not make much sense as the view might change orientation. " +
5900                         "Falling back to WRAP_CONTENT");
5901                 this.width = WRAP_CONTENT;
5902             }
5903 
5904             if (this.height == MATCH_PARENT) {
5905                 Log.w(LOGTAG, "Constructing LayoutParams with height FILL_PARENT " +
5906                         "does not make much sense as the view might change orientation. " +
5907                         "Falling back to WRAP_CONTENT");
5908                 this.height = WRAP_CONTENT;
5909             }
5910         }
5911 
LayoutParams(Context c, AttributeSet attrs)5912         public LayoutParams(Context c, AttributeSet attrs) {
5913             super(c, attrs);
5914 
5915             if (this.width == MATCH_PARENT) {
5916                 Log.w(LOGTAG, "Inflation setting LayoutParams width to MATCH_PARENT - " +
5917                         "does not make much sense as the view might change orientation. " +
5918                         "Falling back to WRAP_CONTENT");
5919                 this.width = MATCH_PARENT;
5920             }
5921 
5922             if (this.height == MATCH_PARENT) {
5923                 Log.w(LOGTAG, "Inflation setting LayoutParams height to MATCH_PARENT - " +
5924                         "does not make much sense as the view might change orientation. " +
5925                         "Falling back to WRAP_CONTENT");
5926                 this.height = WRAP_CONTENT;
5927             }
5928         }
5929 
LayoutParams(ViewGroup.LayoutParams other)5930         public LayoutParams(ViewGroup.LayoutParams other) {
5931             super(other);
5932 
5933             if (this.width == MATCH_PARENT) {
5934                 Log.w(LOGTAG, "Constructing LayoutParams with width MATCH_PARENT - " +
5935                         "does not make much sense as the view might change orientation. " +
5936                         "Falling back to WRAP_CONTENT");
5937                 this.width = WRAP_CONTENT;
5938             }
5939 
5940             if (this.height == MATCH_PARENT) {
5941                 Log.w(LOGTAG, "Constructing LayoutParams with height MATCH_PARENT - " +
5942                         "does not make much sense as the view might change orientation. " +
5943                         "Falling back to WRAP_CONTENT");
5944                 this.height = WRAP_CONTENT;
5945             }
5946         }
5947     }
5948 
5949     class RecycleBin {
5950         private RecyclerListener mRecyclerListener;
5951         private int mFirstActivePosition;
5952         private View[] mActiveViews = new View[0];
5953         private ArrayList<View>[] mScrapViews;
5954         private int mViewTypeCount;
5955         private ArrayList<View> mCurrentScrap;
5956         private SparseArrayCompat<View> mTransientStateViews;
5957 
setViewTypeCount(int viewTypeCount)5958         public void setViewTypeCount(int viewTypeCount) {
5959             if (viewTypeCount < 1) {
5960                 throw new IllegalArgumentException("Can't have a viewTypeCount < 1");
5961             }
5962 
5963             @SuppressWarnings({"unchecked", "rawtypes"})
5964             ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount];
5965             for (int i = 0; i < viewTypeCount; i++) {
5966                 scrapViews[i] = new ArrayList<View>();
5967             }
5968 
5969             mViewTypeCount = viewTypeCount;
5970             mCurrentScrap = scrapViews[0];
5971             mScrapViews = scrapViews;
5972         }
5973 
markChildrenDirty()5974         public void markChildrenDirty() {
5975             if (mViewTypeCount == 1) {
5976                 final ArrayList<View> scrap = mCurrentScrap;
5977                 final int scrapCount = scrap.size();
5978 
5979                 for (int i = 0; i < scrapCount; i++) {
5980                     scrap.get(i).forceLayout();
5981                 }
5982             } else {
5983                 final int typeCount = mViewTypeCount;
5984                 for (int i = 0; i < typeCount; i++) {
5985                     for (View scrap : mScrapViews[i]) {
5986                         scrap.forceLayout();
5987                     }
5988                 }
5989             }
5990 
5991             if (mTransientStateViews != null) {
5992                 final int count = mTransientStateViews.size();
5993                 for (int i = 0; i < count; i++) {
5994                     mTransientStateViews.valueAt(i).forceLayout();
5995                 }
5996             }
5997         }
5998 
shouldRecycleViewType(int viewType)5999         public boolean shouldRecycleViewType(int viewType) {
6000             return viewType >= 0;
6001         }
6002 
clear()6003         void clear() {
6004             if (mViewTypeCount == 1) {
6005                 final ArrayList<View> scrap = mCurrentScrap;
6006                 final int scrapCount = scrap.size();
6007 
6008                 for (int i = 0; i < scrapCount; i++) {
6009                     removeDetachedView(scrap.remove(scrapCount - 1 - i), false);
6010                 }
6011             } else {
6012                 final int typeCount = mViewTypeCount;
6013                 for (int i = 0; i < typeCount; i++) {
6014                     final ArrayList<View> scrap = mScrapViews[i];
6015                     final int scrapCount = scrap.size();
6016 
6017                     for (int j = 0; j < scrapCount; j++) {
6018                         removeDetachedView(scrap.remove(scrapCount - 1 - j), false);
6019                     }
6020                 }
6021             }
6022 
6023             if (mTransientStateViews != null) {
6024                 mTransientStateViews.clear();
6025             }
6026         }
6027 
fillActiveViews(int childCount, int firstActivePosition)6028         void fillActiveViews(int childCount, int firstActivePosition) {
6029             if (mActiveViews.length < childCount) {
6030                 mActiveViews = new View[childCount];
6031             }
6032 
6033             mFirstActivePosition = firstActivePosition;
6034 
6035             final View[] activeViews = mActiveViews;
6036             for (int i = 0; i < childCount; i++) {
6037                 View child = getChildAt(i);
6038 
6039                 // Note:  We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in active views.
6040                 //        However, we will NOT place them into scrap views.
6041                 activeViews[i] = child;
6042             }
6043         }
6044 
getActiveView(int position)6045         View getActiveView(int position) {
6046             final int index = position - mFirstActivePosition;
6047             final View[] activeViews = mActiveViews;
6048 
6049             if (index >= 0 && index < activeViews.length) {
6050                 final View match = activeViews[index];
6051                 activeViews[index] = null;
6052 
6053                 return match;
6054             }
6055 
6056             return null;
6057         }
6058 
getTransientStateView(int position)6059         View getTransientStateView(int position) {
6060             if (mTransientStateViews == null) {
6061                 return null;
6062             }
6063 
6064             final int index = mTransientStateViews.indexOfKey(position);
6065             if (index < 0) {
6066                 return null;
6067             }
6068 
6069             final View result = mTransientStateViews.valueAt(index);
6070             mTransientStateViews.removeAt(index);
6071 
6072             return result;
6073         }
6074 
clearTransientStateViews()6075         void clearTransientStateViews() {
6076             if (mTransientStateViews != null) {
6077                 mTransientStateViews.clear();
6078             }
6079         }
6080 
getScrapView(int position)6081         View getScrapView(int position) {
6082             if (mViewTypeCount == 1) {
6083                 return retrieveFromScrap(mCurrentScrap, position);
6084             } else {
6085                 int whichScrap = mAdapter.getItemViewType(position);
6086                 if (whichScrap >= 0 && whichScrap < mScrapViews.length) {
6087                     return retrieveFromScrap(mScrapViews[whichScrap], position);
6088                 }
6089             }
6090 
6091             return null;
6092         }
6093 
6094         @TargetApi(14)
addScrapView(View scrap, int position)6095         void addScrapView(View scrap, int position) {
6096             LayoutParams lp = (LayoutParams) scrap.getLayoutParams();
6097             if (lp == null) {
6098                 return;
6099             }
6100 
6101             lp.scrappedFromPosition = position;
6102 
6103             final int viewType = lp.viewType;
6104             final boolean scrapHasTransientState = ViewCompat.hasTransientState(scrap);
6105 
6106             // Don't put views that should be ignored into the scrap heap
6107             if (!shouldRecycleViewType(viewType) || scrapHasTransientState) {
6108                 if (scrapHasTransientState) {
6109                     if (mTransientStateViews == null) {
6110                         mTransientStateViews = new SparseArrayCompat<View>();
6111                     }
6112 
6113                     mTransientStateViews.put(position, scrap);
6114                 }
6115 
6116                 return;
6117             }
6118 
6119             if (mViewTypeCount == 1) {
6120                 mCurrentScrap.add(scrap);
6121             } else {
6122                 mScrapViews[viewType].add(scrap);
6123             }
6124 
6125             // FIXME: Unfortunately, ViewCompat.setAccessibilityDelegate() doesn't accept
6126             // null delegates.
6127             if (Build.VERSION.SDK_INT >= 14) {
6128                 scrap.setAccessibilityDelegate(null);
6129             }
6130 
6131             if (mRecyclerListener != null) {
6132                 mRecyclerListener.onMovedToScrapHeap(scrap);
6133             }
6134         }
6135 
6136         @TargetApi(14)
scrapActiveViews()6137         void scrapActiveViews() {
6138             final View[] activeViews = mActiveViews;
6139             final boolean multipleScraps = (mViewTypeCount > 1);
6140 
6141             ArrayList<View> scrapViews = mCurrentScrap;
6142             final int count = activeViews.length;
6143 
6144             for (int i = count - 1; i >= 0; i--) {
6145                 final View victim = activeViews[i];
6146                 if (victim != null) {
6147                     final LayoutParams lp = (LayoutParams) victim.getLayoutParams();
6148                     int whichScrap = lp.viewType;
6149 
6150                     activeViews[i] = null;
6151 
6152                     final boolean scrapHasTransientState = ViewCompat.hasTransientState(victim);
6153                     if (!shouldRecycleViewType(whichScrap) || scrapHasTransientState) {
6154                         if (scrapHasTransientState) {
6155                             removeDetachedView(victim, false);
6156 
6157                             if (mTransientStateViews == null) {
6158                                 mTransientStateViews = new SparseArrayCompat<View>();
6159                             }
6160 
6161                             mTransientStateViews.put(mFirstActivePosition + i, victim);
6162                         }
6163 
6164                         continue;
6165                     }
6166 
6167                     if (multipleScraps) {
6168                         scrapViews = mScrapViews[whichScrap];
6169                     }
6170 
6171                     lp.scrappedFromPosition = mFirstActivePosition + i;
6172                     scrapViews.add(victim);
6173 
6174                     // FIXME: Unfortunately, ViewCompat.setAccessibilityDelegate() doesn't accept
6175                     // null delegates.
6176                     if (Build.VERSION.SDK_INT >= 14) {
6177                         victim.setAccessibilityDelegate(null);
6178                     }
6179 
6180                     if (mRecyclerListener != null) {
6181                         mRecyclerListener.onMovedToScrapHeap(victim);
6182                     }
6183                 }
6184             }
6185 
6186             pruneScrapViews();
6187         }
6188 
pruneScrapViews()6189         private void pruneScrapViews() {
6190             final int maxViews = mActiveViews.length;
6191             final int viewTypeCount = mViewTypeCount;
6192             final ArrayList<View>[] scrapViews = mScrapViews;
6193 
6194             for (int i = 0; i < viewTypeCount; ++i) {
6195                 final ArrayList<View> scrapPile = scrapViews[i];
6196                 int size = scrapPile.size();
6197                 final int extras = size - maxViews;
6198 
6199                 size--;
6200 
6201                 for (int j = 0; j < extras; j++) {
6202                     removeDetachedView(scrapPile.remove(size--), false);
6203                 }
6204             }
6205 
6206             if (mTransientStateViews != null) {
6207                 for (int i = 0; i < mTransientStateViews.size(); i++) {
6208                     final View v = mTransientStateViews.valueAt(i);
6209                     if (!ViewCompat.hasTransientState(v)) {
6210                         mTransientStateViews.removeAt(i);
6211                         i--;
6212                     }
6213                 }
6214             }
6215         }
6216 
reclaimScrapViews(List<View> views)6217         void reclaimScrapViews(List<View> views) {
6218             if (mViewTypeCount == 1) {
6219                 views.addAll(mCurrentScrap);
6220             } else {
6221                 final int viewTypeCount = mViewTypeCount;
6222                 final ArrayList<View>[] scrapViews = mScrapViews;
6223 
6224                 for (int i = 0; i < viewTypeCount; ++i) {
6225                     final ArrayList<View> scrapPile = scrapViews[i];
6226                     views.addAll(scrapPile);
6227                 }
6228             }
6229         }
6230 
retrieveFromScrap(ArrayList<View> scrapViews, int position)6231         View retrieveFromScrap(ArrayList<View> scrapViews, int position) {
6232             int size = scrapViews.size();
6233             if (size <= 0) {
6234                 return null;
6235             }
6236 
6237             for (int i = 0; i < size; i++) {
6238                 final View scrapView = scrapViews.get(i);
6239                 final LayoutParams lp = (LayoutParams) scrapView.getLayoutParams();
6240 
6241                 if (lp.scrappedFromPosition == position) {
6242                     scrapViews.remove(i);
6243                     return scrapView;
6244                 }
6245             }
6246 
6247             return scrapViews.remove(size - 1);
6248         }
6249     }
6250 
6251     @Override
setEmptyView(View emptyView)6252     public void setEmptyView(View emptyView) {
6253         super.setEmptyView(emptyView);
6254         mEmptyView = emptyView;
6255         updateEmptyStatus();
6256     }
6257 
6258     @Override
setFocusable(boolean focusable)6259     public void setFocusable(boolean focusable) {
6260         final ListAdapter adapter = getAdapter();
6261         final boolean empty = (adapter == null || adapter.getCount() == 0);
6262 
6263         mDesiredFocusableState = focusable;
6264         if (!focusable) {
6265             mDesiredFocusableInTouchModeState = false;
6266         }
6267 
6268         super.setFocusable(focusable && !empty);
6269     }
6270 
6271     @Override
setFocusableInTouchMode(boolean focusable)6272     public void setFocusableInTouchMode(boolean focusable) {
6273         final ListAdapter adapter = getAdapter();
6274         final boolean empty = (adapter == null || adapter.getCount() == 0);
6275 
6276         mDesiredFocusableInTouchModeState = focusable;
6277         if (focusable) {
6278             mDesiredFocusableState = true;
6279         }
6280 
6281         super.setFocusableInTouchMode(focusable && !empty);
6282     }
6283 
checkFocus()6284     private void checkFocus() {
6285         final ListAdapter adapter = getAdapter();
6286         final boolean focusable = (adapter != null && adapter.getCount() > 0);
6287 
6288         // The order in which we set focusable in touch mode/focusable may matter
6289         // for the client, see View.setFocusableInTouchMode() comments for more
6290         // details
6291         super.setFocusableInTouchMode(focusable && mDesiredFocusableInTouchModeState);
6292         super.setFocusable(focusable && mDesiredFocusableState);
6293 
6294         if (mEmptyView != null) {
6295             updateEmptyStatus();
6296         }
6297     }
6298 
updateEmptyStatus()6299     private void updateEmptyStatus() {
6300         final boolean isEmpty = (mAdapter == null || mAdapter.isEmpty());
6301 
6302         if (isEmpty) {
6303             if (mEmptyView != null) {
6304                 mEmptyView.setVisibility(View.VISIBLE);
6305                 setVisibility(View.GONE);
6306             } else {
6307                 // If the caller just removed our empty view, make sure the list
6308                 // view is visible
6309                 setVisibility(View.VISIBLE);
6310             }
6311 
6312             // We are now GONE, so pending layouts will not be dispatched.
6313             // Force one here to make sure that the state of the list matches
6314             // the state of the adapter.
6315             if (mDataChanged) {
6316                 layout(getLeft(), getTop(), getRight(), getBottom());
6317             }
6318         } else {
6319             if (mEmptyView != null) {
6320                 mEmptyView.setVisibility(View.GONE);
6321             }
6322 
6323             setVisibility(View.VISIBLE);
6324         }
6325     }
6326 
6327     private class AdapterDataSetObserver extends DataSetObserver {
6328         private Parcelable mInstanceState = null;
6329 
6330         @Override
onChanged()6331         public void onChanged() {
6332             mDataChanged = true;
6333             mOldItemCount = mItemCount;
6334             mItemCount = getAdapter().getCount();
6335 
6336             // Detect the case where a cursor that was previously invalidated has
6337             // been re-populated with new data.
6338             if (TwoWayView.this.mHasStableIds && mInstanceState != null
6339                     && mOldItemCount == 0 && mItemCount > 0) {
6340                 TwoWayView.this.onRestoreInstanceState(mInstanceState);
6341                 mInstanceState = null;
6342             } else {
6343                 rememberSyncState();
6344             }
6345 
6346             checkFocus();
6347             requestLayout();
6348         }
6349 
6350         @Override
onInvalidated()6351         public void onInvalidated() {
6352             mDataChanged = true;
6353 
6354             if (TwoWayView.this.mHasStableIds) {
6355                 // Remember the current state for the case where our hosting activity is being
6356                 // stopped and later restarted
6357                 mInstanceState = TwoWayView.this.onSaveInstanceState();
6358             }
6359 
6360             // Data is invalid so we should reset our state
6361             mOldItemCount = mItemCount;
6362             mItemCount = 0;
6363 
6364             mSelectedPosition = INVALID_POSITION;
6365             mSelectedRowId = INVALID_ROW_ID;
6366 
6367             mNextSelectedPosition = INVALID_POSITION;
6368             mNextSelectedRowId = INVALID_ROW_ID;
6369 
6370             mNeedSync = false;
6371 
6372             checkFocus();
6373             requestLayout();
6374         }
6375     }
6376 
6377     static class SavedState extends BaseSavedState {
6378         long selectedId;
6379         long firstId;
6380         int viewStart;
6381         int position;
6382         int size;
6383         int checkedItemCount;
6384         SparseBooleanArray checkState;
6385         LongSparseArray<Integer> checkIdState;
6386 
6387         /**
6388          * Constructor called from {@link TwoWayView#onSaveInstanceState()}
6389          */
SavedState(Parcelable superState)6390         SavedState(Parcelable superState) {
6391             super(superState);
6392         }
6393 
6394         /**
6395          * Constructor called from {@link #CREATOR}
6396          */
SavedState(Parcel in)6397         private SavedState(Parcel in) {
6398             super(in);
6399 
6400             selectedId = in.readLong();
6401             firstId = in.readLong();
6402             viewStart = in.readInt();
6403             position = in.readInt();
6404             size = in.readInt();
6405 
6406             checkedItemCount = in.readInt();
6407             checkState = in.readSparseBooleanArray();
6408 
6409             final int N = in.readInt();
6410             if (N > 0) {
6411                 checkIdState = new LongSparseArray<Integer>();
6412                 for (int i = 0; i < N; i++) {
6413                     final long key = in.readLong();
6414                     final int value = in.readInt();
6415                     checkIdState.put(key, value);
6416                 }
6417             }
6418         }
6419 
6420         @Override
writeToParcel(Parcel out, int flags)6421         public void writeToParcel(Parcel out, int flags) {
6422             super.writeToParcel(out, flags);
6423 
6424             out.writeLong(selectedId);
6425             out.writeLong(firstId);
6426             out.writeInt(viewStart);
6427             out.writeInt(position);
6428             out.writeInt(size);
6429 
6430             out.writeInt(checkedItemCount);
6431             out.writeSparseBooleanArray(checkState);
6432 
6433             final int N = checkIdState != null ? checkIdState.size() : 0;
6434             out.writeInt(N);
6435 
6436             for (int i = 0; i < N; i++) {
6437                 out.writeLong(checkIdState.keyAt(i));
6438                 out.writeInt(checkIdState.valueAt(i));
6439             }
6440         }
6441 
6442         @Override
toString()6443         public String toString() {
6444             return "TwoWayView.SavedState{"
6445                     + Integer.toHexString(System.identityHashCode(this))
6446                     + " selectedId=" + selectedId
6447                     + " firstId=" + firstId
6448                     + " viewStart=" + viewStart
6449                     + " size=" + size
6450                     + " position=" + position
6451                     + " checkState=" + checkState + "}";
6452         }
6453 
6454         public static final Parcelable.Creator<SavedState> CREATOR
6455                 = new Parcelable.Creator<SavedState>() {
6456             @Override
6457             public SavedState createFromParcel(Parcel in) {
6458                 return new SavedState(in);
6459             }
6460 
6461             @Override
6462             public SavedState[] newArray(int size) {
6463                 return new SavedState[size];
6464             }
6465         };
6466     }
6467 
6468     private class SelectionNotifier implements Runnable {
6469         @Override
run()6470         public void run() {
6471             if (mDataChanged) {
6472                 // Data has changed between when this SelectionNotifier
6473                 // was posted and now. We need to wait until the AdapterView
6474                 // has been synched to the new data.
6475                 if (mAdapter != null) {
6476                     post(this);
6477                 }
6478             } else {
6479                 fireOnSelected();
6480                 performAccessibilityActionsOnSelected();
6481             }
6482         }
6483     }
6484 
6485     private class WindowRunnable {
6486         private int mOriginalAttachCount;
6487 
rememberWindowAttachCount()6488         public void rememberWindowAttachCount() {
6489             mOriginalAttachCount = getWindowAttachCount();
6490         }
6491 
sameWindow()6492         public boolean sameWindow() {
6493             return hasWindowFocus() && getWindowAttachCount() == mOriginalAttachCount;
6494         }
6495     }
6496 
6497     private class PerformClick extends WindowRunnable implements Runnable {
6498         int mClickMotionPosition;
6499 
6500         @Override
run()6501         public void run() {
6502             if (mDataChanged) {
6503                 return;
6504             }
6505 
6506             final ListAdapter adapter = mAdapter;
6507             final int motionPosition = mClickMotionPosition;
6508 
6509             if (adapter != null && mItemCount > 0 &&
6510                 motionPosition != INVALID_POSITION &&
6511                 motionPosition < adapter.getCount() && sameWindow()) {
6512 
6513                 final View child = getChildAt(motionPosition - mFirstPosition);
6514                 if (child != null) {
6515                     performItemClick(child, motionPosition, adapter.getItemId(motionPosition));
6516                 }
6517             }
6518         }
6519     }
6520 
6521     private final class CheckForTap implements Runnable {
6522         @Override
run()6523         public void run() {
6524             if (mTouchMode != TOUCH_MODE_DOWN) {
6525                 return;
6526             }
6527 
6528             mTouchMode = TOUCH_MODE_TAP;
6529 
6530             final View child = getChildAt(mMotionPosition - mFirstPosition);
6531             if (child != null && !child.hasFocusable()) {
6532                 mLayoutMode = LAYOUT_NORMAL;
6533 
6534                 if (!mDataChanged) {
6535                     setPressed(true);
6536                     child.setPressed(true);
6537 
6538                     layoutChildren();
6539                     positionSelector(mMotionPosition, child);
6540                     refreshDrawableState();
6541 
6542                     positionSelector(mMotionPosition, child);
6543                     refreshDrawableState();
6544 
6545                     final boolean longClickable = isLongClickable();
6546 
6547                     if (mSelector != null) {
6548                         Drawable d = mSelector.getCurrent();
6549 
6550                         if (d != null && d instanceof TransitionDrawable) {
6551                             if (longClickable) {
6552                                 final int longPressTimeout = ViewConfiguration.getLongPressTimeout();
6553                                 ((TransitionDrawable) d).startTransition(longPressTimeout);
6554                             } else {
6555                                 ((TransitionDrawable) d).resetTransition();
6556                             }
6557                         }
6558                     }
6559 
6560                     if (longClickable) {
6561                         triggerCheckForLongPress();
6562                     } else {
6563                         mTouchMode = TOUCH_MODE_DONE_WAITING;
6564                     }
6565                 } else {
6566                     mTouchMode = TOUCH_MODE_DONE_WAITING;
6567                 }
6568             }
6569         }
6570     }
6571 
6572     private class CheckForLongPress extends WindowRunnable implements Runnable {
6573         @Override
run()6574         public void run() {
6575             final int motionPosition = mMotionPosition;
6576             final View child = getChildAt(motionPosition - mFirstPosition);
6577 
6578             if (child != null) {
6579                 final long longPressId = mAdapter.getItemId(mMotionPosition);
6580 
6581                 boolean handled = false;
6582                 if (sameWindow() && !mDataChanged) {
6583                     handled = performLongPress(child, motionPosition, longPressId);
6584                 }
6585 
6586                 if (handled) {
6587                     mTouchMode = TOUCH_MODE_REST;
6588                     setPressed(false);
6589                     child.setPressed(false);
6590                 } else {
6591                     mTouchMode = TOUCH_MODE_DONE_WAITING;
6592                 }
6593             }
6594         }
6595     }
6596 
6597     private class CheckForKeyLongPress extends WindowRunnable implements Runnable {
run()6598         public void run() {
6599             if (!isPressed() || mSelectedPosition < 0) {
6600                 return;
6601             }
6602 
6603             final int index = mSelectedPosition - mFirstPosition;
6604             final View v = getChildAt(index);
6605 
6606             if (!mDataChanged) {
6607                 boolean handled = false;
6608 
6609                 if (sameWindow()) {
6610                     handled = performLongPress(v, mSelectedPosition, mSelectedRowId);
6611                 }
6612 
6613                 if (handled) {
6614                     setPressed(false);
6615                     v.setPressed(false);
6616                 }
6617             } else {
6618                 setPressed(false);
6619 
6620                 if (v != null) {
6621                     v.setPressed(false);
6622                 }
6623             }
6624         }
6625     }
6626 
6627     private static class ArrowScrollFocusResult {
6628         private int mSelectedPosition;
6629         private int mAmountToScroll;
6630 
6631         /**
6632          * How {@link TwoWayView#arrowScrollFocused} returns its values.
6633          */
populate(int selectedPosition, int amountToScroll)6634         void populate(int selectedPosition, int amountToScroll) {
6635             mSelectedPosition = selectedPosition;
6636             mAmountToScroll = amountToScroll;
6637         }
6638 
getSelectedPosition()6639         public int getSelectedPosition() {
6640             return mSelectedPosition;
6641         }
6642 
getAmountToScroll()6643         public int getAmountToScroll() {
6644             return mAmountToScroll;
6645         }
6646     }
6647 
6648     private class ListItemAccessibilityDelegate extends AccessibilityDelegateCompat {
6649         @Override
onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info)6650         public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
6651             super.onInitializeAccessibilityNodeInfo(host, info);
6652 
6653             final int position = getPositionForView(host);
6654             final ListAdapter adapter = getAdapter();
6655 
6656             // Cannot perform actions on invalid items
6657             if (position == INVALID_POSITION || adapter == null) {
6658                 return;
6659             }
6660 
6661             // Cannot perform actions on disabled items
6662             if (!isEnabled() || !adapter.isEnabled(position)) {
6663                 return;
6664             }
6665 
6666             if (position == getSelectedItemPosition()) {
6667                 info.setSelected(true);
6668                 info.addAction(AccessibilityNodeInfoCompat.ACTION_CLEAR_SELECTION);
6669             } else {
6670                 info.addAction(AccessibilityNodeInfoCompat.ACTION_SELECT);
6671             }
6672 
6673             if (isClickable()) {
6674                 info.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
6675                 info.setClickable(true);
6676             }
6677 
6678             if (isLongClickable()) {
6679                 info.addAction(AccessibilityNodeInfoCompat.ACTION_LONG_CLICK);
6680                 info.setLongClickable(true);
6681             }
6682         }
6683 
6684         @Override
performAccessibilityAction(View host, int action, Bundle arguments)6685         public boolean performAccessibilityAction(View host, int action, Bundle arguments) {
6686             if (super.performAccessibilityAction(host, action, arguments)) {
6687                 return true;
6688             }
6689 
6690             final int position = getPositionForView(host);
6691             final ListAdapter adapter = getAdapter();
6692 
6693             // Cannot perform actions on invalid items
6694             if (position == INVALID_POSITION || adapter == null) {
6695                 return false;
6696             }
6697 
6698             // Cannot perform actions on disabled items
6699             if (!isEnabled() || !adapter.isEnabled(position)) {
6700                 return false;
6701             }
6702 
6703             final long id = getItemIdAtPosition(position);
6704 
6705             switch (action) {
6706             case AccessibilityNodeInfoCompat.ACTION_CLEAR_SELECTION:
6707                 if (getSelectedItemPosition() == position) {
6708                     setSelection(INVALID_POSITION);
6709                     return true;
6710                 }
6711                 return false;
6712 
6713             case AccessibilityNodeInfoCompat.ACTION_SELECT:
6714                 if (getSelectedItemPosition() != position) {
6715                     setSelection(position);
6716                     return true;
6717                 }
6718                 return false;
6719 
6720             case AccessibilityNodeInfoCompat.ACTION_CLICK:
6721                 return isClickable() && performItemClick(host, position, id);
6722 
6723             case AccessibilityNodeInfoCompat.ACTION_LONG_CLICK:
6724                 return isLongClickable() && performLongPress(host, position, id);
6725             }
6726 
6727             return false;
6728         }
6729     }
6730 
6731     private class PositionScroller implements Runnable {
6732         private static final int SCROLL_DURATION = 200;
6733 
6734         private static final int MOVE_AFTER_POS = 1;
6735         private static final int MOVE_BEFORE_POS = 2;
6736         private static final int MOVE_AFTER_BOUND = 3;
6737         private static final int MOVE_BEFORE_BOUND = 4;
6738         private static final int MOVE_OFFSET = 5;
6739 
6740         private int mMode;
6741         private int mTargetPosition;
6742         private int mBoundPosition;
6743         private int mLastSeenPosition;
6744         private int mScrollDuration;
6745         private final int mExtraScroll;
6746 
6747         private int mOffsetFromStart;
6748 
PositionScroller()6749         PositionScroller() {
6750             mExtraScroll = ViewConfiguration.get(mContext).getScaledFadingEdgeLength();
6751         }
6752 
start(final int position)6753         void start(final int position) {
6754             stop();
6755 
6756             if (mDataChanged) {
6757                 // Wait until we're back in a stable state to try this.
6758                 mPositionScrollAfterLayout = new Runnable() {
6759                     @Override public void run() {
6760                         start(position);
6761                     }
6762                 };
6763 
6764                 return;
6765             }
6766 
6767             final int childCount = getChildCount();
6768             if (childCount == 0) {
6769                 // Can't scroll without children.
6770                 return;
6771             }
6772 
6773             final int firstPosition = mFirstPosition;
6774             final int lastPosition = firstPosition + childCount - 1;
6775 
6776             final int clampedPosition = Math.max(0, Math.min(getCount() - 1, position));
6777 
6778             final int viewTravelCount;
6779             if (clampedPosition < firstPosition) {
6780                 viewTravelCount = firstPosition - clampedPosition + 1;
6781                 mMode = MOVE_BEFORE_POS;
6782             } else if (clampedPosition > lastPosition) {
6783                 viewTravelCount = clampedPosition - lastPosition + 1;
6784                 mMode = MOVE_AFTER_POS;
6785             } else {
6786                 scrollToVisible(clampedPosition, INVALID_POSITION, SCROLL_DURATION);
6787                 return;
6788             }
6789 
6790             if (viewTravelCount > 0) {
6791                 mScrollDuration = SCROLL_DURATION / viewTravelCount;
6792             } else {
6793                 mScrollDuration = SCROLL_DURATION;
6794             }
6795 
6796             mTargetPosition = clampedPosition;
6797             mBoundPosition = INVALID_POSITION;
6798             mLastSeenPosition = INVALID_POSITION;
6799 
6800             ViewCompat.postOnAnimation(TwoWayView.this, this);
6801         }
6802 
start(final int position, final int boundPosition)6803         void start(final int position, final int boundPosition) {
6804             stop();
6805 
6806             if (boundPosition == INVALID_POSITION) {
6807                 start(position);
6808                 return;
6809             }
6810 
6811             if (mDataChanged) {
6812                 // Wait until we're back in a stable state to try this.
6813                 mPositionScrollAfterLayout = new Runnable() {
6814                     @Override public void run() {
6815                         start(position, boundPosition);
6816                     }
6817                 };
6818 
6819                 return;
6820             }
6821 
6822             final int childCount = getChildCount();
6823             if (childCount == 0) {
6824                 // Can't scroll without children.
6825                 return;
6826             }
6827 
6828             final int firstPosition = mFirstPosition;
6829             final int lastPosition = firstPosition + childCount - 1;
6830 
6831             final int clampedPosition = Math.max(0, Math.min(getCount() - 1, position));
6832 
6833             final int viewTravelCount;
6834             if (clampedPosition < firstPosition) {
6835                 final int boundPositionFromLast = lastPosition - boundPosition;
6836                 if (boundPositionFromLast < 1) {
6837                     // Moving would shift our bound position off the screen. Abort.
6838                     return;
6839                 }
6840 
6841                 final int positionTravel = firstPosition - clampedPosition + 1;
6842                 final int boundTravel = boundPositionFromLast - 1;
6843                 if (boundTravel < positionTravel) {
6844                     viewTravelCount = boundTravel;
6845                     mMode = MOVE_BEFORE_BOUND;
6846                 } else {
6847                     viewTravelCount = positionTravel;
6848                     mMode = MOVE_BEFORE_POS;
6849                 }
6850             } else if (clampedPosition > lastPosition) {
6851                 final int boundPositionFromFirst = boundPosition - firstPosition;
6852                 if (boundPositionFromFirst < 1) {
6853                     // Moving would shift our bound position off the screen. Abort.
6854                     return;
6855                 }
6856 
6857                 final int positionTravel = clampedPosition - lastPosition + 1;
6858                 final int boundTravel = boundPositionFromFirst - 1;
6859                 if (boundTravel < positionTravel) {
6860                     viewTravelCount = boundTravel;
6861                     mMode = MOVE_AFTER_BOUND;
6862                 } else {
6863                     viewTravelCount = positionTravel;
6864                     mMode = MOVE_AFTER_POS;
6865                 }
6866             } else {
6867                 scrollToVisible(clampedPosition, boundPosition, SCROLL_DURATION);
6868                 return;
6869             }
6870 
6871             if (viewTravelCount > 0) {
6872                 mScrollDuration = SCROLL_DURATION / viewTravelCount;
6873             } else {
6874                 mScrollDuration = SCROLL_DURATION;
6875             }
6876 
6877             mTargetPosition = clampedPosition;
6878             mBoundPosition = boundPosition;
6879             mLastSeenPosition = INVALID_POSITION;
6880 
6881             ViewCompat.postOnAnimation(TwoWayView.this, this);
6882         }
6883 
startWithOffset(int position, int offset)6884         void startWithOffset(int position, int offset) {
6885             startWithOffset(position, offset, SCROLL_DURATION);
6886         }
6887 
startWithOffset(final int position, int offset, final int duration)6888         void startWithOffset(final int position, int offset, final int duration) {
6889             stop();
6890 
6891             if (mDataChanged) {
6892                 // Wait until we're back in a stable state to try this.
6893                 final int postOffset = offset;
6894                 mPositionScrollAfterLayout = new Runnable() {
6895                     @Override public void run() {
6896                         startWithOffset(position, postOffset, duration);
6897                     }
6898                 };
6899 
6900                 return;
6901             }
6902 
6903             final int childCount = getChildCount();
6904             if (childCount == 0) {
6905                 // Can't scroll without children.
6906                 return;
6907             }
6908 
6909             offset += getStartEdge();
6910 
6911             mTargetPosition = Math.max(0, Math.min(getCount() - 1, position));
6912             mOffsetFromStart = offset;
6913             mBoundPosition = INVALID_POSITION;
6914             mLastSeenPosition = INVALID_POSITION;
6915             mMode = MOVE_OFFSET;
6916 
6917             final int firstPosition = mFirstPosition;
6918             final int lastPosition = firstPosition + childCount - 1;
6919 
6920             final int viewTravelCount;
6921             if (mTargetPosition < firstPosition) {
6922                 viewTravelCount = firstPosition - mTargetPosition;
6923             } else if (mTargetPosition > lastPosition) {
6924                 viewTravelCount = mTargetPosition - lastPosition;
6925             } else {
6926                 // On-screen, just scroll.
6927                 final View targetView = getChildAt(mTargetPosition - firstPosition);
6928                 final int targetStart = getChildStartEdge(targetView);
6929                 smoothScrollBy(targetStart - offset, duration);
6930                 return;
6931             }
6932 
6933             // Estimate how many screens we should travel
6934             final float screenTravelCount = (float) viewTravelCount / childCount;
6935             mScrollDuration = screenTravelCount < 1 ?
6936                     duration : (int) (duration / screenTravelCount);
6937             mLastSeenPosition = INVALID_POSITION;
6938 
6939             ViewCompat.postOnAnimation(TwoWayView.this, this);
6940         }
6941 
6942         /**
6943          * Scroll such that targetPos is in the visible padded region without scrolling
6944          * boundPos out of view. Assumes targetPos is onscreen.
6945          */
6946         void scrollToVisible(int targetPosition, int boundPosition, int duration) {
6947             final int childCount = getChildCount();
6948             final int firstPosition = mFirstPosition;
6949             final int lastPosition = firstPosition + childCount - 1;
6950 
6951             final int start = getStartEdge();
6952             final int end = getEndEdge();
6953 
6954             if (targetPosition < firstPosition || targetPosition > lastPosition) {
6955                 Log.w(LOGTAG, "scrollToVisible called with targetPosition " + targetPosition +
6956                         " not visible [" + firstPosition + ", " + lastPosition + "]");
6957             }
6958 
6959             if (boundPosition < firstPosition || boundPosition > lastPosition) {
6960                 // boundPos doesn't matter, it's already offscreen.
6961                 boundPosition = INVALID_POSITION;
6962             }
6963 
6964             final View targetChild = getChildAt(targetPosition - firstPosition);
6965             final int targetStart = getChildStartEdge(targetChild);
6966             final int targetEnd = getChildEndEdge(targetChild);
6967 
6968             int scrollBy = 0;
6969             if (targetEnd > end) {
6970                 scrollBy = targetEnd - end;
6971             }
6972             if (targetStart < start) {
6973                 scrollBy = targetStart - start;
6974             }
6975 
6976             if (scrollBy == 0) {
6977                 return;
6978             }
6979 
6980             if (boundPosition >= 0) {
6981                 final View boundChild = getChildAt(boundPosition - firstPosition);
6982                 final int boundStart = getChildStartEdge(boundChild);
6983                 final int boundEnd = getChildEndEdge(boundChild);
6984                 final int absScroll = Math.abs(scrollBy);
6985 
6986                 if (scrollBy < 0 && boundEnd + absScroll > end) {
6987                     // Don't scroll the bound view off the end of the screen.
6988                     scrollBy = Math.max(0, boundEnd - end);
6989                 } else if (scrollBy > 0 && boundStart - absScroll < start) {
6990                     // Don't scroll the bound view off the top of the screen.
6991                     scrollBy = Math.min(0, boundStart - start);
6992                 }
6993             }
6994 
6995             smoothScrollBy(scrollBy, duration);
6996         }
6997 
stop()6998         void stop() {
6999             removeCallbacks(this);
7000         }
7001 
7002         @Override
run()7003         public void run() {
7004             final int size = getAvailableSize();
7005             final int firstPosition = mFirstPosition;
7006 
7007             final int startPadding = (mIsVertical ? getPaddingTop() : getPaddingLeft());
7008             final int endPadding = (mIsVertical ? getPaddingBottom() : getPaddingRight());
7009 
7010             switch (mMode) {
7011                 case MOVE_AFTER_POS: {
7012                     final int lastViewIndex = getChildCount() - 1;
7013                     if (lastViewIndex < 0) {
7014                         return;
7015                     }
7016 
7017                     final int lastPosition = firstPosition + lastViewIndex;
7018                     if (lastPosition == mLastSeenPosition) {
7019                         // No new views, let things keep going.
7020                         ViewCompat.postOnAnimation(TwoWayView.this, this);
7021                         return;
7022                     }
7023 
7024                     final View lastView = getChildAt(lastViewIndex);
7025                     final int lastViewSize = getChildSize(lastView);
7026                     final int lastViewStart = getChildStartEdge(lastView);
7027                     final int lastViewPixelsShowing = size - lastViewStart;
7028                     final int extraScroll = lastPosition < mItemCount - 1 ?
7029                             Math.max(endPadding, mExtraScroll) : endPadding;
7030 
7031                     final int scrollBy = lastViewSize - lastViewPixelsShowing + extraScroll;
7032                     smoothScrollBy(scrollBy, mScrollDuration);
7033 
7034                     mLastSeenPosition = lastPosition;
7035                     if (lastPosition < mTargetPosition) {
7036                         ViewCompat.postOnAnimation(TwoWayView.this, this);
7037                     }
7038 
7039                     break;
7040                 }
7041 
7042                 case MOVE_AFTER_BOUND: {
7043                     final int nextViewIndex = 1;
7044                     final int childCount = getChildCount();
7045                     if (firstPosition == mBoundPosition ||
7046                         childCount <= nextViewIndex ||
7047                         firstPosition + childCount >= mItemCount) {
7048                         return;
7049                     }
7050 
7051                     final int nextPosition = firstPosition + nextViewIndex;
7052 
7053                     if (nextPosition == mLastSeenPosition) {
7054                         // No new views, let things keep going.
7055                         ViewCompat.postOnAnimation(TwoWayView.this, this);
7056                         return;
7057                     }
7058 
7059                     final View nextView = getChildAt(nextViewIndex);
7060                     final int nextViewSize = getChildSize(nextView);
7061                     final int nextViewStart = getChildStartEdge(nextView);
7062                     final int extraScroll = Math.max(endPadding, mExtraScroll);
7063                     if (nextPosition < mBoundPosition) {
7064                         smoothScrollBy(Math.max(0, nextViewSize + nextViewStart - extraScroll),
7065                                 mScrollDuration);
7066                         mLastSeenPosition = nextPosition;
7067                         ViewCompat.postOnAnimation(TwoWayView.this, this);
7068                     } else  {
7069                         if (nextViewSize > extraScroll) {
7070                             smoothScrollBy(nextViewSize - extraScroll, mScrollDuration);
7071                         }
7072                     }
7073 
7074                     break;
7075                 }
7076 
7077                 case MOVE_BEFORE_POS: {
7078                     if (firstPosition == mLastSeenPosition) {
7079                         // No new views, let things keep going.
7080                         ViewCompat.postOnAnimation(TwoWayView.this, this);
7081                         return;
7082                     }
7083 
7084                     final View firstView = getChildAt(0);
7085                     if (firstView == null) {
7086                         return;
7087                     }
7088 
7089                     final int firstViewTop = getChildStartEdge(firstView);
7090                     final int extraScroll = firstPosition > 0 ?
7091                             Math.max(mExtraScroll, startPadding) : startPadding;
7092 
7093                     smoothScrollBy(firstViewTop - extraScroll, mScrollDuration);
7094                     mLastSeenPosition = firstPosition;
7095 
7096                     if (firstPosition > mTargetPosition) {
7097                         ViewCompat.postOnAnimation(TwoWayView.this, this);
7098                     }
7099 
7100                     break;
7101                 }
7102 
7103                 case MOVE_BEFORE_BOUND: {
7104                     final int lastViewIndex = getChildCount() - 2;
7105                     if (lastViewIndex < 0) {
7106                         return;
7107                     }
7108 
7109                     final int lastPosition = firstPosition + lastViewIndex;
7110 
7111                     if (lastPosition == mLastSeenPosition) {
7112                         // No new views, let things keep going.
7113                         ViewCompat.postOnAnimation(TwoWayView.this, this);
7114                         return;
7115                     }
7116 
7117                     final View lastView = getChildAt(lastViewIndex);
7118                     final int lastViewSize = getChildSize(lastView);
7119                     final int lastViewStart = getChildStartEdge(lastView);
7120                     final int lastViewPixelsShowing = size - lastViewStart;
7121                     final int extraScroll = Math.max(startPadding, mExtraScroll);
7122 
7123                     mLastSeenPosition = lastPosition;
7124 
7125                     if (lastPosition > mBoundPosition) {
7126                         smoothScrollBy(-(lastViewPixelsShowing - extraScroll), mScrollDuration);
7127                         ViewCompat.postOnAnimation(TwoWayView.this, this);
7128                     } else {
7129                         final int end = size - extraScroll;
7130                         final int lastViewEnd = lastViewStart + lastViewSize;
7131                         if (end > lastViewEnd) {
7132                             smoothScrollBy(-(end - lastViewEnd), mScrollDuration);
7133                         }
7134                     }
7135 
7136                     break;
7137                 }
7138 
7139                 case MOVE_OFFSET: {
7140                     if (mLastSeenPosition == firstPosition) {
7141                         // No new views, let things keep going.
7142                         ViewCompat.postOnAnimation(TwoWayView.this, this);
7143                         return;
7144                     }
7145 
7146                     mLastSeenPosition = firstPosition;
7147 
7148                     final int childCount = getChildCount();
7149                     final int position = mTargetPosition;
7150                     final int lastPos = firstPosition + childCount - 1;
7151 
7152                     int viewTravelCount = 0;
7153                     if (position < firstPosition) {
7154                         viewTravelCount = firstPosition - position + 1;
7155                     } else if (position > lastPos) {
7156                         viewTravelCount = position - lastPos;
7157                     }
7158 
7159                     // Estimate how many screens we should travel
7160                     final float screenTravelCount = (float) viewTravelCount / childCount;
7161 
7162                     final float modifier = Math.min(Math.abs(screenTravelCount), 1.f);
7163                     if (position < firstPosition) {
7164                         final int distance = (int) (-getSize() * modifier);
7165                         final int duration = (int) (mScrollDuration * modifier);
7166                         smoothScrollBy(distance, duration);
7167                         ViewCompat.postOnAnimation(TwoWayView.this, this);
7168                     } else if (position > lastPos) {
7169                         final int distance = (int) (getSize() * modifier);
7170                         final int duration = (int) (mScrollDuration * modifier);
7171                         smoothScrollBy(distance, duration);
7172                         ViewCompat.postOnAnimation(TwoWayView.this, this);
7173                     } else {
7174                         // On-screen, just scroll.
7175                         final View targetView = getChildAt(position - firstPosition);
7176                         final int targetStart = getChildStartEdge(targetView);
7177                         final int distance = targetStart - mOffsetFromStart;
7178                         final int duration = (int) (mScrollDuration *
7179                                 ((float) Math.abs(distance) / getSize()));
7180                         smoothScrollBy(distance, duration);
7181                     }
7182 
7183                     break;
7184                 }
7185 
7186                 default:
7187                     break;
7188             }
7189         }
7190     }
7191 }
7192