Wed, 31 Dec 2014 07:22:50 +0100
Correct previous dual key logic pending first delivery installment.
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 */
22 package org.mozilla.gecko.widget;
24 import org.mozilla.gecko.R;
26 import android.annotation.TargetApi;
27 import android.content.Context;
28 import android.content.res.TypedArray;
29 import android.database.DataSetObserver;
30 import android.graphics.Canvas;
31 import android.graphics.Rect;
32 import android.graphics.drawable.Drawable;
33 import android.graphics.drawable.TransitionDrawable;
34 import android.os.Build;
35 import android.os.Bundle;
36 import android.os.Parcel;
37 import android.os.Parcelable;
38 import android.os.SystemClock;
39 import android.support.v4.util.LongSparseArray;
40 import android.support.v4.util.SparseArrayCompat;
41 import android.support.v4.view.AccessibilityDelegateCompat;
42 import android.support.v4.view.KeyEventCompat;
43 import android.support.v4.view.MotionEventCompat;
44 import android.support.v4.view.VelocityTrackerCompat;
45 import android.support.v4.view.ViewCompat;
46 import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
47 import android.support.v4.widget.EdgeEffectCompat;
48 import android.util.AttributeSet;
49 import android.util.Log;
50 import android.util.SparseBooleanArray;
51 import android.view.ContextMenu.ContextMenuInfo;
52 import android.view.FocusFinder;
53 import android.view.HapticFeedbackConstants;
54 import android.view.KeyEvent;
55 import android.view.MotionEvent;
56 import android.view.SoundEffectConstants;
57 import android.view.VelocityTracker;
58 import android.view.View;
59 import android.view.ViewConfiguration;
60 import android.view.ViewGroup;
61 import android.view.ViewParent;
62 import android.view.ViewTreeObserver;
63 import android.view.accessibility.AccessibilityEvent;
64 import android.view.accessibility.AccessibilityNodeInfo;
65 import android.widget.AdapterView;
66 import android.widget.Checkable;
67 import android.widget.ListAdapter;
68 import android.widget.Scroller;
70 import java.util.ArrayList;
71 import java.util.List;
73 /*
74 * Implementation Notes:
75 *
76 * Some terminology:
77 *
78 * index - index of the items that are currently visible
79 * position - index of the items in the cursor
80 *
81 * Given the bi-directional nature of this view, the source code
82 * usually names variables with 'start' to mean 'top' or 'left'; and
83 * 'end' to mean 'bottom' or 'right', depending on the current
84 * orientation of the widget.
85 */
87 /**
88 * A view that shows items in a vertical or horizontal scrolling list.
89 * The items come from the {@link ListAdapter} associated with this view.
90 */
91 public class TwoWayView extends AdapterView<ListAdapter> implements
92 ViewTreeObserver.OnTouchModeChangeListener {
93 private static final String LOGTAG = "TwoWayView";
95 private static final int NO_POSITION = -1;
96 private static final int INVALID_POINTER = -1;
98 public static final int[] STATE_NOTHING = new int[] { 0 };
100 private static final int TOUCH_MODE_REST = -1;
101 private static final int TOUCH_MODE_DOWN = 0;
102 private static final int TOUCH_MODE_TAP = 1;
103 private static final int TOUCH_MODE_DONE_WAITING = 2;
104 private static final int TOUCH_MODE_DRAGGING = 3;
105 private static final int TOUCH_MODE_FLINGING = 4;
106 private static final int TOUCH_MODE_OVERSCROLL = 5;
108 private static final int TOUCH_MODE_UNKNOWN = -1;
109 private static final int TOUCH_MODE_ON = 0;
110 private static final int TOUCH_MODE_OFF = 1;
112 private static final int LAYOUT_NORMAL = 0;
113 private static final int LAYOUT_FORCE_TOP = 1;
114 private static final int LAYOUT_SET_SELECTION = 2;
115 private static final int LAYOUT_FORCE_BOTTOM = 3;
116 private static final int LAYOUT_SPECIFIC = 4;
117 private static final int LAYOUT_SYNC = 5;
118 private static final int LAYOUT_MOVE_SELECTION = 6;
120 private static final int SYNC_SELECTED_POSITION = 0;
121 private static final int SYNC_FIRST_POSITION = 1;
123 private static final int SYNC_MAX_DURATION_MILLIS = 100;
125 private static final int CHECK_POSITION_SEARCH_DISTANCE = 20;
127 private static final float MAX_SCROLL_FACTOR = 0.33f;
129 private static final int MIN_SCROLL_PREVIEW_PIXELS = 10;
131 public static enum ChoiceMode {
132 NONE,
133 SINGLE,
134 MULTIPLE
135 }
137 public static enum Orientation {
138 HORIZONTAL,
139 VERTICAL;
140 };
142 private ListAdapter mAdapter;
144 private boolean mIsVertical;
146 private int mItemMargin;
148 private boolean mInLayout;
149 private boolean mBlockLayoutRequests;
151 private boolean mIsAttached;
153 private final RecycleBin mRecycler;
154 private AdapterDataSetObserver mDataSetObserver;
156 private boolean mItemsCanFocus;
158 final boolean[] mIsScrap = new boolean[1];
160 private boolean mDataChanged;
161 private int mItemCount;
162 private int mOldItemCount;
163 private boolean mHasStableIds;
164 private boolean mAreAllItemsSelectable;
166 private int mFirstPosition;
167 private int mSpecificStart;
169 private SavedState mPendingSync;
171 private final int mTouchSlop;
172 private final int mMaximumVelocity;
173 private final int mFlingVelocity;
174 private float mLastTouchPos;
175 private float mTouchRemainderPos;
176 private int mActivePointerId;
178 private final Rect mTempRect;
180 private final ArrowScrollFocusResult mArrowScrollFocusResult;
182 private Rect mTouchFrame;
183 private int mMotionPosition;
184 private CheckForTap mPendingCheckForTap;
185 private CheckForLongPress mPendingCheckForLongPress;
186 private CheckForKeyLongPress mPendingCheckForKeyLongPress;
187 private PerformClick mPerformClick;
188 private Runnable mTouchModeReset;
189 private int mResurrectToPosition;
191 private boolean mIsChildViewEnabled;
193 private boolean mDrawSelectorOnTop;
194 private Drawable mSelector;
195 private int mSelectorPosition;
196 private final Rect mSelectorRect;
198 private int mOverScroll;
199 private final int mOverscrollDistance;
201 private boolean mDesiredFocusableState;
202 private boolean mDesiredFocusableInTouchModeState;
204 private SelectionNotifier mSelectionNotifier;
206 private boolean mNeedSync;
207 private int mSyncMode;
208 private int mSyncPosition;
209 private long mSyncRowId;
210 private long mSyncHeight;
211 private int mSelectedStart;
213 private int mNextSelectedPosition;
214 private long mNextSelectedRowId;
215 private int mSelectedPosition;
216 private long mSelectedRowId;
217 private int mOldSelectedPosition;
218 private long mOldSelectedRowId;
220 private ChoiceMode mChoiceMode;
221 private int mCheckedItemCount;
222 private SparseBooleanArray mCheckStates;
223 LongSparseArray<Integer> mCheckedIdStates;
225 private ContextMenuInfo mContextMenuInfo;
227 private int mLayoutMode;
228 private int mTouchMode;
229 private int mLastTouchMode;
230 private VelocityTracker mVelocityTracker;
231 private final Scroller mScroller;
233 private EdgeEffectCompat mStartEdge;
234 private EdgeEffectCompat mEndEdge;
236 private OnScrollListener mOnScrollListener;
237 private int mLastScrollState;
239 private View mEmptyView;
241 private ListItemAccessibilityDelegate mAccessibilityDelegate;
243 private int mLastAccessibilityScrollEventFromIndex;
244 private int mLastAccessibilityScrollEventToIndex;
246 public interface OnScrollListener {
248 /**
249 * The view is not scrolling. Note navigating the list using the trackball counts as
250 * being in the idle state since these transitions are not animated.
251 */
252 public static int SCROLL_STATE_IDLE = 0;
254 /**
255 * The user is scrolling using touch, and their finger is still on the screen
256 */
257 public static int SCROLL_STATE_TOUCH_SCROLL = 1;
259 /**
260 * The user had previously been scrolling using touch and had performed a fling. The
261 * animation is now coasting to a stop
262 */
263 public static int SCROLL_STATE_FLING = 2;
265 /**
266 * Callback method to be invoked while the list view or grid view is being scrolled. If the
267 * view is being scrolled, this method will be called before the next frame of the scroll is
268 * rendered. In particular, it will be called before any calls to
269 * {@link Adapter#getView(int, View, ViewGroup)}.
270 *
271 * @param view The view whose scroll state is being reported
272 *
273 * @param scrollState The current scroll state. One of {@link #SCROLL_STATE_IDLE},
274 * {@link #SCROLL_STATE_TOUCH_SCROLL} or {@link #SCROLL_STATE_IDLE}.
275 */
276 public void onScrollStateChanged(TwoWayView view, int scrollState);
278 /**
279 * Callback method to be invoked when the list or grid has been scrolled. This will be
280 * called after the scroll has completed
281 * @param view The view whose scroll state is being reported
282 * @param firstVisibleItem the index of the first visible cell (ignore if
283 * visibleItemCount == 0)
284 * @param visibleItemCount the number of visible cells
285 * @param totalItemCount the number of items in the list adaptor
286 */
287 public void onScroll(TwoWayView view, int firstVisibleItem, int visibleItemCount,
288 int totalItemCount);
289 }
291 /**
292 * A RecyclerListener is used to receive a notification whenever a View is placed
293 * inside the RecycleBin's scrap heap. This listener is used to free resources
294 * associated to Views placed in the RecycleBin.
295 *
296 * @see TwoWayView.RecycleBin
297 * @see TwoWayView#setRecyclerListener(TwoWayView.RecyclerListener)
298 */
299 public static interface RecyclerListener {
300 /**
301 * Indicates that the specified View was moved into the recycler's scrap heap.
302 * The view is not displayed on screen any more and any expensive resource
303 * associated with the view should be discarded.
304 *
305 * @param view
306 */
307 void onMovedToScrapHeap(View view);
308 }
310 public TwoWayView(Context context) {
311 this(context, null);
312 }
314 public TwoWayView(Context context, AttributeSet attrs) {
315 this(context, attrs, 0);
316 }
318 public TwoWayView(Context context, AttributeSet attrs, int defStyle) {
319 super(context, attrs, defStyle);
321 mNeedSync = false;
322 mVelocityTracker = null;
324 mLayoutMode = LAYOUT_NORMAL;
325 mTouchMode = TOUCH_MODE_REST;
326 mLastTouchMode = TOUCH_MODE_UNKNOWN;
328 mIsAttached = false;
330 mContextMenuInfo = null;
332 mOnScrollListener = null;
333 mLastScrollState = OnScrollListener.SCROLL_STATE_IDLE;
335 final ViewConfiguration vc = ViewConfiguration.get(context);
336 mTouchSlop = vc.getScaledTouchSlop();
337 mMaximumVelocity = vc.getScaledMaximumFlingVelocity();
338 mFlingVelocity = vc.getScaledMinimumFlingVelocity();
339 mOverscrollDistance = getScaledOverscrollDistance(vc);
341 mOverScroll = 0;
343 mScroller = new Scroller(context);
345 mIsVertical = true;
347 mItemsCanFocus = false;
349 mTempRect = new Rect();
351 mArrowScrollFocusResult = new ArrowScrollFocusResult();
353 mSelectorPosition = INVALID_POSITION;
355 mSelectorRect = new Rect();
356 mSelectedStart = 0;
358 mResurrectToPosition = INVALID_POSITION;
360 mSelectedStart = 0;
361 mNextSelectedPosition = INVALID_POSITION;
362 mNextSelectedRowId = INVALID_ROW_ID;
363 mSelectedPosition = INVALID_POSITION;
364 mSelectedRowId = INVALID_ROW_ID;
365 mOldSelectedPosition = INVALID_POSITION;
366 mOldSelectedRowId = INVALID_ROW_ID;
368 mChoiceMode = ChoiceMode.NONE;
369 mCheckedItemCount = 0;
370 mCheckedIdStates = null;
371 mCheckStates = null;
373 mRecycler = new RecycleBin();
374 mDataSetObserver = null;
376 mAreAllItemsSelectable = true;
378 mStartEdge = null;
379 mEndEdge = null;
381 setClickable(true);
382 setFocusableInTouchMode(true);
383 setWillNotDraw(false);
384 setAlwaysDrawnWithCacheEnabled(false);
385 setWillNotDraw(false);
386 setClipToPadding(false);
388 ViewCompat.setOverScrollMode(this, ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS);
390 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TwoWayView, defStyle, 0);
391 initializeScrollbars(a);
393 mDrawSelectorOnTop = a.getBoolean(
394 R.styleable.TwoWayView_android_drawSelectorOnTop, false);
396 Drawable d = a.getDrawable(R.styleable.TwoWayView_android_listSelector);
397 if (d != null) {
398 setSelector(d);
399 }
401 int orientation = a.getInt(R.styleable.TwoWayView_android_orientation, -1);
402 if (orientation >= 0) {
403 setOrientation(Orientation.values()[orientation]);
404 }
406 int choiceMode = a.getInt(R.styleable.TwoWayView_android_choiceMode, -1);
407 if (choiceMode >= 0) {
408 setChoiceMode(ChoiceMode.values()[choiceMode]);
409 }
411 a.recycle();
413 updateScrollbarsDirection();
414 }
416 public void setOrientation(Orientation orientation) {
417 final boolean isVertical = (orientation.compareTo(Orientation.VERTICAL) == 0);
418 if (mIsVertical == isVertical) {
419 return;
420 }
422 mIsVertical = isVertical;
424 updateScrollbarsDirection();
425 resetState();
426 mRecycler.clear();
428 requestLayout();
429 }
431 public Orientation getOrientation() {
432 return (mIsVertical ? Orientation.VERTICAL : Orientation.HORIZONTAL);
433 }
435 public void setItemMargin(int itemMargin) {
436 if (mItemMargin == itemMargin) {
437 return;
438 }
440 mItemMargin = itemMargin;
441 requestLayout();
442 }
444 public int getItemMargin() {
445 return mItemMargin;
446 }
448 /**
449 * Indicates that the views created by the ListAdapter can contain focusable
450 * items.
451 *
452 * @param itemsCanFocus true if items can get focus, false otherwise
453 */
454 public void setItemsCanFocus(boolean itemsCanFocus) {
455 mItemsCanFocus = itemsCanFocus;
456 if (!itemsCanFocus) {
457 setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
458 }
459 }
461 /**
462 * @return Whether the views created by the ListAdapter can contain focusable
463 * items.
464 */
465 public boolean getItemsCanFocus() {
466 return mItemsCanFocus;
467 }
469 /**
470 * Set the listener that will receive notifications every time the list scrolls.
471 *
472 * @param l the scroll listener
473 */
474 public void setOnScrollListener(OnScrollListener l) {
475 mOnScrollListener = l;
476 invokeOnItemScrollListener();
477 }
479 /**
480 * Sets the recycler listener to be notified whenever a View is set aside in
481 * the recycler for later reuse. This listener can be used to free resources
482 * associated to the View.
483 *
484 * @param listener The recycler listener to be notified of views set aside
485 * in the recycler.
486 *
487 * @see TwoWayView.RecycleBin
488 * @see TwoWayView.RecyclerListener
489 */
490 public void setRecyclerListener(RecyclerListener l) {
491 mRecycler.mRecyclerListener = l;
492 }
494 /**
495 * Controls whether the selection highlight drawable should be drawn on top of the item or
496 * behind it.
497 *
498 * @param onTop If true, the selector will be drawn on the item it is highlighting. The default
499 * is false.
500 *
501 * @attr ref android.R.styleable#AbsListView_drawSelectorOnTop
502 */
503 public void setDrawSelectorOnTop(boolean drawSelectorOnTop) {
504 mDrawSelectorOnTop = drawSelectorOnTop;
505 }
507 /**
508 * Set a Drawable that should be used to highlight the currently selected item.
509 *
510 * @param resID A Drawable resource to use as the selection highlight.
511 *
512 * @attr ref android.R.styleable#AbsListView_listSelector
513 */
514 public void setSelector(int resID) {
515 setSelector(getResources().getDrawable(resID));
516 }
518 /**
519 * Set a Drawable that should be used to highlight the currently selected item.
520 *
521 * @param selector A Drawable to use as the selection highlight.
522 *
523 * @attr ref android.R.styleable#AbsListView_listSelector
524 */
525 public void setSelector(Drawable selector) {
526 if (mSelector != null) {
527 mSelector.setCallback(null);
528 unscheduleDrawable(mSelector);
529 }
531 mSelector = selector;
532 Rect padding = new Rect();
533 selector.getPadding(padding);
535 selector.setCallback(this);
536 updateSelectorState();
537 }
539 /**
540 * Returns the selector {@link android.graphics.drawable.Drawable} that is used to draw the
541 * selection in the list.
542 *
543 * @return the drawable used to display the selector
544 */
545 public Drawable getSelector() {
546 return mSelector;
547 }
549 /**
550 * {@inheritDoc}
551 */
552 @Override
553 public int getSelectedItemPosition() {
554 return mNextSelectedPosition;
555 }
557 /**
558 * {@inheritDoc}
559 */
560 @Override
561 public long getSelectedItemId() {
562 return mNextSelectedRowId;
563 }
565 /**
566 * Returns the number of items currently selected. This will only be valid
567 * if the choice mode is not {@link #CHOICE_MODE_NONE} (default).
568 *
569 * <p>To determine the specific items that are currently selected, use one of
570 * the <code>getChecked*</code> methods.
571 *
572 * @return The number of items currently selected
573 *
574 * @see #getCheckedItemPosition()
575 * @see #getCheckedItemPositions()
576 * @see #getCheckedItemIds()
577 */
578 public int getCheckedItemCount() {
579 return mCheckedItemCount;
580 }
582 /**
583 * Returns the checked state of the specified position. The result is only
584 * valid if the choice mode has been set to {@link #CHOICE_MODE_SINGLE}
585 * or {@link #CHOICE_MODE_MULTIPLE}.
586 *
587 * @param position The item whose checked state to return
588 * @return The item's checked state or <code>false</code> if choice mode
589 * is invalid
590 *
591 * @see #setChoiceMode(int)
592 */
593 public boolean isItemChecked(int position) {
594 if (mChoiceMode.compareTo(ChoiceMode.NONE) == 0 && mCheckStates != null) {
595 return mCheckStates.get(position);
596 }
598 return false;
599 }
601 /**
602 * Returns the currently checked item. The result is only valid if the choice
603 * mode has been set to {@link #CHOICE_MODE_SINGLE}.
604 *
605 * @return The position of the currently checked item or
606 * {@link #INVALID_POSITION} if nothing is selected
607 *
608 * @see #setChoiceMode(int)
609 */
610 public int getCheckedItemPosition() {
611 if (mChoiceMode.compareTo(ChoiceMode.SINGLE) == 0 &&
612 mCheckStates != null && mCheckStates.size() == 1) {
613 return mCheckStates.keyAt(0);
614 }
616 return INVALID_POSITION;
617 }
619 /**
620 * Returns the set of checked items in the list. The result is only valid if
621 * the choice mode has not been set to {@link #CHOICE_MODE_NONE}.
622 *
623 * @return A SparseBooleanArray which will return true for each call to
624 * get(int position) where position is a position in the list,
625 * or <code>null</code> if the choice mode is set to
626 * {@link #CHOICE_MODE_NONE}.
627 */
628 public SparseBooleanArray getCheckedItemPositions() {
629 if (mChoiceMode.compareTo(ChoiceMode.NONE) != 0) {
630 return mCheckStates;
631 }
633 return null;
634 }
636 /**
637 * Returns the set of checked items ids. The result is only valid if the
638 * choice mode has not been set to {@link #CHOICE_MODE_NONE} and the adapter
639 * has stable IDs. ({@link ListAdapter#hasStableIds()} == {@code true})
640 *
641 * @return A new array which contains the id of each checked item in the
642 * list.
643 */
644 public long[] getCheckedItemIds() {
645 if (mChoiceMode.compareTo(ChoiceMode.NONE) == 0 ||
646 mCheckedIdStates == null || mAdapter == null) {
647 return new long[0];
648 }
650 final LongSparseArray<Integer> idStates = mCheckedIdStates;
651 final int count = idStates.size();
652 final long[] ids = new long[count];
654 for (int i = 0; i < count; i++) {
655 ids[i] = idStates.keyAt(i);
656 }
658 return ids;
659 }
661 /**
662 * Sets the checked state of the specified position. The is only valid if
663 * the choice mode has been set to {@link #CHOICE_MODE_SINGLE} or
664 * {@link #CHOICE_MODE_MULTIPLE}.
665 *
666 * @param position The item whose checked state is to be checked
667 * @param value The new checked state for the item
668 */
669 public void setItemChecked(int position, boolean value) {
670 if (mChoiceMode.compareTo(ChoiceMode.NONE) == 0) {
671 return;
672 }
674 if (mChoiceMode.compareTo(ChoiceMode.MULTIPLE) == 0) {
675 boolean oldValue = mCheckStates.get(position);
676 mCheckStates.put(position, value);
678 if (mCheckedIdStates != null && mAdapter.hasStableIds()) {
679 if (value) {
680 mCheckedIdStates.put(mAdapter.getItemId(position), position);
681 } else {
682 mCheckedIdStates.delete(mAdapter.getItemId(position));
683 }
684 }
686 if (oldValue != value) {
687 if (value) {
688 mCheckedItemCount++;
689 } else {
690 mCheckedItemCount--;
691 }
692 }
693 } else {
694 boolean updateIds = mCheckedIdStates != null && mAdapter.hasStableIds();
696 // Clear all values if we're checking something, or unchecking the currently
697 // selected item
698 if (value || isItemChecked(position)) {
699 mCheckStates.clear();
701 if (updateIds) {
702 mCheckedIdStates.clear();
703 }
704 }
706 // This may end up selecting the value we just cleared but this way
707 // we ensure length of mCheckStates is 1, a fact getCheckedItemPosition relies on
708 if (value) {
709 mCheckStates.put(position, true);
711 if (updateIds) {
712 mCheckedIdStates.put(mAdapter.getItemId(position), position);
713 }
715 mCheckedItemCount = 1;
716 } else if (mCheckStates.size() == 0 || !mCheckStates.valueAt(0)) {
717 mCheckedItemCount = 0;
718 }
719 }
721 // Do not generate a data change while we are in the layout phase
722 if (!mInLayout && !mBlockLayoutRequests) {
723 mDataChanged = true;
724 rememberSyncState();
725 requestLayout();
726 }
727 }
729 /**
730 * Clear any choices previously set
731 */
732 public void clearChoices() {
733 if (mCheckStates != null) {
734 mCheckStates.clear();
735 }
737 if (mCheckedIdStates != null) {
738 mCheckedIdStates.clear();
739 }
741 mCheckedItemCount = 0;
742 }
744 /**
745 * @see #setChoiceMode(int)
746 *
747 * @return The current choice mode
748 */
749 public ChoiceMode getChoiceMode() {
750 return mChoiceMode;
751 }
753 /**
754 * Defines the choice behavior for the List. By default, Lists do not have any choice behavior
755 * ({@link #CHOICE_MODE_NONE}). By setting the choiceMode to {@link #CHOICE_MODE_SINGLE}, the
756 * List allows up to one item to be in a chosen state. By setting the choiceMode to
757 * {@link #CHOICE_MODE_MULTIPLE}, the list allows any number of items to be chosen.
758 *
759 * @param choiceMode One of {@link #CHOICE_MODE_NONE}, {@link #CHOICE_MODE_SINGLE}, or
760 * {@link #CHOICE_MODE_MULTIPLE}
761 */
762 public void setChoiceMode(ChoiceMode choiceMode) {
763 mChoiceMode = choiceMode;
765 if (mChoiceMode.compareTo(ChoiceMode.NONE) != 0) {
766 if (mCheckStates == null) {
767 mCheckStates = new SparseBooleanArray();
768 }
770 if (mCheckedIdStates == null && mAdapter != null && mAdapter.hasStableIds()) {
771 mCheckedIdStates = new LongSparseArray<Integer>();
772 }
773 }
774 }
776 @Override
777 public ListAdapter getAdapter() {
778 return mAdapter;
779 }
781 @Override
782 public void setAdapter(ListAdapter adapter) {
783 if (mAdapter != null && mDataSetObserver != null) {
784 mAdapter.unregisterDataSetObserver(mDataSetObserver);
785 }
787 resetState();
788 mRecycler.clear();
790 mAdapter = adapter;
791 mDataChanged = true;
793 mOldSelectedPosition = INVALID_POSITION;
794 mOldSelectedRowId = INVALID_ROW_ID;
796 if (mCheckStates != null) {
797 mCheckStates.clear();
798 }
800 if (mCheckedIdStates != null) {
801 mCheckedIdStates.clear();
802 }
804 if (mAdapter != null) {
805 mOldItemCount = mItemCount;
806 mItemCount = adapter.getCount();
808 mDataSetObserver = new AdapterDataSetObserver();
809 mAdapter.registerDataSetObserver(mDataSetObserver);
811 mRecycler.setViewTypeCount(adapter.getViewTypeCount());
813 mHasStableIds = adapter.hasStableIds();
814 mAreAllItemsSelectable = adapter.areAllItemsEnabled();
816 if (mChoiceMode.compareTo(ChoiceMode.NONE) != 0 && mHasStableIds &&
817 mCheckedIdStates == null) {
818 mCheckedIdStates = new LongSparseArray<Integer>();
819 }
821 final int position = lookForSelectablePosition(0);
822 setSelectedPositionInt(position);
823 setNextSelectedPositionInt(position);
825 if (mItemCount == 0) {
826 checkSelectionChanged();
827 }
828 } else {
829 mItemCount = 0;
830 mHasStableIds = false;
831 mAreAllItemsSelectable = true;
833 checkSelectionChanged();
834 }
836 checkFocus();
837 requestLayout();
838 }
840 @Override
841 public int getFirstVisiblePosition() {
842 return mFirstPosition;
843 }
845 @Override
846 public int getLastVisiblePosition() {
847 return mFirstPosition + getChildCount() - 1;
848 }
850 @Override
851 public int getCount() {
852 return mItemCount;
853 }
855 @Override
856 public int getPositionForView(View view) {
857 View child = view;
858 try {
859 View v;
860 while (!(v = (View) child.getParent()).equals(this)) {
861 child = v;
862 }
863 } catch (ClassCastException e) {
864 // We made it up to the window without find this list view
865 return INVALID_POSITION;
866 }
868 // Search the children for the list item
869 final int childCount = getChildCount();
870 for (int i = 0; i < childCount; i++) {
871 if (getChildAt(i).equals(child)) {
872 return mFirstPosition + i;
873 }
874 }
876 // Child not found!
877 return INVALID_POSITION;
878 }
880 @Override
881 public void getFocusedRect(Rect r) {
882 View view = getSelectedView();
884 if (view != null && view.getParent() == this) {
885 // The focused rectangle of the selected view offset into the
886 // coordinate space of this view.
887 view.getFocusedRect(r);
888 offsetDescendantRectToMyCoords(view, r);
889 } else {
890 super.getFocusedRect(r);
891 }
892 }
894 @Override
895 protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
896 super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
898 if (gainFocus && mSelectedPosition < 0 && !isInTouchMode()) {
899 if (!mIsAttached && mAdapter != null) {
900 // Data may have changed while we were detached and it's valid
901 // to change focus while detached. Refresh so we don't die.
902 mDataChanged = true;
903 mOldItemCount = mItemCount;
904 mItemCount = mAdapter.getCount();
905 }
907 resurrectSelection();
908 }
910 final ListAdapter adapter = mAdapter;
911 int closetChildIndex = INVALID_POSITION;
912 int closestChildStart = 0;
914 if (adapter != null && gainFocus && previouslyFocusedRect != null) {
915 previouslyFocusedRect.offset(getScrollX(), getScrollY());
917 // Don't cache the result of getChildCount or mFirstPosition here,
918 // it could change in layoutChildren.
919 if (adapter.getCount() < getChildCount() + mFirstPosition) {
920 mLayoutMode = LAYOUT_NORMAL;
921 layoutChildren();
922 }
924 // Figure out which item should be selected based on previously
925 // focused rect.
926 Rect otherRect = mTempRect;
927 int minDistance = Integer.MAX_VALUE;
928 final int childCount = getChildCount();
929 final int firstPosition = mFirstPosition;
931 for (int i = 0; i < childCount; i++) {
932 // Only consider selectable views
933 if (!adapter.isEnabled(firstPosition + i)) {
934 continue;
935 }
937 View other = getChildAt(i);
938 other.getDrawingRect(otherRect);
939 offsetDescendantRectToMyCoords(other, otherRect);
940 int distance = getDistance(previouslyFocusedRect, otherRect, direction);
942 if (distance < minDistance) {
943 minDistance = distance;
944 closetChildIndex = i;
945 closestChildStart = (mIsVertical ? other.getTop() : other.getLeft());
946 }
947 }
948 }
950 if (closetChildIndex >= 0) {
951 setSelectionFromOffset(closetChildIndex + mFirstPosition, closestChildStart);
952 } else {
953 requestLayout();
954 }
955 }
957 @Override
958 protected void onAttachedToWindow() {
959 super.onAttachedToWindow();
961 final ViewTreeObserver treeObserver = getViewTreeObserver();
962 treeObserver.addOnTouchModeChangeListener(this);
964 if (mAdapter != null && mDataSetObserver == null) {
965 mDataSetObserver = new AdapterDataSetObserver();
966 mAdapter.registerDataSetObserver(mDataSetObserver);
968 // Data may have changed while we were detached. Refresh.
969 mDataChanged = true;
970 mOldItemCount = mItemCount;
971 mItemCount = mAdapter.getCount();
972 }
974 mIsAttached = true;
975 }
977 @Override
978 protected void onDetachedFromWindow() {
979 super.onDetachedFromWindow();
981 // Detach any view left in the scrap heap
982 mRecycler.clear();
984 final ViewTreeObserver treeObserver = getViewTreeObserver();
985 treeObserver.removeOnTouchModeChangeListener(this);
987 if (mAdapter != null) {
988 mAdapter.unregisterDataSetObserver(mDataSetObserver);
989 mDataSetObserver = null;
990 }
992 if (mPerformClick != null) {
993 removeCallbacks(mPerformClick);
994 }
996 if (mTouchModeReset != null) {
997 removeCallbacks(mTouchModeReset);
998 mTouchModeReset.run();
999 }
1001 mIsAttached = false;
1002 }
1004 @Override
1005 public void onWindowFocusChanged(boolean hasWindowFocus) {
1006 super.onWindowFocusChanged(hasWindowFocus);
1008 final int touchMode = isInTouchMode() ? TOUCH_MODE_ON : TOUCH_MODE_OFF;
1010 if (!hasWindowFocus) {
1011 if (touchMode == TOUCH_MODE_OFF) {
1012 // Remember the last selected element
1013 mResurrectToPosition = mSelectedPosition;
1014 }
1015 } else {
1016 // If we changed touch mode since the last time we had focus
1017 if (touchMode != mLastTouchMode && mLastTouchMode != TOUCH_MODE_UNKNOWN) {
1018 // If we come back in trackball mode, we bring the selection back
1019 if (touchMode == TOUCH_MODE_OFF) {
1020 // This will trigger a layout
1021 resurrectSelection();
1023 // If we come back in touch mode, then we want to hide the selector
1024 } else {
1025 hideSelector();
1026 mLayoutMode = LAYOUT_NORMAL;
1027 layoutChildren();
1028 }
1029 }
1030 }
1032 mLastTouchMode = touchMode;
1033 }
1035 @Override
1036 protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
1037 boolean needsInvalidate = false;
1039 if (mIsVertical && mOverScroll != scrollY) {
1040 onScrollChanged(getScrollX(), scrollY, getScrollX(), mOverScroll);
1041 mOverScroll = scrollY;
1042 needsInvalidate = true;
1043 } else if (!mIsVertical && mOverScroll != scrollX) {
1044 onScrollChanged(scrollX, getScrollY(), mOverScroll, getScrollY());
1045 mOverScroll = scrollX;
1046 needsInvalidate = true;
1047 }
1049 if (needsInvalidate) {
1050 invalidate();
1051 awakenScrollbarsInternal();
1052 }
1053 }
1055 @TargetApi(9)
1056 private boolean overScrollByInternal(int deltaX, int deltaY,
1057 int scrollX, int scrollY,
1058 int scrollRangeX, int scrollRangeY,
1059 int maxOverScrollX, int maxOverScrollY,
1060 boolean isTouchEvent) {
1061 if (Build.VERSION.SDK_INT < 9) {
1062 return false;
1063 }
1065 return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX,
1066 scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent);
1067 }
1069 @Override
1070 @TargetApi(9)
1071 public void setOverScrollMode(int mode) {
1072 if (Build.VERSION.SDK_INT < 9) {
1073 return;
1074 }
1076 if (mode != ViewCompat.OVER_SCROLL_NEVER) {
1077 if (mStartEdge == null) {
1078 Context context = getContext();
1080 mStartEdge = new EdgeEffectCompat(context);
1081 mEndEdge = new EdgeEffectCompat(context);
1082 }
1083 } else {
1084 mStartEdge = null;
1085 mEndEdge = null;
1086 }
1088 super.setOverScrollMode(mode);
1089 }
1091 public int pointToPosition(int x, int y) {
1092 Rect frame = mTouchFrame;
1093 if (frame == null) {
1094 mTouchFrame = new Rect();
1095 frame = mTouchFrame;
1096 }
1098 final int count = getChildCount();
1099 for (int i = count - 1; i >= 0; i--) {
1100 final View child = getChildAt(i);
1102 if (child.getVisibility() == View.VISIBLE) {
1103 child.getHitRect(frame);
1105 if (frame.contains(x, y)) {
1106 return mFirstPosition + i;
1107 }
1108 }
1109 }
1110 return INVALID_POSITION;
1111 }
1113 @Override
1114 protected int computeVerticalScrollExtent() {
1115 final int count = getChildCount();
1116 if (count == 0) {
1117 return 0;
1118 }
1120 int extent = count * 100;
1122 View child = getChildAt(0);
1123 final int childTop = child.getTop();
1125 int childHeight = child.getHeight();
1126 if (childHeight > 0) {
1127 extent += (childTop * 100) / childHeight;
1128 }
1130 child = getChildAt(count - 1);
1131 final int childBottom = child.getBottom();
1133 childHeight = child.getHeight();
1134 if (childHeight > 0) {
1135 extent -= ((childBottom - getHeight()) * 100) / childHeight;
1136 }
1138 return extent;
1139 }
1141 @Override
1142 protected int computeHorizontalScrollExtent() {
1143 final int count = getChildCount();
1144 if (count == 0) {
1145 return 0;
1146 }
1148 int extent = count * 100;
1150 View child = getChildAt(0);
1151 final int childLeft = child.getLeft();
1153 int childWidth = child.getWidth();
1154 if (childWidth > 0) {
1155 extent += (childLeft * 100) / childWidth;
1156 }
1158 child = getChildAt(count - 1);
1159 final int childRight = child.getRight();
1161 childWidth = child.getWidth();
1162 if (childWidth > 0) {
1163 extent -= ((childRight - getWidth()) * 100) / childWidth;
1164 }
1166 return extent;
1167 }
1169 @Override
1170 protected int computeVerticalScrollOffset() {
1171 final int firstPosition = mFirstPosition;
1172 final int childCount = getChildCount();
1174 if (firstPosition < 0 || childCount == 0) {
1175 return 0;
1176 }
1178 final View child = getChildAt(0);
1179 final int childTop = child.getTop();
1181 int childHeight = child.getHeight();
1182 if (childHeight > 0) {
1183 return Math.max(firstPosition * 100 - (childTop * 100) / childHeight, 0);
1184 }
1186 return 0;
1187 }
1189 @Override
1190 protected int computeHorizontalScrollOffset() {
1191 final int firstPosition = mFirstPosition;
1192 final int childCount = getChildCount();
1194 if (firstPosition < 0 || childCount == 0) {
1195 return 0;
1196 }
1198 final View child = getChildAt(0);
1199 final int childLeft = child.getLeft();
1201 int childWidth = child.getWidth();
1202 if (childWidth > 0) {
1203 return Math.max(firstPosition * 100 - (childLeft * 100) / childWidth, 0);
1204 }
1206 return 0;
1207 }
1209 @Override
1210 protected int computeVerticalScrollRange() {
1211 int result = Math.max(mItemCount * 100, 0);
1213 if (mIsVertical && mOverScroll != 0) {
1214 // Compensate for overscroll
1215 result += Math.abs((int) ((float) mOverScroll / getHeight() * mItemCount * 100));
1216 }
1218 return result;
1219 }
1221 @Override
1222 protected int computeHorizontalScrollRange() {
1223 int result = Math.max(mItemCount * 100, 0);
1225 if (!mIsVertical && mOverScroll != 0) {
1226 // Compensate for overscroll
1227 result += Math.abs((int) ((float) mOverScroll / getWidth() * mItemCount * 100));
1228 }
1230 return result;
1231 }
1233 @Override
1234 public boolean showContextMenuForChild(View originalView) {
1235 final int longPressPosition = getPositionForView(originalView);
1236 if (longPressPosition >= 0) {
1237 final long longPressId = mAdapter.getItemId(longPressPosition);
1238 boolean handled = false;
1240 OnItemLongClickListener listener = getOnItemLongClickListener();
1241 if (listener != null) {
1242 handled = listener.onItemLongClick(TwoWayView.this, originalView,
1243 longPressPosition, longPressId);
1244 }
1246 if (!handled) {
1247 mContextMenuInfo = createContextMenuInfo(
1248 getChildAt(longPressPosition - mFirstPosition),
1249 longPressPosition, longPressId);
1251 handled = super.showContextMenuForChild(originalView);
1252 }
1254 return handled;
1255 }
1257 return false;
1258 }
1260 @Override
1261 public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
1262 if (disallowIntercept) {
1263 recycleVelocityTracker();
1264 }
1266 super.requestDisallowInterceptTouchEvent(disallowIntercept);
1267 }
1269 @Override
1270 public boolean onInterceptTouchEvent(MotionEvent ev) {
1271 if (!mIsAttached) {
1272 return false;
1273 }
1275 final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
1276 switch (action) {
1277 case MotionEvent.ACTION_DOWN:
1278 initOrResetVelocityTracker();
1279 mVelocityTracker.addMovement(ev);
1281 mScroller.abortAnimation();
1283 final float x = ev.getX();
1284 final float y = ev.getY();
1286 mLastTouchPos = (mIsVertical ? y : x);
1288 final int motionPosition = findMotionRowOrColumn((int) mLastTouchPos);
1290 mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
1291 mTouchRemainderPos = 0;
1293 if (mTouchMode == TOUCH_MODE_FLINGING) {
1294 return true;
1295 } else if (motionPosition >= 0) {
1296 mMotionPosition = motionPosition;
1297 mTouchMode = TOUCH_MODE_DOWN;
1298 }
1300 break;
1302 case MotionEvent.ACTION_MOVE: {
1303 if (mTouchMode != TOUCH_MODE_DOWN) {
1304 break;
1305 }
1307 initVelocityTrackerIfNotExists();
1308 mVelocityTracker.addMovement(ev);
1310 final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
1311 if (index < 0) {
1312 Log.e(LOGTAG, "onInterceptTouchEvent could not find pointer with id " +
1313 mActivePointerId + " - did TwoWayView receive an inconsistent " +
1314 "event stream?");
1315 return false;
1316 }
1318 final float pos;
1319 if (mIsVertical) {
1320 pos = MotionEventCompat.getY(ev, index);
1321 } else {
1322 pos = MotionEventCompat.getX(ev, index);
1323 }
1325 final float diff = pos - mLastTouchPos + mTouchRemainderPos;
1326 final int delta = (int) diff;
1327 mTouchRemainderPos = diff - delta;
1329 if (maybeStartScrolling(delta)) {
1330 return true;
1331 }
1333 break;
1334 }
1336 case MotionEvent.ACTION_CANCEL:
1337 case MotionEvent.ACTION_UP:
1338 mActivePointerId = INVALID_POINTER;
1339 mTouchMode = TOUCH_MODE_REST;
1340 recycleVelocityTracker();
1341 reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
1343 break;
1344 }
1346 return false;
1347 }
1349 @Override
1350 public boolean onTouchEvent(MotionEvent ev) {
1351 if (!isEnabled()) {
1352 // A disabled view that is clickable still consumes the touch
1353 // events, it just doesn't respond to them.
1354 return isClickable() || isLongClickable();
1355 }
1357 if (!mIsAttached) {
1358 return false;
1359 }
1361 boolean needsInvalidate = false;
1363 initVelocityTrackerIfNotExists();
1364 mVelocityTracker.addMovement(ev);
1366 final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
1367 switch (action) {
1368 case MotionEvent.ACTION_DOWN: {
1369 if (mDataChanged) {
1370 break;
1371 }
1373 mVelocityTracker.clear();
1374 mScroller.abortAnimation();
1376 final float x = ev.getX();
1377 final float y = ev.getY();
1379 mLastTouchPos = (mIsVertical ? y : x);
1381 int motionPosition = pointToPosition((int) x, (int) y);
1383 mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
1384 mTouchRemainderPos = 0;
1386 if (mDataChanged) {
1387 break;
1388 }
1390 if (mTouchMode == TOUCH_MODE_FLINGING) {
1391 mTouchMode = TOUCH_MODE_DRAGGING;
1392 reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
1393 motionPosition = findMotionRowOrColumn((int) mLastTouchPos);
1394 return true;
1395 } else if (mMotionPosition >= 0 && mAdapter.isEnabled(mMotionPosition)) {
1396 mTouchMode = TOUCH_MODE_DOWN;
1397 triggerCheckForTap();
1398 }
1400 mMotionPosition = motionPosition;
1402 break;
1403 }
1405 case MotionEvent.ACTION_MOVE: {
1406 final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
1407 if (index < 0) {
1408 Log.e(LOGTAG, "onInterceptTouchEvent could not find pointer with id " +
1409 mActivePointerId + " - did TwoWayView receive an inconsistent " +
1410 "event stream?");
1411 return false;
1412 }
1414 final float pos;
1415 if (mIsVertical) {
1416 pos = MotionEventCompat.getY(ev, index);
1417 } else {
1418 pos = MotionEventCompat.getX(ev, index);
1419 }
1421 if (mDataChanged) {
1422 // Re-sync everything if data has been changed
1423 // since the scroll operation can query the adapter.
1424 layoutChildren();
1425 }
1427 final float diff = pos - mLastTouchPos + mTouchRemainderPos;
1428 final int delta = (int) diff;
1429 mTouchRemainderPos = diff - delta;
1431 switch (mTouchMode) {
1432 case TOUCH_MODE_DOWN:
1433 case TOUCH_MODE_TAP:
1434 case TOUCH_MODE_DONE_WAITING:
1435 // Check if we have moved far enough that it looks more like a
1436 // scroll than a tap
1437 maybeStartScrolling(delta);
1438 break;
1440 case TOUCH_MODE_DRAGGING:
1441 case TOUCH_MODE_OVERSCROLL:
1442 mLastTouchPos = pos;
1443 maybeScroll(delta);
1444 break;
1445 }
1447 break;
1448 }
1450 case MotionEvent.ACTION_CANCEL:
1451 cancelCheckForTap();
1452 mTouchMode = TOUCH_MODE_REST;
1453 reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
1455 setPressed(false);
1456 View motionView = this.getChildAt(mMotionPosition - mFirstPosition);
1457 if (motionView != null) {
1458 motionView.setPressed(false);
1459 }
1461 if (mStartEdge != null && mEndEdge != null) {
1462 needsInvalidate = mStartEdge.onRelease() | mEndEdge.onRelease();
1463 }
1465 recycleVelocityTracker();
1467 break;
1469 case MotionEvent.ACTION_UP: {
1470 switch (mTouchMode) {
1471 case TOUCH_MODE_DOWN:
1472 case TOUCH_MODE_TAP:
1473 case TOUCH_MODE_DONE_WAITING: {
1474 final int motionPosition = mMotionPosition;
1475 final View child = getChildAt(motionPosition - mFirstPosition);
1477 final float x = ev.getX();
1478 final float y = ev.getY();
1480 boolean inList = false;
1481 if (mIsVertical) {
1482 inList = x > getPaddingLeft() && x < getWidth() - getPaddingRight();
1483 } else {
1484 inList = y > getPaddingTop() && y < getHeight() - getPaddingBottom();
1485 }
1487 if (child != null && !child.hasFocusable() && inList) {
1488 if (mTouchMode != TOUCH_MODE_DOWN) {
1489 child.setPressed(false);
1490 }
1492 if (mPerformClick == null) {
1493 mPerformClick = new PerformClick();
1494 }
1496 final PerformClick performClick = mPerformClick;
1497 performClick.mClickMotionPosition = motionPosition;
1498 performClick.rememberWindowAttachCount();
1500 mResurrectToPosition = motionPosition;
1502 if (mTouchMode == TOUCH_MODE_DOWN || mTouchMode == TOUCH_MODE_TAP) {
1503 if (mTouchMode == TOUCH_MODE_DOWN) {
1504 cancelCheckForTap();
1505 } else {
1506 cancelCheckForLongPress();
1507 }
1509 mLayoutMode = LAYOUT_NORMAL;
1511 if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {
1512 mTouchMode = TOUCH_MODE_TAP;
1514 setPressed(true);
1515 positionSelector(mMotionPosition, child);
1516 child.setPressed(true);
1518 if (mSelector != null) {
1519 Drawable d = mSelector.getCurrent();
1520 if (d != null && d instanceof TransitionDrawable) {
1521 ((TransitionDrawable) d).resetTransition();
1522 }
1523 }
1525 if (mTouchModeReset != null) {
1526 removeCallbacks(mTouchModeReset);
1527 }
1529 mTouchModeReset = new Runnable() {
1530 @Override
1531 public void run() {
1532 mTouchMode = TOUCH_MODE_REST;
1534 setPressed(false);
1535 child.setPressed(false);
1537 if (!mDataChanged) {
1538 performClick.run();
1539 }
1541 mTouchModeReset = null;
1542 }
1543 };
1545 postDelayed(mTouchModeReset,
1546 ViewConfiguration.getPressedStateDuration());
1547 } else {
1548 mTouchMode = TOUCH_MODE_REST;
1549 updateSelectorState();
1550 }
1551 } else if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {
1552 performClick.run();
1553 }
1554 }
1556 mTouchMode = TOUCH_MODE_REST;
1557 updateSelectorState();
1559 break;
1560 }
1562 case TOUCH_MODE_DRAGGING:
1563 if (contentFits()) {
1564 mTouchMode = TOUCH_MODE_REST;
1565 reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
1566 break;
1567 }
1569 mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
1571 final float velocity;
1572 if (mIsVertical) {
1573 velocity = VelocityTrackerCompat.getYVelocity(mVelocityTracker,
1574 mActivePointerId);
1575 } else {
1576 velocity = VelocityTrackerCompat.getXVelocity(mVelocityTracker,
1577 mActivePointerId);
1578 }
1580 if (Math.abs(velocity) >= mFlingVelocity) {
1581 mTouchMode = TOUCH_MODE_FLINGING;
1582 reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
1584 mScroller.fling(0, 0,
1585 (int) (mIsVertical ? 0 : velocity),
1586 (int) (mIsVertical ? velocity : 0),
1587 (mIsVertical ? 0 : Integer.MIN_VALUE),
1588 (mIsVertical ? 0 : Integer.MAX_VALUE),
1589 (mIsVertical ? Integer.MIN_VALUE : 0),
1590 (mIsVertical ? Integer.MAX_VALUE : 0));
1592 mLastTouchPos = 0;
1593 needsInvalidate = true;
1594 } else {
1595 mTouchMode = TOUCH_MODE_REST;
1596 reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
1597 }
1599 break;
1601 case TOUCH_MODE_OVERSCROLL:
1602 mTouchMode = TOUCH_MODE_REST;
1603 reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
1604 break;
1605 }
1607 cancelCheckForTap();
1608 cancelCheckForLongPress();
1609 setPressed(false);
1611 if (mStartEdge != null && mEndEdge != null) {
1612 needsInvalidate |= mStartEdge.onRelease() | mEndEdge.onRelease();
1613 }
1615 recycleVelocityTracker();
1617 break;
1618 }
1619 }
1621 if (needsInvalidate) {
1622 ViewCompat.postInvalidateOnAnimation(this);
1623 }
1625 return true;
1626 }
1628 @Override
1629 public void onTouchModeChanged(boolean isInTouchMode) {
1630 if (isInTouchMode) {
1631 // Get rid of the selection when we enter touch mode
1632 hideSelector();
1634 // Layout, but only if we already have done so previously.
1635 // (Otherwise may clobber a LAYOUT_SYNC layout that was requested to restore
1636 // state.)
1637 if (getWidth() > 0 && getHeight() > 0 && getChildCount() > 0) {
1638 layoutChildren();
1639 }
1641 updateSelectorState();
1642 } else {
1643 final int touchMode = mTouchMode;
1644 if (touchMode == TOUCH_MODE_OVERSCROLL) {
1645 if (mOverScroll != 0) {
1646 mOverScroll = 0;
1647 finishEdgeGlows();
1648 invalidate();
1649 }
1650 }
1651 }
1652 }
1654 @Override
1655 public boolean onKeyDown(int keyCode, KeyEvent event) {
1656 return handleKeyEvent(keyCode, 1, event);
1657 }
1659 @Override
1660 public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
1661 return handleKeyEvent(keyCode, repeatCount, event);
1662 }
1664 @Override
1665 public boolean onKeyUp(int keyCode, KeyEvent event) {
1666 return handleKeyEvent(keyCode, 1, event);
1667 }
1669 @Override
1670 public void sendAccessibilityEvent(int eventType) {
1671 // Since this class calls onScrollChanged even if the mFirstPosition and the
1672 // child count have not changed we will avoid sending duplicate accessibility
1673 // events.
1674 if (eventType == AccessibilityEvent.TYPE_VIEW_SCROLLED) {
1675 final int firstVisiblePosition = getFirstVisiblePosition();
1676 final int lastVisiblePosition = getLastVisiblePosition();
1678 if (mLastAccessibilityScrollEventFromIndex == firstVisiblePosition
1679 && mLastAccessibilityScrollEventToIndex == lastVisiblePosition) {
1680 return;
1681 } else {
1682 mLastAccessibilityScrollEventFromIndex = firstVisiblePosition;
1683 mLastAccessibilityScrollEventToIndex = lastVisiblePosition;
1684 }
1685 }
1687 super.sendAccessibilityEvent(eventType);
1688 }
1690 @Override
1691 @TargetApi(14)
1692 public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
1693 super.onInitializeAccessibilityEvent(event);
1694 event.setClassName(TwoWayView.class.getName());
1695 }
1697 @Override
1698 @TargetApi(14)
1699 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
1700 super.onInitializeAccessibilityNodeInfo(info);
1701 info.setClassName(TwoWayView.class.getName());
1703 AccessibilityNodeInfoCompat infoCompat = new AccessibilityNodeInfoCompat(info);
1705 if (isEnabled()) {
1706 if (getFirstVisiblePosition() > 0) {
1707 infoCompat.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD);
1708 }
1710 if (getLastVisiblePosition() < getCount() - 1) {
1711 infoCompat.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD);
1712 }
1713 }
1714 }
1716 @Override
1717 @TargetApi(16)
1718 public boolean performAccessibilityAction(int action, Bundle arguments) {
1719 if (super.performAccessibilityAction(action, arguments)) {
1720 return true;
1721 }
1723 switch (action) {
1724 case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
1725 if (isEnabled() && getLastVisiblePosition() < getCount() - 1) {
1726 final int viewportSize;
1727 if (mIsVertical) {
1728 viewportSize = getHeight() - getPaddingTop() - getPaddingBottom();
1729 } else {
1730 viewportSize = getWidth() - getPaddingLeft() - getPaddingRight();
1731 }
1733 // TODO: Use some form of smooth scroll instead
1734 trackMotionScroll(viewportSize);
1735 return true;
1736 }
1737 return false;
1739 case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD:
1740 if (isEnabled() && mFirstPosition > 0) {
1741 final int viewportSize;
1742 if (mIsVertical) {
1743 viewportSize = getHeight() - getPaddingTop() - getPaddingBottom();
1744 } else {
1745 viewportSize = getWidth() - getPaddingLeft() - getPaddingRight();
1746 }
1748 // TODO: Use some form of smooth scroll instead
1749 trackMotionScroll(-viewportSize);
1750 return true;
1751 }
1752 return false;
1753 }
1755 return false;
1756 }
1758 /**
1759 * Return true if child is an ancestor of parent, (or equal to the parent).
1760 */
1761 private boolean isViewAncestorOf(View child, View parent) {
1762 if (child == parent) {
1763 return true;
1764 }
1766 final ViewParent theParent = child.getParent();
1768 return (theParent instanceof ViewGroup) &&
1769 isViewAncestorOf((View) theParent, parent);
1770 }
1772 private void forceValidFocusDirection(int direction) {
1773 if (mIsVertical && direction != View.FOCUS_UP && direction != View.FOCUS_DOWN) {
1774 throw new IllegalArgumentException("Focus direction must be one of"
1775 + " {View.FOCUS_UP, View.FOCUS_DOWN} for vertical orientation");
1776 } else if (!mIsVertical && direction != View.FOCUS_LEFT && direction != View.FOCUS_RIGHT) {
1777 throw new IllegalArgumentException("Focus direction must be one of"
1778 + " {View.FOCUS_LEFT, View.FOCUS_RIGHT} for vertical orientation");
1779 }
1780 }
1782 private void forceValidInnerFocusDirection(int direction) {
1783 if (mIsVertical && direction != View.FOCUS_LEFT && direction != View.FOCUS_RIGHT) {
1784 throw new IllegalArgumentException("Direction must be one of"
1785 + " {View.FOCUS_LEFT, View.FOCUS_RIGHT} for vertical orientation");
1786 } else if (!mIsVertical && direction != View.FOCUS_UP && direction != View.FOCUS_DOWN) {
1787 throw new IllegalArgumentException("direction must be one of"
1788 + " {View.FOCUS_UP, View.FOCUS_DOWN} for horizontal orientation");
1789 }
1790 }
1792 /**
1793 * Scrolls up or down by the number of items currently present on screen.
1794 *
1795 * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or
1796 * {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the
1797 * current view orientation.
1798 *
1799 * @return whether selection was moved
1800 */
1801 boolean pageScroll(int direction) {
1802 forceValidFocusDirection(direction);
1804 boolean forward = false;
1805 int nextPage = -1;
1807 if (direction == View.FOCUS_UP || direction == View.FOCUS_LEFT) {
1808 nextPage = Math.max(0, mSelectedPosition - getChildCount() - 1);
1809 } else if (direction == View.FOCUS_DOWN || direction == View.FOCUS_RIGHT) {
1810 nextPage = Math.min(mItemCount - 1, mSelectedPosition + getChildCount() - 1);
1811 forward = true;
1812 }
1814 if (nextPage < 0) {
1815 return false;
1816 }
1818 final int position = lookForSelectablePosition(nextPage, forward);
1819 if (position >= 0) {
1820 mLayoutMode = LAYOUT_SPECIFIC;
1821 mSpecificStart = (mIsVertical ? getPaddingTop() : getPaddingLeft());
1823 if (forward && position > mItemCount - getChildCount()) {
1824 mLayoutMode = LAYOUT_FORCE_BOTTOM;
1825 }
1827 if (!forward && position < getChildCount()) {
1828 mLayoutMode = LAYOUT_FORCE_TOP;
1829 }
1831 setSelectionInt(position);
1832 invokeOnItemScrollListener();
1834 if (!awakenScrollbarsInternal()) {
1835 invalidate();
1836 }
1838 return true;
1839 }
1841 return false;
1842 }
1844 /**
1845 * Go to the last or first item if possible (not worrying about panning across or navigating
1846 * within the internal focus of the currently selected item.)
1847 *
1848 * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or
1849 * {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the
1850 * current view orientation.
1851 *
1852 * @return whether selection was moved
1853 */
1854 boolean fullScroll(int direction) {
1855 forceValidFocusDirection(direction);
1857 boolean moved = false;
1858 if (direction == View.FOCUS_UP || direction == View.FOCUS_LEFT) {
1859 if (mSelectedPosition != 0) {
1860 int position = lookForSelectablePosition(0, true);
1861 if (position >= 0) {
1862 mLayoutMode = LAYOUT_FORCE_TOP;
1863 setSelectionInt(position);
1864 invokeOnItemScrollListener();
1865 }
1867 moved = true;
1868 }
1869 } else if (direction == View.FOCUS_DOWN || direction == View.FOCUS_RIGHT) {
1870 if (mSelectedPosition < mItemCount - 1) {
1871 int position = lookForSelectablePosition(mItemCount - 1, true);
1872 if (position >= 0) {
1873 mLayoutMode = LAYOUT_FORCE_BOTTOM;
1874 setSelectionInt(position);
1875 invokeOnItemScrollListener();
1876 }
1878 moved = true;
1879 }
1880 }
1882 if (moved && !awakenScrollbarsInternal()) {
1883 awakenScrollbarsInternal();
1884 invalidate();
1885 }
1887 return moved;
1888 }
1890 /**
1891 * To avoid horizontal/vertical focus searches changing the selected item,
1892 * we manually focus search within the selected item (as applicable), and
1893 * prevent focus from jumping to something within another item.
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 this consumes the key event.
1900 */
1901 private boolean handleFocusWithinItem(int direction) {
1902 forceValidInnerFocusDirection(direction);
1904 final int numChildren = getChildCount();
1906 if (mItemsCanFocus && numChildren > 0 && mSelectedPosition != INVALID_POSITION) {
1907 final View selectedView = getSelectedView();
1909 if (selectedView != null && selectedView.hasFocus() &&
1910 selectedView instanceof ViewGroup) {
1912 final View currentFocus = selectedView.findFocus();
1913 final View nextFocus = FocusFinder.getInstance().findNextFocus(
1914 (ViewGroup) selectedView, currentFocus, direction);
1916 if (nextFocus != null) {
1917 // Do the math to get interesting rect in next focus' coordinates
1918 currentFocus.getFocusedRect(mTempRect);
1919 offsetDescendantRectToMyCoords(currentFocus, mTempRect);
1920 offsetRectIntoDescendantCoords(nextFocus, mTempRect);
1922 if (nextFocus.requestFocus(direction, mTempRect)) {
1923 return true;
1924 }
1925 }
1927 // We are blocking the key from being handled (by returning true)
1928 // if the global result is going to be some other view within this
1929 // list. This is to achieve the overall goal of having horizontal/vertical
1930 // d-pad navigation remain in the current item depending on the current
1931 // orientation in this view.
1932 final View globalNextFocus = FocusFinder.getInstance().findNextFocus(
1933 (ViewGroup) getRootView(), currentFocus, direction);
1935 if (globalNextFocus != null) {
1936 return isViewAncestorOf(globalNextFocus, this);
1937 }
1938 }
1939 }
1941 return false;
1942 }
1944 /**
1945 * Scrolls to the next or previous item if possible.
1946 *
1947 * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or
1948 * {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the
1949 * current view orientation.
1950 *
1951 * @return whether selection was moved
1952 */
1953 private boolean arrowScroll(int direction) {
1954 forceValidFocusDirection(direction);
1956 try {
1957 mInLayout = true;
1959 final boolean handled = arrowScrollImpl(direction);
1960 if (handled) {
1961 playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction));
1962 }
1964 return handled;
1965 } finally {
1966 mInLayout = false;
1967 }
1968 }
1970 /**
1971 * When selection changes, it is possible that the previously selected or the
1972 * next selected item will change its size. If so, we need to offset some folks,
1973 * and re-layout the items as appropriate.
1974 *
1975 * @param selectedView The currently selected view (before changing selection).
1976 * should be <code>null</code> if there was no previous selection.
1977 * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or
1978 * {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the
1979 * current view orientation.
1980 * @param newSelectedPosition The position of the next selection.
1981 * @param newFocusAssigned whether new focus was assigned. This matters because
1982 * when something has focus, we don't want to show selection (ugh).
1983 */
1984 private void handleNewSelectionChange(View selectedView, int direction, int newSelectedPosition,
1985 boolean newFocusAssigned) {
1986 forceValidFocusDirection(direction);
1988 if (newSelectedPosition == INVALID_POSITION) {
1989 throw new IllegalArgumentException("newSelectedPosition needs to be valid");
1990 }
1992 // Whether or not we are moving down/right or up/left, we want to preserve the
1993 // top/left of whatever view is at the start:
1994 // - moving down/right: the view that had selection
1995 // - moving up/left: the view that is getting selection
1996 final int selectedIndex = mSelectedPosition - mFirstPosition;
1997 final int nextSelectedIndex = newSelectedPosition - mFirstPosition;
1998 int startViewIndex, endViewIndex;
1999 boolean topSelected = false;
2000 View startView;
2001 View endView;
2003 if (direction == View.FOCUS_UP || direction == View.FOCUS_LEFT) {
2004 startViewIndex = nextSelectedIndex;
2005 endViewIndex = selectedIndex;
2006 startView = getChildAt(startViewIndex);
2007 endView = selectedView;
2008 topSelected = true;
2009 } else {
2010 startViewIndex = selectedIndex;
2011 endViewIndex = nextSelectedIndex;
2012 startView = selectedView;
2013 endView = getChildAt(endViewIndex);
2014 }
2016 final int numChildren = getChildCount();
2018 // start with top view: is it changing size?
2019 if (startView != null) {
2020 startView.setSelected(!newFocusAssigned && topSelected);
2021 measureAndAdjustDown(startView, startViewIndex, numChildren);
2022 }
2024 // is the bottom view changing size?
2025 if (endView != null) {
2026 endView.setSelected(!newFocusAssigned && !topSelected);
2027 measureAndAdjustDown(endView, endViewIndex, numChildren);
2028 }
2029 }
2031 /**
2032 * Re-measure a child, and if its height changes, lay it out preserving its
2033 * top, and adjust the children below it appropriately.
2034 *
2035 * @param child The child
2036 * @param childIndex The view group index of the child.
2037 * @param numChildren The number of children in the view group.
2038 */
2039 private void measureAndAdjustDown(View child, int childIndex, int numChildren) {
2040 int oldHeight = child.getHeight();
2041 measureChild(child);
2043 if (child.getMeasuredHeight() == oldHeight) {
2044 return;
2045 }
2047 // lay out the view, preserving its top
2048 relayoutMeasuredChild(child);
2050 // adjust views below appropriately
2051 final int heightDelta = child.getMeasuredHeight() - oldHeight;
2052 for (int i = childIndex + 1; i < numChildren; i++) {
2053 getChildAt(i).offsetTopAndBottom(heightDelta);
2054 }
2055 }
2057 /**
2058 * Do an arrow scroll based on focus searching. If a new view is
2059 * given focus, return the selection delta and amount to scroll via
2060 * an {@link ArrowScrollFocusResult}, otherwise, return null.
2061 *
2062 * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or
2063 * {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the
2064 * current view orientation.
2065 *
2066 * @return The result if focus has changed, or <code>null</code>.
2067 */
2068 private ArrowScrollFocusResult arrowScrollFocused(final int direction) {
2069 forceValidFocusDirection(direction);
2071 final View selectedView = getSelectedView();
2072 final View newFocus;
2073 final int searchPoint;
2075 if (selectedView != null && selectedView.hasFocus()) {
2076 View oldFocus = selectedView.findFocus();
2077 newFocus = FocusFinder.getInstance().findNextFocus(this, oldFocus, direction);
2078 } else {
2079 if (direction == View.FOCUS_DOWN || direction == View.FOCUS_RIGHT) {
2080 final int start = (mIsVertical ? getPaddingTop() : getPaddingLeft());
2082 final int selectedStart;
2083 if (selectedView != null) {
2084 selectedStart = (mIsVertical ? selectedView.getTop() : selectedView.getLeft());
2085 } else {
2086 selectedStart = start;
2087 }
2089 searchPoint = Math.max(selectedStart, start);
2090 } else {
2091 final int end = (mIsVertical ? getHeight() - getPaddingBottom() :
2092 getWidth() - getPaddingRight());
2094 final int selectedEnd;
2095 if (selectedView != null) {
2096 selectedEnd = (mIsVertical ? selectedView.getBottom() : selectedView.getRight());
2097 } else {
2098 selectedEnd = end;
2099 }
2101 searchPoint = Math.min(selectedEnd, end);
2102 }
2104 final int x = (mIsVertical ? 0 : searchPoint);
2105 final int y = (mIsVertical ? searchPoint : 0);
2106 mTempRect.set(x, y, x, y);
2108 newFocus = FocusFinder.getInstance().findNextFocusFromRect(this, mTempRect, direction);
2109 }
2111 if (newFocus != null) {
2112 final int positionOfNewFocus = positionOfNewFocus(newFocus);
2114 // If the focus change is in a different new position, make sure
2115 // we aren't jumping over another selectable position.
2116 if (mSelectedPosition != INVALID_POSITION && positionOfNewFocus != mSelectedPosition) {
2117 final int selectablePosition = lookForSelectablePositionOnScreen(direction);
2119 final boolean movingForward =
2120 (direction == View.FOCUS_DOWN || direction == View.FOCUS_RIGHT);
2121 final boolean movingBackward =
2122 (direction == View.FOCUS_UP || direction == View.FOCUS_LEFT);
2124 if (selectablePosition != INVALID_POSITION &&
2125 ((movingForward && selectablePosition < positionOfNewFocus) ||
2126 (movingBackward && selectablePosition > positionOfNewFocus))) {
2127 return null;
2128 }
2129 }
2131 int focusScroll = amountToScrollToNewFocus(direction, newFocus, positionOfNewFocus);
2133 final int maxScrollAmount = getMaxScrollAmount();
2134 if (focusScroll < maxScrollAmount) {
2135 // Not moving too far, safe to give next view focus
2136 newFocus.requestFocus(direction);
2137 mArrowScrollFocusResult.populate(positionOfNewFocus, focusScroll);
2138 return mArrowScrollFocusResult;
2139 } else if (distanceToView(newFocus) < maxScrollAmount){
2140 // Case to consider:
2141 // Too far to get entire next focusable on screen, but by going
2142 // max scroll amount, we are getting it at least partially in view,
2143 // so give it focus and scroll the max amount.
2144 newFocus.requestFocus(direction);
2145 mArrowScrollFocusResult.populate(positionOfNewFocus, maxScrollAmount);
2146 return mArrowScrollFocusResult;
2147 }
2148 }
2150 return null;
2151 }
2153 /**
2154 * @return The maximum amount a list view will scroll in response to
2155 * an arrow event.
2156 */
2157 public int getMaxScrollAmount() {
2158 return (int) (MAX_SCROLL_FACTOR * getHeight());
2159 }
2161 /**
2162 * @return The amount to preview next items when arrow scrolling.
2163 */
2164 private int getArrowScrollPreviewLength() {
2165 // FIXME: TwoWayView has no fading edge support just yet but using it
2166 // makes it convenient for defining the next item's previous length.
2167 int fadingEdgeLength =
2168 (mIsVertical ? getVerticalFadingEdgeLength() : getHorizontalFadingEdgeLength());
2170 return mItemMargin + Math.max(MIN_SCROLL_PREVIEW_PIXELS, fadingEdgeLength);
2171 }
2173 /**
2174 * @param newFocus The view that would have focus.
2175 * @return the position that contains newFocus
2176 */
2177 private int positionOfNewFocus(View newFocus) {
2178 final int numChildren = getChildCount();
2180 for (int i = 0; i < numChildren; i++) {
2181 final View child = getChildAt(i);
2182 if (isViewAncestorOf(newFocus, child)) {
2183 return mFirstPosition + i;
2184 }
2185 }
2187 throw new IllegalArgumentException("newFocus is not a child of any of the"
2188 + " children of the list!");
2189 }
2191 /**
2192 * Handle an arrow scroll going up or down. Take into account whether items are selectable,
2193 * whether there are focusable items, etc.
2194 *
2195 * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or
2196 * {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the
2197 * current view orientation.
2198 *
2199 * @return Whether any scrolling, selection or focus change occurred.
2200 */
2201 private boolean arrowScrollImpl(int direction) {
2202 forceValidFocusDirection(direction);
2204 if (getChildCount() <= 0) {
2205 return false;
2206 }
2208 View selectedView = getSelectedView();
2209 int selectedPos = mSelectedPosition;
2211 int nextSelectedPosition = lookForSelectablePositionOnScreen(direction);
2212 int amountToScroll = amountToScroll(direction, nextSelectedPosition);
2214 // If we are moving focus, we may OVERRIDE the default behaviour
2215 final ArrowScrollFocusResult focusResult = (mItemsCanFocus ? arrowScrollFocused(direction) : null);
2216 if (focusResult != null) {
2217 nextSelectedPosition = focusResult.getSelectedPosition();
2218 amountToScroll = focusResult.getAmountToScroll();
2219 }
2221 boolean needToRedraw = (focusResult != null);
2222 if (nextSelectedPosition != INVALID_POSITION) {
2223 handleNewSelectionChange(selectedView, direction, nextSelectedPosition, focusResult != null);
2225 setSelectedPositionInt(nextSelectedPosition);
2226 setNextSelectedPositionInt(nextSelectedPosition);
2228 selectedView = getSelectedView();
2229 selectedPos = nextSelectedPosition;
2231 if (mItemsCanFocus && focusResult == null) {
2232 // There was no new view found to take focus, make sure we
2233 // don't leave focus with the old selection.
2234 final View focused = getFocusedChild();
2235 if (focused != null) {
2236 focused.clearFocus();
2237 }
2238 }
2240 needToRedraw = true;
2241 checkSelectionChanged();
2242 }
2244 if (amountToScroll > 0) {
2245 trackMotionScroll(direction == View.FOCUS_UP || direction == View.FOCUS_LEFT ?
2246 amountToScroll : -amountToScroll);
2247 needToRedraw = true;
2248 }
2250 // If we didn't find a new focusable, make sure any existing focused
2251 // item that was panned off screen gives up focus.
2252 if (mItemsCanFocus && focusResult == null &&
2253 selectedView != null && selectedView.hasFocus()) {
2254 final View focused = selectedView.findFocus();
2255 if (!isViewAncestorOf(focused, this) || distanceToView(focused) > 0) {
2256 focused.clearFocus();
2257 }
2258 }
2260 // If the current selection is panned off, we need to remove the selection
2261 if (nextSelectedPosition == INVALID_POSITION && selectedView != null
2262 && !isViewAncestorOf(selectedView, this)) {
2263 selectedView = null;
2264 hideSelector();
2266 // But we don't want to set the ressurect position (that would make subsequent
2267 // unhandled key events bring back the item we just scrolled off)
2268 mResurrectToPosition = INVALID_POSITION;
2269 }
2271 if (needToRedraw) {
2272 if (selectedView != null) {
2273 positionSelector(selectedPos, selectedView);
2274 mSelectedStart = selectedView.getTop();
2275 }
2277 if (!awakenScrollbarsInternal()) {
2278 invalidate();
2279 }
2281 invokeOnItemScrollListener();
2282 return true;
2283 }
2285 return false;
2286 }
2288 /**
2289 * Determine how much we need to scroll in order to get the next selected view
2290 * visible. The amount is capped at {@link #getMaxScrollAmount()}.
2291 *
2292 * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or
2293 * {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the
2294 * current view orientation.
2295 * @param nextSelectedPosition The position of the next selection, or
2296 * {@link #INVALID_POSITION} if there is no next selectable position
2297 *
2298 * @return The amount to scroll. Note: this is always positive! Direction
2299 * needs to be taken into account when actually scrolling.
2300 */
2301 private int amountToScroll(int direction, int nextSelectedPosition) {
2302 forceValidFocusDirection(direction);
2304 final int numChildren = getChildCount();
2306 if (direction == View.FOCUS_DOWN || direction == View.FOCUS_RIGHT) {
2307 final int end = (mIsVertical ? getHeight() - getPaddingBottom() :
2308 getWidth() - getPaddingRight());
2310 int indexToMakeVisible = numChildren - 1;
2311 if (nextSelectedPosition != INVALID_POSITION) {
2312 indexToMakeVisible = nextSelectedPosition - mFirstPosition;
2313 }
2315 final int positionToMakeVisible = mFirstPosition + indexToMakeVisible;
2316 final View viewToMakeVisible = getChildAt(indexToMakeVisible);
2318 int goalEnd = end;
2319 if (positionToMakeVisible < mItemCount - 1) {
2320 goalEnd -= getArrowScrollPreviewLength();
2321 }
2323 final int viewToMakeVisibleStart =
2324 (mIsVertical ? viewToMakeVisible.getTop() : viewToMakeVisible.getLeft());
2325 final int viewToMakeVisibleEnd =
2326 (mIsVertical ? viewToMakeVisible.getBottom() : viewToMakeVisible.getRight());
2328 if (viewToMakeVisibleEnd <= goalEnd) {
2329 // Target item is fully visible
2330 return 0;
2331 }
2333 if (nextSelectedPosition != INVALID_POSITION &&
2334 (goalEnd - viewToMakeVisibleStart) >= getMaxScrollAmount()) {
2335 // Item already has enough of it visible, changing selection is good enough
2336 return 0;
2337 }
2339 int amountToScroll = (viewToMakeVisibleEnd - goalEnd);
2341 if (mFirstPosition + numChildren == mItemCount) {
2342 final View lastChild = getChildAt(numChildren - 1);
2343 final int lastChildEnd = (mIsVertical ? lastChild.getBottom() : lastChild.getRight());
2345 // Last is last in list -> Make sure we don't scroll past it
2346 final int max = lastChildEnd - end;
2347 amountToScroll = Math.min(amountToScroll, max);
2348 }
2350 return Math.min(amountToScroll, getMaxScrollAmount());
2351 } else {
2352 final int start = (mIsVertical ? getPaddingTop() : getPaddingLeft());
2354 int indexToMakeVisible = 0;
2355 if (nextSelectedPosition != INVALID_POSITION) {
2356 indexToMakeVisible = nextSelectedPosition - mFirstPosition;
2357 }
2359 final int positionToMakeVisible = mFirstPosition + indexToMakeVisible;
2360 final View viewToMakeVisible = getChildAt(indexToMakeVisible);
2362 int goalStart = start;
2363 if (positionToMakeVisible > 0) {
2364 goalStart += getArrowScrollPreviewLength();
2365 }
2367 final int viewToMakeVisibleStart =
2368 (mIsVertical ? viewToMakeVisible.getTop() : viewToMakeVisible.getLeft());
2369 final int viewToMakeVisibleEnd =
2370 (mIsVertical ? viewToMakeVisible.getBottom() : viewToMakeVisible.getRight());
2372 if (viewToMakeVisibleStart >= goalStart) {
2373 // Item is fully visible
2374 return 0;
2375 }
2377 if (nextSelectedPosition != INVALID_POSITION &&
2378 (viewToMakeVisibleEnd - goalStart) >= getMaxScrollAmount()) {
2379 // Item already has enough of it visible, changing selection is good enough
2380 return 0;
2381 }
2383 int amountToScroll = (goalStart - viewToMakeVisibleStart);
2385 if (mFirstPosition == 0) {
2386 final View firstChild = getChildAt(0);
2387 final int firstChildStart = (mIsVertical ? firstChild.getTop() : firstChild.getLeft());
2389 // First is first in list -> make sure we don't scroll past it
2390 final int max = start - firstChildStart;
2391 amountToScroll = Math.min(amountToScroll, max);
2392 }
2394 return Math.min(amountToScroll, getMaxScrollAmount());
2395 }
2396 }
2398 /**
2399 * Determine how much we need to scroll in order to get newFocus in view.
2400 *
2401 * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or
2402 * {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the
2403 * current view orientation.
2404 * @param newFocus The view that would take focus.
2405 * @param positionOfNewFocus The position of the list item containing newFocus
2406 *
2407 * @return The amount to scroll. Note: this is always positive! Direction
2408 * needs to be taken into account when actually scrolling.
2409 */
2410 private int amountToScrollToNewFocus(int direction, View newFocus, int positionOfNewFocus) {
2411 forceValidFocusDirection(direction);
2413 int amountToScroll = 0;
2415 newFocus.getDrawingRect(mTempRect);
2416 offsetDescendantRectToMyCoords(newFocus, mTempRect);
2418 if (direction == View.FOCUS_UP || direction == View.FOCUS_LEFT) {
2419 final int start = (mIsVertical ? getPaddingTop() : getPaddingLeft());
2420 final int newFocusStart = (mIsVertical ? mTempRect.top : mTempRect.left);
2422 if (newFocusStart < start) {
2423 amountToScroll = start - newFocusStart;
2424 if (positionOfNewFocus > 0) {
2425 amountToScroll += getArrowScrollPreviewLength();
2426 }
2427 }
2428 } else {
2429 final int end = (mIsVertical ? getHeight() - getPaddingBottom() :
2430 getWidth() - getPaddingRight());
2431 final int newFocusEnd = (mIsVertical ? mTempRect.bottom : mTempRect.right);
2433 if (newFocusEnd > end) {
2434 amountToScroll = newFocusEnd - end;
2435 if (positionOfNewFocus < mItemCount - 1) {
2436 amountToScroll += getArrowScrollPreviewLength();
2437 }
2438 }
2439 }
2441 return amountToScroll;
2442 }
2444 /**
2445 * Determine the distance to the nearest edge of a view in a particular
2446 * direction.
2447 *
2448 * @param descendant A descendant of this list.
2449 * @return The distance, or 0 if the nearest edge is already on screen.
2450 */
2451 private int distanceToView(View descendant) {
2452 descendant.getDrawingRect(mTempRect);
2453 offsetDescendantRectToMyCoords(descendant, mTempRect);
2455 final int start = (mIsVertical ? getPaddingTop() : getPaddingLeft());
2456 final int end = (mIsVertical ? getHeight() - getPaddingBottom() :
2457 getWidth() - getPaddingRight());
2459 final int viewStart = (mIsVertical ? mTempRect.top : mTempRect.left);
2460 final int viewEnd = (mIsVertical ? mTempRect.bottom : mTempRect.right);
2462 int distance = 0;
2463 if (viewEnd < start) {
2464 distance = start - viewEnd;
2465 } else if (viewStart > end) {
2466 distance = viewStart - end;
2467 }
2469 return distance;
2470 }
2472 private boolean handleKeyScroll(KeyEvent event, int count, int direction) {
2473 boolean handled = false;
2475 if (KeyEventCompat.hasNoModifiers(event)) {
2476 handled = resurrectSelectionIfNeeded();
2477 if (!handled) {
2478 while (count-- > 0) {
2479 if (arrowScroll(direction)) {
2480 handled = true;
2481 } else {
2482 break;
2483 }
2484 }
2485 }
2486 } else if (KeyEventCompat.hasModifiers(event, KeyEvent.META_ALT_ON)) {
2487 handled = resurrectSelectionIfNeeded() || fullScroll(direction);
2488 }
2490 return handled;
2491 }
2493 private boolean handleKeyEvent(int keyCode, int count, KeyEvent event) {
2494 if (mAdapter == null || !mIsAttached) {
2495 return false;
2496 }
2498 if (mDataChanged) {
2499 layoutChildren();
2500 }
2502 boolean handled = false;
2503 final int action = event.getAction();
2505 if (action != KeyEvent.ACTION_UP) {
2506 switch (keyCode) {
2507 case KeyEvent.KEYCODE_DPAD_UP:
2508 if (mIsVertical) {
2509 handled = handleKeyScroll(event, count, View.FOCUS_UP);
2510 } else if (KeyEventCompat.hasNoModifiers(event)) {
2511 handled = handleFocusWithinItem(View.FOCUS_UP);
2512 }
2513 break;
2515 case KeyEvent.KEYCODE_DPAD_DOWN: {
2516 if (mIsVertical) {
2517 handled = handleKeyScroll(event, count, View.FOCUS_DOWN);
2518 } else if (KeyEventCompat.hasNoModifiers(event)) {
2519 handled = handleFocusWithinItem(View.FOCUS_DOWN);
2520 }
2521 break;
2522 }
2524 case KeyEvent.KEYCODE_DPAD_LEFT:
2525 if (!mIsVertical) {
2526 handled = handleKeyScroll(event, count, View.FOCUS_LEFT);
2527 } else if (KeyEventCompat.hasNoModifiers(event)) {
2528 handled = handleFocusWithinItem(View.FOCUS_LEFT);
2529 }
2530 break;
2532 case KeyEvent.KEYCODE_DPAD_RIGHT:
2533 if (!mIsVertical) {
2534 handled = handleKeyScroll(event, count, View.FOCUS_RIGHT);
2535 } else if (KeyEventCompat.hasNoModifiers(event)) {
2536 handled = handleFocusWithinItem(View.FOCUS_RIGHT);
2537 }
2538 break;
2540 case KeyEvent.KEYCODE_DPAD_CENTER:
2541 case KeyEvent.KEYCODE_ENTER:
2542 if (KeyEventCompat.hasNoModifiers(event)) {
2543 handled = resurrectSelectionIfNeeded();
2544 if (!handled
2545 && event.getRepeatCount() == 0 && getChildCount() > 0) {
2546 keyPressed();
2547 handled = true;
2548 }
2549 }
2550 break;
2552 case KeyEvent.KEYCODE_SPACE:
2553 if (KeyEventCompat.hasNoModifiers(event)) {
2554 handled = resurrectSelectionIfNeeded() ||
2555 pageScroll(mIsVertical ? View.FOCUS_DOWN : View.FOCUS_RIGHT);
2556 } else if (KeyEventCompat.hasModifiers(event, KeyEvent.META_SHIFT_ON)) {
2557 handled = resurrectSelectionIfNeeded() ||
2558 fullScroll(mIsVertical ? View.FOCUS_UP : View.FOCUS_LEFT);
2559 }
2561 handled = true;
2562 break;
2564 case KeyEvent.KEYCODE_PAGE_UP:
2565 if (KeyEventCompat.hasNoModifiers(event)) {
2566 handled = resurrectSelectionIfNeeded() ||
2567 pageScroll(mIsVertical ? View.FOCUS_UP : View.FOCUS_LEFT);
2568 } else if (KeyEventCompat.hasModifiers(event, KeyEvent.META_ALT_ON)) {
2569 handled = resurrectSelectionIfNeeded() ||
2570 fullScroll(mIsVertical ? View.FOCUS_UP : View.FOCUS_LEFT);
2571 }
2572 break;
2574 case KeyEvent.KEYCODE_PAGE_DOWN:
2575 if (KeyEventCompat.hasNoModifiers(event)) {
2576 handled = resurrectSelectionIfNeeded() ||
2577 pageScroll(mIsVertical ? View.FOCUS_DOWN : View.FOCUS_RIGHT);
2578 } else if (KeyEventCompat.hasModifiers(event, KeyEvent.META_ALT_ON)) {
2579 handled = resurrectSelectionIfNeeded() ||
2580 fullScroll(mIsVertical ? View.FOCUS_DOWN : View.FOCUS_RIGHT);
2581 }
2582 break;
2584 case KeyEvent.KEYCODE_MOVE_HOME:
2585 if (KeyEventCompat.hasNoModifiers(event)) {
2586 handled = resurrectSelectionIfNeeded() ||
2587 fullScroll(mIsVertical ? View.FOCUS_UP : View.FOCUS_LEFT);
2588 }
2589 break;
2591 case KeyEvent.KEYCODE_MOVE_END:
2592 if (KeyEventCompat.hasNoModifiers(event)) {
2593 handled = resurrectSelectionIfNeeded() ||
2594 fullScroll(mIsVertical ? View.FOCUS_DOWN : View.FOCUS_RIGHT);
2595 }
2596 break;
2597 }
2598 }
2600 if (handled) {
2601 return true;
2602 }
2604 switch (action) {
2605 case KeyEvent.ACTION_DOWN:
2606 return super.onKeyDown(keyCode, event);
2608 case KeyEvent.ACTION_UP:
2609 if (!isEnabled()) {
2610 return true;
2611 }
2613 if (isClickable() && isPressed() &&
2614 mSelectedPosition >= 0 && mAdapter != null &&
2615 mSelectedPosition < mAdapter.getCount()) {
2617 final View child = getChildAt(mSelectedPosition - mFirstPosition);
2618 if (child != null) {
2619 performItemClick(child, mSelectedPosition, mSelectedRowId);
2620 child.setPressed(false);
2621 }
2623 setPressed(false);
2624 return true;
2625 }
2627 return false;
2629 case KeyEvent.ACTION_MULTIPLE:
2630 return super.onKeyMultiple(keyCode, count, event);
2632 default:
2633 return false;
2634 }
2635 }
2637 private void initOrResetVelocityTracker() {
2638 if (mVelocityTracker == null) {
2639 mVelocityTracker = VelocityTracker.obtain();
2640 } else {
2641 mVelocityTracker.clear();
2642 }
2643 }
2645 private void initVelocityTrackerIfNotExists() {
2646 if (mVelocityTracker == null) {
2647 mVelocityTracker = VelocityTracker.obtain();
2648 }
2649 }
2651 private void recycleVelocityTracker() {
2652 if (mVelocityTracker != null) {
2653 mVelocityTracker.recycle();
2654 mVelocityTracker = null;
2655 }
2656 }
2658 /**
2659 * Notify our scroll listener (if there is one) of a change in scroll state
2660 */
2661 private void invokeOnItemScrollListener() {
2662 if (mOnScrollListener != null) {
2663 mOnScrollListener.onScroll(this, mFirstPosition, getChildCount(), mItemCount);
2664 }
2666 // Dummy values, View's implementation does not use these.
2667 onScrollChanged(0, 0, 0, 0);
2668 }
2670 private void reportScrollStateChange(int newState) {
2671 if (newState == mLastScrollState) {
2672 return;
2673 }
2675 if (mOnScrollListener != null) {
2676 mLastScrollState = newState;
2677 mOnScrollListener.onScrollStateChanged(this, newState);
2678 }
2679 }
2681 private boolean maybeStartScrolling(int delta) {
2682 final boolean isOverScroll = (mOverScroll != 0);
2683 if (Math.abs(delta) <= mTouchSlop && !isOverScroll) {
2684 return false;
2685 }
2687 if (isOverScroll) {
2688 mTouchMode = TOUCH_MODE_OVERSCROLL;
2689 } else {
2690 mTouchMode = TOUCH_MODE_DRAGGING;
2691 }
2693 // Time to start stealing events! Once we've stolen them, don't
2694 // let anyone steal from us.
2695 final ViewParent parent = getParent();
2696 if (parent != null) {
2697 parent.requestDisallowInterceptTouchEvent(true);
2698 }
2700 cancelCheckForLongPress();
2702 setPressed(false);
2703 View motionView = getChildAt(mMotionPosition - mFirstPosition);
2704 if (motionView != null) {
2705 motionView.setPressed(false);
2706 }
2708 reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
2710 return true;
2711 }
2713 private void maybeScroll(int delta) {
2714 if (mTouchMode == TOUCH_MODE_DRAGGING) {
2715 handleDragChange(delta);
2716 } else if (mTouchMode == TOUCH_MODE_OVERSCROLL) {
2717 handleOverScrollChange(delta);
2718 }
2719 }
2721 private void handleDragChange(int delta) {
2722 // Time to start stealing events! Once we've stolen them, don't
2723 // let anyone steal from us.
2724 final ViewParent parent = getParent();
2725 if (parent != null) {
2726 parent.requestDisallowInterceptTouchEvent(true);
2727 }
2729 final int motionIndex;
2730 if (mMotionPosition >= 0) {
2731 motionIndex = mMotionPosition - mFirstPosition;
2732 } else {
2733 // If we don't have a motion position that we can reliably track,
2734 // pick something in the middle to make a best guess at things below.
2735 motionIndex = getChildCount() / 2;
2736 }
2738 int motionViewPrevStart = 0;
2739 View motionView = this.getChildAt(motionIndex);
2740 if (motionView != null) {
2741 motionViewPrevStart = (mIsVertical ? motionView.getTop() : motionView.getLeft());
2742 }
2744 boolean atEdge = trackMotionScroll(delta);
2746 motionView = this.getChildAt(motionIndex);
2747 if (motionView != null) {
2748 final int motionViewRealStart =
2749 (mIsVertical ? motionView.getTop() : motionView.getLeft());
2751 if (atEdge) {
2752 final int overscroll = -delta - (motionViewRealStart - motionViewPrevStart);
2753 updateOverScrollState(delta, overscroll);
2754 }
2755 }
2756 }
2758 private void updateOverScrollState(int delta, int overscroll) {
2759 overScrollByInternal((mIsVertical ? 0 : overscroll),
2760 (mIsVertical ? overscroll : 0),
2761 (mIsVertical ? 0 : mOverScroll),
2762 (mIsVertical ? mOverScroll : 0),
2763 0, 0,
2764 (mIsVertical ? 0 : mOverscrollDistance),
2765 (mIsVertical ? mOverscrollDistance : 0),
2766 true);
2768 if (Math.abs(mOverscrollDistance) == Math.abs(mOverScroll)) {
2769 // Break fling velocity if we impacted an edge
2770 if (mVelocityTracker != null) {
2771 mVelocityTracker.clear();
2772 }
2773 }
2775 final int overscrollMode = ViewCompat.getOverScrollMode(this);
2776 if (overscrollMode == ViewCompat.OVER_SCROLL_ALWAYS ||
2777 (overscrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && !contentFits())) {
2778 mTouchMode = TOUCH_MODE_OVERSCROLL;
2780 float pull = (float) overscroll / (mIsVertical ? getHeight() : getWidth());
2781 if (delta > 0) {
2782 mStartEdge.onPull(pull);
2784 if (!mEndEdge.isFinished()) {
2785 mEndEdge.onRelease();
2786 }
2787 } else if (delta < 0) {
2788 mEndEdge.onPull(pull);
2790 if (!mStartEdge.isFinished()) {
2791 mStartEdge.onRelease();
2792 }
2793 }
2795 if (delta != 0) {
2796 ViewCompat.postInvalidateOnAnimation(this);
2797 }
2798 }
2799 }
2801 private void handleOverScrollChange(int delta) {
2802 final int oldOverScroll = mOverScroll;
2803 final int newOverScroll = oldOverScroll - delta;
2805 int overScrollDistance = -delta;
2806 if ((newOverScroll < 0 && oldOverScroll >= 0) ||
2807 (newOverScroll > 0 && oldOverScroll <= 0)) {
2808 overScrollDistance = -oldOverScroll;
2809 delta += overScrollDistance;
2810 } else {
2811 delta = 0;
2812 }
2814 if (overScrollDistance != 0) {
2815 updateOverScrollState(delta, overScrollDistance);
2816 }
2818 if (delta != 0) {
2819 if (mOverScroll != 0) {
2820 mOverScroll = 0;
2821 ViewCompat.postInvalidateOnAnimation(this);
2822 }
2824 trackMotionScroll(delta);
2825 mTouchMode = TOUCH_MODE_DRAGGING;
2827 // We did not scroll the full amount. Treat this essentially like the
2828 // start of a new touch scroll
2829 mMotionPosition = findClosestMotionRowOrColumn((int) mLastTouchPos);
2830 mTouchRemainderPos = 0;
2831 }
2832 }
2834 /**
2835 * What is the distance between the source and destination rectangles given the direction of
2836 * focus navigation between them? The direction basically helps figure out more quickly what is
2837 * self evident by the relationship between the rects...
2838 *
2839 * @param source the source rectangle
2840 * @param dest the destination rectangle
2841 * @param direction the direction
2842 * @return the distance between the rectangles
2843 */
2844 private static int getDistance(Rect source, Rect dest, int direction) {
2845 int sX, sY; // source x, y
2846 int dX, dY; // dest x, y
2848 switch (direction) {
2849 case View.FOCUS_RIGHT:
2850 sX = source.right;
2851 sY = source.top + source.height() / 2;
2852 dX = dest.left;
2853 dY = dest.top + dest.height() / 2;
2854 break;
2856 case View.FOCUS_DOWN:
2857 sX = source.left + source.width() / 2;
2858 sY = source.bottom;
2859 dX = dest.left + dest.width() / 2;
2860 dY = dest.top;
2861 break;
2863 case View.FOCUS_LEFT:
2864 sX = source.left;
2865 sY = source.top + source.height() / 2;
2866 dX = dest.right;
2867 dY = dest.top + dest.height() / 2;
2868 break;
2870 case View.FOCUS_UP:
2871 sX = source.left + source.width() / 2;
2872 sY = source.top;
2873 dX = dest.left + dest.width() / 2;
2874 dY = dest.bottom;
2875 break;
2877 case View.FOCUS_FORWARD:
2878 case View.FOCUS_BACKWARD:
2879 sX = source.right + source.width() / 2;
2880 sY = source.top + source.height() / 2;
2881 dX = dest.left + dest.width() / 2;
2882 dY = dest.top + dest.height() / 2;
2883 break;
2885 default:
2886 throw new IllegalArgumentException("direction must be one of "
2887 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT, "
2888 + "FOCUS_FORWARD, FOCUS_BACKWARD}.");
2889 }
2891 int deltaX = dX - sX;
2892 int deltaY = dY - sY;
2894 return deltaY * deltaY + deltaX * deltaX;
2895 }
2897 private int findMotionRowOrColumn(int motionPos) {
2898 int childCount = getChildCount();
2899 if (childCount == 0) {
2900 return INVALID_POSITION;
2901 }
2903 for (int i = 0; i < childCount; i++) {
2904 View v = getChildAt(i);
2906 if ((mIsVertical && motionPos <= v.getBottom()) ||
2907 (!mIsVertical && motionPos <= v.getRight())) {
2908 return mFirstPosition + i;
2909 }
2910 }
2912 return INVALID_POSITION;
2913 }
2915 private int findClosestMotionRowOrColumn(int motionPos) {
2916 final int childCount = getChildCount();
2917 if (childCount == 0) {
2918 return INVALID_POSITION;
2919 }
2921 final int motionRow = findMotionRowOrColumn(motionPos);
2922 if (motionRow != INVALID_POSITION) {
2923 return motionRow;
2924 } else {
2925 return mFirstPosition + childCount - 1;
2926 }
2927 }
2929 @TargetApi(9)
2930 private int getScaledOverscrollDistance(ViewConfiguration vc) {
2931 if (Build.VERSION.SDK_INT < 9) {
2932 return 0;
2933 }
2935 return vc.getScaledOverscrollDistance();
2936 }
2938 private boolean contentFits() {
2939 final int childCount = getChildCount();
2940 if (childCount == 0) {
2941 return true;
2942 }
2944 if (childCount != mItemCount) {
2945 return false;
2946 }
2948 View first = getChildAt(0);
2949 View last = getChildAt(childCount - 1);
2951 if (mIsVertical) {
2952 return first.getTop() >= getPaddingTop() &&
2953 last.getBottom() <= getHeight() - getPaddingBottom();
2954 } else {
2955 return first.getLeft() >= getPaddingLeft() &&
2956 last.getRight() <= getWidth() - getPaddingRight();
2957 }
2958 }
2960 private void updateScrollbarsDirection() {
2961 setHorizontalScrollBarEnabled(!mIsVertical);
2962 setVerticalScrollBarEnabled(mIsVertical);
2963 }
2965 private void triggerCheckForTap() {
2966 if (mPendingCheckForTap == null) {
2967 mPendingCheckForTap = new CheckForTap();
2968 }
2970 postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
2971 }
2973 private void cancelCheckForTap() {
2974 if (mPendingCheckForTap == null) {
2975 return;
2976 }
2978 removeCallbacks(mPendingCheckForTap);
2979 }
2981 private void triggerCheckForLongPress() {
2982 if (mPendingCheckForLongPress == null) {
2983 mPendingCheckForLongPress = new CheckForLongPress();
2984 }
2986 mPendingCheckForLongPress.rememberWindowAttachCount();
2988 postDelayed(mPendingCheckForLongPress,
2989 ViewConfiguration.getLongPressTimeout());
2990 }
2992 private void cancelCheckForLongPress() {
2993 if (mPendingCheckForLongPress == null) {
2994 return;
2995 }
2997 removeCallbacks(mPendingCheckForLongPress);
2998 }
3000 boolean trackMotionScroll(int incrementalDelta) {
3001 final int childCount = getChildCount();
3002 if (childCount == 0) {
3003 return true;
3004 }
3006 final View first = getChildAt(0);
3007 final int firstStart = (mIsVertical ? first.getTop() : first.getLeft());
3009 final View last = getChildAt(childCount - 1);
3010 final int lastEnd = (mIsVertical ? last.getBottom() : last.getRight());
3012 final int paddingTop = getPaddingTop();
3013 final int paddingBottom = getPaddingBottom();
3014 final int paddingLeft = getPaddingLeft();
3015 final int paddingRight = getPaddingRight();
3017 final int paddingStart = (mIsVertical ? paddingTop : paddingLeft);
3019 final int spaceBefore = paddingStart - firstStart;
3020 final int end = (mIsVertical ? getHeight() - paddingBottom :
3021 getWidth() - paddingRight);
3022 final int spaceAfter = lastEnd - end;
3024 final int size;
3025 if (mIsVertical) {
3026 size = getHeight() - paddingBottom - paddingTop;
3027 } else {
3028 size = getWidth() - paddingRight - paddingLeft;
3029 }
3031 if (incrementalDelta < 0) {
3032 incrementalDelta = Math.max(-(size - 1), incrementalDelta);
3033 } else {
3034 incrementalDelta = Math.min(size - 1, incrementalDelta);
3035 }
3037 final int firstPosition = mFirstPosition;
3039 final boolean cannotScrollDown = (firstPosition == 0 &&
3040 firstStart >= paddingStart && incrementalDelta >= 0);
3041 final boolean cannotScrollUp = (firstPosition + childCount == mItemCount &&
3042 lastEnd <= end && incrementalDelta <= 0);
3044 if (cannotScrollDown || cannotScrollUp) {
3045 return incrementalDelta != 0;
3046 }
3048 final boolean inTouchMode = isInTouchMode();
3049 if (inTouchMode) {
3050 hideSelector();
3051 }
3053 int start = 0;
3054 int count = 0;
3056 final boolean down = (incrementalDelta < 0);
3057 if (down) {
3058 int childrenStart = -incrementalDelta + paddingStart;
3060 for (int i = 0; i < childCount; i++) {
3061 final View child = getChildAt(i);
3062 final int childEnd = (mIsVertical ? child.getBottom() : child.getRight());
3064 if (childEnd >= childrenStart) {
3065 break;
3066 }
3068 count++;
3069 mRecycler.addScrapView(child, firstPosition + i);
3070 }
3071 } else {
3072 int childrenEnd = end - incrementalDelta;
3074 for (int i = childCount - 1; i >= 0; i--) {
3075 final View child = getChildAt(i);
3076 final int childStart = (mIsVertical ? child.getTop() : child.getLeft());
3078 if (childStart <= childrenEnd) {
3079 break;
3080 }
3082 start = i;
3083 count++;
3084 mRecycler.addScrapView(child, firstPosition + i);
3085 }
3086 }
3088 mBlockLayoutRequests = true;
3090 if (count > 0) {
3091 detachViewsFromParent(start, count);
3092 }
3094 // invalidate before moving the children to avoid unnecessary invalidate
3095 // calls to bubble up from the children all the way to the top
3096 if (!awakenScrollbarsInternal()) {
3097 invalidate();
3098 }
3100 offsetChildren(incrementalDelta);
3102 if (down) {
3103 mFirstPosition += count;
3104 }
3106 final int absIncrementalDelta = Math.abs(incrementalDelta);
3107 if (spaceBefore < absIncrementalDelta || spaceAfter < absIncrementalDelta) {
3108 fillGap(down);
3109 }
3111 if (!inTouchMode && mSelectedPosition != INVALID_POSITION) {
3112 final int childIndex = mSelectedPosition - mFirstPosition;
3113 if (childIndex >= 0 && childIndex < getChildCount()) {
3114 positionSelector(mSelectedPosition, getChildAt(childIndex));
3115 }
3116 } else if (mSelectorPosition != INVALID_POSITION) {
3117 final int childIndex = mSelectorPosition - mFirstPosition;
3118 if (childIndex >= 0 && childIndex < getChildCount()) {
3119 positionSelector(INVALID_POSITION, getChildAt(childIndex));
3120 }
3121 } else {
3122 mSelectorRect.setEmpty();
3123 }
3125 mBlockLayoutRequests = false;
3127 invokeOnItemScrollListener();
3129 return false;
3130 }
3132 @TargetApi(14)
3133 private final float getCurrVelocity() {
3134 if (Build.VERSION.SDK_INT >= 14) {
3135 return mScroller.getCurrVelocity();
3136 }
3138 return 0;
3139 }
3141 @TargetApi(5)
3142 private boolean awakenScrollbarsInternal() {
3143 if (Build.VERSION.SDK_INT >= 5) {
3144 return super.awakenScrollBars();
3145 } else {
3146 return false;
3147 }
3148 }
3150 @Override
3151 public void computeScroll() {
3152 if (!mScroller.computeScrollOffset()) {
3153 return;
3154 }
3156 final int pos;
3157 if (mIsVertical) {
3158 pos = mScroller.getCurrY();
3159 } else {
3160 pos = mScroller.getCurrX();
3161 }
3163 final int diff = (int) (pos - mLastTouchPos);
3164 mLastTouchPos = pos;
3166 final boolean stopped = trackMotionScroll(diff);
3168 if (!stopped && !mScroller.isFinished()) {
3169 ViewCompat.postInvalidateOnAnimation(this);
3170 } else {
3171 if (stopped) {
3172 final int overScrollMode = ViewCompat.getOverScrollMode(this);
3173 if (overScrollMode != ViewCompat.OVER_SCROLL_NEVER) {
3174 final EdgeEffectCompat edge =
3175 (diff > 0 ? mStartEdge : mEndEdge);
3177 boolean needsInvalidate =
3178 edge.onAbsorb(Math.abs((int) getCurrVelocity()));
3180 if (needsInvalidate) {
3181 ViewCompat.postInvalidateOnAnimation(this);
3182 }
3183 }
3185 mScroller.abortAnimation();
3186 }
3188 mTouchMode = TOUCH_MODE_REST;
3189 reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
3190 }
3191 }
3193 private void finishEdgeGlows() {
3194 if (mStartEdge != null) {
3195 mStartEdge.finish();
3196 }
3198 if (mEndEdge != null) {
3199 mEndEdge.finish();
3200 }
3201 }
3203 private boolean drawStartEdge(Canvas canvas) {
3204 if (mStartEdge.isFinished()) {
3205 return false;
3206 }
3208 if (mIsVertical) {
3209 return mStartEdge.draw(canvas);
3210 }
3212 final int restoreCount = canvas.save();
3213 final int height = getHeight() - getPaddingTop() - getPaddingBottom();
3215 canvas.translate(0, height);
3216 canvas.rotate(270);
3218 final boolean needsInvalidate = mStartEdge.draw(canvas);
3219 canvas.restoreToCount(restoreCount);
3220 return needsInvalidate;
3221 }
3223 private boolean drawEndEdge(Canvas canvas) {
3224 if (mEndEdge.isFinished()) {
3225 return false;
3226 }
3228 final int restoreCount = canvas.save();
3229 final int width = getWidth() - getPaddingLeft() - getPaddingRight();
3230 final int height = getHeight() - getPaddingTop() - getPaddingBottom();
3232 if (mIsVertical) {
3233 canvas.translate(-width, height);
3234 canvas.rotate(180, width, 0);
3235 } else {
3236 canvas.translate(width, 0);
3237 canvas.rotate(90);
3238 }
3240 final boolean needsInvalidate = mEndEdge.draw(canvas);
3241 canvas.restoreToCount(restoreCount);
3242 return needsInvalidate;
3243 }
3245 private void drawSelector(Canvas canvas) {
3246 if (!mSelectorRect.isEmpty()) {
3247 final Drawable selector = mSelector;
3248 selector.setBounds(mSelectorRect);
3249 selector.draw(canvas);
3250 }
3251 }
3253 private void useDefaultSelector() {
3254 setSelector(getResources().getDrawable(
3255 android.R.drawable.list_selector_background));
3256 }
3258 private boolean shouldShowSelector() {
3259 return (hasFocus() && !isInTouchMode()) || touchModeDrawsInPressedState();
3260 }
3262 private void positionSelector(int position, View selected) {
3263 if (position != INVALID_POSITION) {
3264 mSelectorPosition = position;
3265 }
3267 mSelectorRect.set(selected.getLeft(), selected.getTop(), selected.getRight(),
3268 selected.getBottom());
3270 final boolean isChildViewEnabled = mIsChildViewEnabled;
3271 if (selected.isEnabled() != isChildViewEnabled) {
3272 mIsChildViewEnabled = !isChildViewEnabled;
3274 if (getSelectedItemPosition() != INVALID_POSITION) {
3275 refreshDrawableState();
3276 }
3277 }
3278 }
3280 private void hideSelector() {
3281 if (mSelectedPosition != INVALID_POSITION) {
3282 if (mLayoutMode != LAYOUT_SPECIFIC) {
3283 mResurrectToPosition = mSelectedPosition;
3284 }
3286 if (mNextSelectedPosition >= 0 && mNextSelectedPosition != mSelectedPosition) {
3287 mResurrectToPosition = mNextSelectedPosition;
3288 }
3290 setSelectedPositionInt(INVALID_POSITION);
3291 setNextSelectedPositionInt(INVALID_POSITION);
3293 mSelectedStart = 0;
3294 }
3295 }
3297 private void setSelectedPositionInt(int position) {
3298 mSelectedPosition = position;
3299 mSelectedRowId = getItemIdAtPosition(position);
3300 }
3302 private void setSelectionInt(int position) {
3303 setNextSelectedPositionInt(position);
3304 boolean awakeScrollbars = false;
3306 final int selectedPosition = mSelectedPosition;
3307 if (selectedPosition >= 0) {
3308 if (position == selectedPosition - 1) {
3309 awakeScrollbars = true;
3310 } else if (position == selectedPosition + 1) {
3311 awakeScrollbars = true;
3312 }
3313 }
3315 layoutChildren();
3317 if (awakeScrollbars) {
3318 awakenScrollbarsInternal();
3319 }
3320 }
3322 private void setNextSelectedPositionInt(int position) {
3323 mNextSelectedPosition = position;
3324 mNextSelectedRowId = getItemIdAtPosition(position);
3326 // If we are trying to sync to the selection, update that too
3327 if (mNeedSync && mSyncMode == SYNC_SELECTED_POSITION && position >= 0) {
3328 mSyncPosition = position;
3329 mSyncRowId = mNextSelectedRowId;
3330 }
3331 }
3333 private boolean touchModeDrawsInPressedState() {
3334 switch (mTouchMode) {
3335 case TOUCH_MODE_TAP:
3336 case TOUCH_MODE_DONE_WAITING:
3337 return true;
3338 default:
3339 return false;
3340 }
3341 }
3343 /**
3344 * Sets the selector state to "pressed" and posts a CheckForKeyLongPress to see if
3345 * this is a long press.
3346 */
3347 private void keyPressed() {
3348 if (!isEnabled() || !isClickable()) {
3349 return;
3350 }
3352 final Drawable selector = mSelector;
3353 final Rect selectorRect = mSelectorRect;
3355 if (selector != null && (isFocused() || touchModeDrawsInPressedState())
3356 && !selectorRect.isEmpty()) {
3358 final View child = getChildAt(mSelectedPosition - mFirstPosition);
3360 if (child != null) {
3361 if (child.hasFocusable()) {
3362 return;
3363 }
3365 child.setPressed(true);
3366 }
3368 setPressed(true);
3370 final boolean longClickable = isLongClickable();
3371 final Drawable d = selector.getCurrent();
3372 if (d != null && d instanceof TransitionDrawable) {
3373 if (longClickable) {
3374 ((TransitionDrawable) d).startTransition(
3375 ViewConfiguration.getLongPressTimeout());
3376 } else {
3377 ((TransitionDrawable) d).resetTransition();
3378 }
3379 }
3381 if (longClickable && !mDataChanged) {
3382 if (mPendingCheckForKeyLongPress == null) {
3383 mPendingCheckForKeyLongPress = new CheckForKeyLongPress();
3384 }
3386 mPendingCheckForKeyLongPress.rememberWindowAttachCount();
3387 postDelayed(mPendingCheckForKeyLongPress, ViewConfiguration.getLongPressTimeout());
3388 }
3389 }
3390 }
3392 private void updateSelectorState() {
3393 if (mSelector != null) {
3394 if (shouldShowSelector()) {
3395 mSelector.setState(getDrawableState());
3396 } else {
3397 mSelector.setState(STATE_NOTHING);
3398 }
3399 }
3400 }
3402 private void checkSelectionChanged() {
3403 if ((mSelectedPosition != mOldSelectedPosition) || (mSelectedRowId != mOldSelectedRowId)) {
3404 selectionChanged();
3405 mOldSelectedPosition = mSelectedPosition;
3406 mOldSelectedRowId = mSelectedRowId;
3407 }
3408 }
3410 private void selectionChanged() {
3411 OnItemSelectedListener listener = getOnItemSelectedListener();
3412 if (listener == null) {
3413 return;
3414 }
3416 if (mInLayout || mBlockLayoutRequests) {
3417 // If we are in a layout traversal, defer notification
3418 // by posting. This ensures that the view tree is
3419 // in a consistent state and is able to accommodate
3420 // new layout or invalidate requests.
3421 if (mSelectionNotifier == null) {
3422 mSelectionNotifier = new SelectionNotifier();
3423 }
3425 post(mSelectionNotifier);
3426 } else {
3427 fireOnSelected();
3428 performAccessibilityActionsOnSelected();
3429 }
3430 }
3432 private void fireOnSelected() {
3433 OnItemSelectedListener listener = getOnItemSelectedListener();
3434 if (listener == null) {
3435 return;
3436 }
3438 final int selection = getSelectedItemPosition();
3439 if (selection >= 0) {
3440 View v = getSelectedView();
3441 listener.onItemSelected(this, v, selection,
3442 mAdapter.getItemId(selection));
3443 } else {
3444 listener.onNothingSelected(this);
3445 }
3446 }
3448 private void performAccessibilityActionsOnSelected() {
3449 final int position = getSelectedItemPosition();
3450 if (position >= 0) {
3451 // We fire selection events here not in View
3452 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
3453 }
3454 }
3456 private int lookForSelectablePosition(int position) {
3457 return lookForSelectablePosition(position, true);
3458 }
3460 private int lookForSelectablePosition(int position, boolean lookDown) {
3461 final ListAdapter adapter = mAdapter;
3462 if (adapter == null || isInTouchMode()) {
3463 return INVALID_POSITION;
3464 }
3466 final int itemCount = mItemCount;
3467 if (!mAreAllItemsSelectable) {
3468 if (lookDown) {
3469 position = Math.max(0, position);
3470 while (position < itemCount && !adapter.isEnabled(position)) {
3471 position++;
3472 }
3473 } else {
3474 position = Math.min(position, itemCount - 1);
3475 while (position >= 0 && !adapter.isEnabled(position)) {
3476 position--;
3477 }
3478 }
3480 if (position < 0 || position >= itemCount) {
3481 return INVALID_POSITION;
3482 }
3484 return position;
3485 } else {
3486 if (position < 0 || position >= itemCount) {
3487 return INVALID_POSITION;
3488 }
3490 return position;
3491 }
3492 }
3494 /**
3495 * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN} or
3496 * {@link View#FOCUS_LEFT} or {@link View#FOCUS_RIGHT} depending on the
3497 * current view orientation.
3498 *
3499 * @return The position of the next selectable position of the views that
3500 * are currently visible, taking into account the fact that there might
3501 * be no selection. Returns {@link #INVALID_POSITION} if there is no
3502 * selectable view on screen in the given direction.
3503 */
3504 private int lookForSelectablePositionOnScreen(int direction) {
3505 forceValidFocusDirection(direction);
3507 final int firstPosition = mFirstPosition;
3508 final ListAdapter adapter = getAdapter();
3510 if (direction == View.FOCUS_DOWN || direction == View.FOCUS_RIGHT) {
3511 int startPos = (mSelectedPosition != INVALID_POSITION ?
3512 mSelectedPosition + 1 : firstPosition);
3514 if (startPos >= adapter.getCount()) {
3515 return INVALID_POSITION;
3516 }
3518 if (startPos < firstPosition) {
3519 startPos = firstPosition;
3520 }
3522 final int lastVisiblePos = getLastVisiblePosition();
3524 for (int pos = startPos; pos <= lastVisiblePos; pos++) {
3525 if (adapter.isEnabled(pos)
3526 && getChildAt(pos - firstPosition).getVisibility() == View.VISIBLE) {
3527 return pos;
3528 }
3529 }
3530 } else {
3531 final int last = firstPosition + getChildCount() - 1;
3533 int startPos = (mSelectedPosition != INVALID_POSITION) ?
3534 mSelectedPosition - 1 : firstPosition + getChildCount() - 1;
3536 if (startPos < 0 || startPos >= adapter.getCount()) {
3537 return INVALID_POSITION;
3538 }
3540 if (startPos > last) {
3541 startPos = last;
3542 }
3544 for (int pos = startPos; pos >= firstPosition; pos--) {
3545 if (adapter.isEnabled(pos)
3546 && getChildAt(pos - firstPosition).getVisibility() == View.VISIBLE) {
3547 return pos;
3548 }
3549 }
3550 }
3552 return INVALID_POSITION;
3553 }
3555 @Override
3556 protected void drawableStateChanged() {
3557 super.drawableStateChanged();
3558 updateSelectorState();
3559 }
3561 @Override
3562 protected int[] onCreateDrawableState(int extraSpace) {
3563 // If the child view is enabled then do the default behavior.
3564 if (mIsChildViewEnabled) {
3565 // Common case
3566 return super.onCreateDrawableState(extraSpace);
3567 }
3569 // The selector uses this View's drawable state. The selected child view
3570 // is disabled, so we need to remove the enabled state from the drawable
3571 // states.
3572 final int enabledState = ENABLED_STATE_SET[0];
3574 // If we don't have any extra space, it will return one of the static state arrays,
3575 // and clearing the enabled state on those arrays is a bad thing! If we specify
3576 // we need extra space, it will create+copy into a new array that safely mutable.
3577 int[] state = super.onCreateDrawableState(extraSpace + 1);
3578 int enabledPos = -1;
3579 for (int i = state.length - 1; i >= 0; i--) {
3580 if (state[i] == enabledState) {
3581 enabledPos = i;
3582 break;
3583 }
3584 }
3586 // Remove the enabled state
3587 if (enabledPos >= 0) {
3588 System.arraycopy(state, enabledPos + 1, state, enabledPos,
3589 state.length - enabledPos - 1);
3590 }
3592 return state;
3593 }
3595 @Override
3596 protected boolean canAnimate() {
3597 return (super.canAnimate() && mItemCount > 0);
3598 }
3600 @Override
3601 protected void dispatchDraw(Canvas canvas) {
3602 final boolean drawSelectorOnTop = mDrawSelectorOnTop;
3603 if (!drawSelectorOnTop) {
3604 drawSelector(canvas);
3605 }
3607 super.dispatchDraw(canvas);
3609 if (drawSelectorOnTop) {
3610 drawSelector(canvas);
3611 }
3612 }
3614 @Override
3615 public void draw(Canvas canvas) {
3616 super.draw(canvas);
3618 boolean needsInvalidate = false;
3620 if (mStartEdge != null) {
3621 needsInvalidate |= drawStartEdge(canvas);
3622 }
3624 if (mEndEdge != null) {
3625 needsInvalidate |= drawEndEdge(canvas);
3626 }
3628 if (needsInvalidate) {
3629 ViewCompat.postInvalidateOnAnimation(this);
3630 }
3631 }
3633 @Override
3634 public void requestLayout() {
3635 if (!mInLayout && !mBlockLayoutRequests) {
3636 super.requestLayout();
3637 }
3638 }
3640 @Override
3641 public View getSelectedView() {
3642 if (mItemCount > 0 && mSelectedPosition >= 0) {
3643 return getChildAt(mSelectedPosition - mFirstPosition);
3644 } else {
3645 return null;
3646 }
3647 }
3649 @Override
3650 public void setSelection(int position) {
3651 setSelectionFromOffset(position, 0);
3652 }
3654 public void setSelectionFromOffset(int position, int offset) {
3655 if (mAdapter == null) {
3656 return;
3657 }
3659 if (!isInTouchMode()) {
3660 position = lookForSelectablePosition(position);
3661 if (position >= 0) {
3662 setNextSelectedPositionInt(position);
3663 }
3664 } else {
3665 mResurrectToPosition = position;
3666 }
3668 if (position >= 0) {
3669 mLayoutMode = LAYOUT_SPECIFIC;
3671 if (mIsVertical) {
3672 mSpecificStart = getPaddingTop() + offset;
3673 } else {
3674 mSpecificStart = getPaddingLeft() + offset;
3675 }
3677 if (mNeedSync) {
3678 mSyncPosition = position;
3679 mSyncRowId = mAdapter.getItemId(position);
3680 }
3682 requestLayout();
3683 }
3684 }
3686 @Override
3687 public boolean dispatchKeyEvent(KeyEvent event) {
3688 // Dispatch in the normal way
3689 boolean handled = super.dispatchKeyEvent(event);
3690 if (!handled) {
3691 // If we didn't handle it...
3692 final View focused = getFocusedChild();
3693 if (focused != null && event.getAction() == KeyEvent.ACTION_DOWN) {
3694 // ... and our focused child didn't handle it
3695 // ... give it to ourselves so we can scroll if necessary
3696 handled = onKeyDown(event.getKeyCode(), event);
3697 }
3698 }
3700 return handled;
3701 }
3703 @Override
3704 protected void dispatchSetPressed(boolean pressed) {
3705 // Don't dispatch setPressed to our children. We call setPressed on ourselves to
3706 // get the selector in the right state, but we don't want to press each child.
3707 }
3709 @Override
3710 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
3711 if (mSelector == null) {
3712 useDefaultSelector();
3713 }
3715 int widthMode = MeasureSpec.getMode(widthMeasureSpec);
3716 int heightMode = MeasureSpec.getMode(heightMeasureSpec);
3717 int widthSize = MeasureSpec.getSize(widthMeasureSpec);
3718 int heightSize = MeasureSpec.getSize(heightMeasureSpec);
3720 int childWidth = 0;
3721 int childHeight = 0;
3723 mItemCount = (mAdapter == null ? 0 : mAdapter.getCount());
3724 if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED ||
3725 heightMode == MeasureSpec.UNSPECIFIED)) {
3726 final View child = obtainView(0, mIsScrap);
3728 final int secondaryMeasureSpec =
3729 (mIsVertical ? widthMeasureSpec : heightMeasureSpec);
3731 measureScrapChild(child, 0, secondaryMeasureSpec);
3733 childWidth = child.getMeasuredWidth();
3734 childHeight = child.getMeasuredHeight();
3736 if (recycleOnMeasure()) {
3737 mRecycler.addScrapView(child, -1);
3738 }
3739 }
3741 if (widthMode == MeasureSpec.UNSPECIFIED) {
3742 widthSize = getPaddingLeft() + getPaddingRight() + childWidth;
3743 if (mIsVertical) {
3744 widthSize += getVerticalScrollbarWidth();
3745 }
3746 }
3748 if (heightMode == MeasureSpec.UNSPECIFIED) {
3749 heightSize = getPaddingTop() + getPaddingBottom() + childHeight;
3750 if (!mIsVertical) {
3751 heightSize += getHorizontalScrollbarHeight();
3752 }
3753 }
3755 if (mIsVertical && heightMode == MeasureSpec.AT_MOST) {
3756 heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
3757 }
3759 if (!mIsVertical && widthMode == MeasureSpec.AT_MOST) {
3760 widthSize = measureWidthOfChildren(heightMeasureSpec, 0, NO_POSITION, widthSize, -1);
3761 }
3763 setMeasuredDimension(widthSize, heightSize);
3764 }
3766 @Override
3767 protected void onLayout(boolean changed, int l, int t, int r, int b) {
3768 mInLayout = true;
3770 if (changed) {
3771 final int childCount = getChildCount();
3772 for (int i = 0; i < childCount; i++) {
3773 getChildAt(i).forceLayout();
3774 }
3776 mRecycler.markChildrenDirty();
3777 }
3779 layoutChildren();
3781 mInLayout = false;
3783 final int width = r - l - getPaddingLeft() - getPaddingRight();
3784 final int height = b - t - getPaddingTop() - getPaddingBottom();
3786 if (mStartEdge != null && mEndEdge != null) {
3787 if (mIsVertical) {
3788 mStartEdge.setSize(width, height);
3789 mEndEdge.setSize(width, height);
3790 } else {
3791 mStartEdge.setSize(height, width);
3792 mEndEdge.setSize(height, width);
3793 }
3794 }
3795 }
3797 private void layoutChildren() {
3798 if (getWidth() == 0 || getHeight() == 0) {
3799 return;
3800 }
3802 final boolean blockLayoutRequests = mBlockLayoutRequests;
3803 if (!blockLayoutRequests) {
3804 mBlockLayoutRequests = true;
3805 } else {
3806 return;
3807 }
3809 try {
3810 invalidate();
3812 if (mAdapter == null) {
3813 resetState();
3814 return;
3815 }
3817 final int start = (mIsVertical ? getPaddingTop() : getPaddingLeft());
3818 final int end =
3819 (mIsVertical ? getHeight() - getPaddingBottom() : getWidth() - getPaddingRight());
3821 int childCount = getChildCount();
3822 int index = 0;
3823 int delta = 0;
3825 View focusLayoutRestoreView = null;
3827 View selected = null;
3828 View oldSelected = null;
3829 View newSelected = null;
3830 View oldFirstChild = null;
3832 switch (mLayoutMode) {
3833 case LAYOUT_SET_SELECTION:
3834 index = mNextSelectedPosition - mFirstPosition;
3835 if (index >= 0 && index < childCount) {
3836 newSelected = getChildAt(index);
3837 }
3839 break;
3841 case LAYOUT_FORCE_TOP:
3842 case LAYOUT_FORCE_BOTTOM:
3843 case LAYOUT_SPECIFIC:
3844 case LAYOUT_SYNC:
3845 break;
3847 case LAYOUT_MOVE_SELECTION:
3848 default:
3849 // Remember the previously selected view
3850 index = mSelectedPosition - mFirstPosition;
3851 if (index >= 0 && index < childCount) {
3852 oldSelected = getChildAt(index);
3853 }
3855 // Remember the previous first child
3856 oldFirstChild = getChildAt(0);
3858 if (mNextSelectedPosition >= 0) {
3859 delta = mNextSelectedPosition - mSelectedPosition;
3860 }
3862 // Caution: newSelected might be null
3863 newSelected = getChildAt(index + delta);
3864 }
3866 final boolean dataChanged = mDataChanged;
3867 if (dataChanged) {
3868 handleDataChanged();
3869 }
3871 // Handle the empty set by removing all views that are visible
3872 // and calling it a day
3873 if (mItemCount == 0) {
3874 resetState();
3875 return;
3876 } else if (mItemCount != mAdapter.getCount()) {
3877 throw new IllegalStateException("The content of the adapter has changed but "
3878 + "TwoWayView did not receive a notification. Make sure the content of "
3879 + "your adapter is not modified from a background thread, but only "
3880 + "from the UI thread. [in TwoWayView(" + getId() + ", " + getClass()
3881 + ") with Adapter(" + mAdapter.getClass() + ")]");
3882 }
3884 setSelectedPositionInt(mNextSelectedPosition);
3886 // Reset the focus restoration
3887 View focusLayoutRestoreDirectChild = null;
3889 // Pull all children into the RecycleBin.
3890 // These views will be reused if possible
3891 final int firstPosition = mFirstPosition;
3892 final RecycleBin recycleBin = mRecycler;
3894 if (dataChanged) {
3895 for (int i = 0; i < childCount; i++) {
3896 recycleBin.addScrapView(getChildAt(i), firstPosition + i);
3897 }
3898 } else {
3899 recycleBin.fillActiveViews(childCount, firstPosition);
3900 }
3902 // Take focus back to us temporarily to avoid the eventual
3903 // call to clear focus when removing the focused child below
3904 // from messing things up when ViewAncestor assigns focus back
3905 // to someone else.
3906 final View focusedChild = getFocusedChild();
3907 if (focusedChild != null) {
3908 // We can remember the focused view to restore after relayout if the
3909 // data hasn't changed, or if the focused position is a header or footer.
3910 if (!dataChanged) {
3911 focusLayoutRestoreDirectChild = focusedChild;
3913 // Remember the specific view that had focus
3914 focusLayoutRestoreView = findFocus();
3915 if (focusLayoutRestoreView != null) {
3916 // Tell it we are going to mess with it
3917 focusLayoutRestoreView.onStartTemporaryDetach();
3918 }
3919 }
3921 requestFocus();
3922 }
3924 // FIXME: We need a way to save current accessibility focus here
3925 // so that it can be restored after we re-attach the children on each
3926 // layout round.
3928 detachAllViewsFromParent();
3930 switch (mLayoutMode) {
3931 case LAYOUT_SET_SELECTION:
3932 if (newSelected != null) {
3933 final int newSelectedStart =
3934 (mIsVertical ? newSelected.getTop() : newSelected.getLeft());
3936 selected = fillFromSelection(newSelectedStart, start, end);
3937 } else {
3938 selected = fillFromMiddle(start, end);
3939 }
3941 break;
3943 case LAYOUT_SYNC:
3944 selected = fillSpecific(mSyncPosition, mSpecificStart);
3945 break;
3947 case LAYOUT_FORCE_BOTTOM:
3948 selected = fillBefore(mItemCount - 1, end);
3949 adjustViewsStartOrEnd();
3950 break;
3952 case LAYOUT_FORCE_TOP:
3953 mFirstPosition = 0;
3954 selected = fillFromOffset(start);
3955 adjustViewsStartOrEnd();
3956 break;
3958 case LAYOUT_SPECIFIC:
3959 selected = fillSpecific(reconcileSelectedPosition(), mSpecificStart);
3960 break;
3962 case LAYOUT_MOVE_SELECTION:
3963 selected = moveSelection(oldSelected, newSelected, delta, start, end);
3964 break;
3966 default:
3967 if (childCount == 0) {
3968 final int position = lookForSelectablePosition(0);
3969 setSelectedPositionInt(position);
3970 selected = fillFromOffset(start);
3971 } else {
3972 if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) {
3973 int offset = start;
3974 if (oldSelected != null) {
3975 offset = (mIsVertical ? oldSelected.getTop() : oldSelected.getLeft());
3976 }
3977 selected = fillSpecific(mSelectedPosition, offset);
3978 } else if (mFirstPosition < mItemCount) {
3979 int offset = start;
3980 if (oldFirstChild != null) {
3981 offset = (mIsVertical ? oldFirstChild.getTop() : oldFirstChild.getLeft());
3982 }
3984 selected = fillSpecific(mFirstPosition, offset);
3985 } else {
3986 selected = fillSpecific(0, start);
3987 }
3988 }
3990 break;
3992 }
3994 recycleBin.scrapActiveViews();
3996 if (selected != null) {
3997 if (mItemsCanFocus && hasFocus() && !selected.hasFocus()) {
3998 final boolean focusWasTaken = (selected == focusLayoutRestoreDirectChild &&
3999 focusLayoutRestoreView != null &&
4000 focusLayoutRestoreView.requestFocus()) || selected.requestFocus();
4002 if (!focusWasTaken) {
4003 // Selected item didn't take focus, fine, but still want
4004 // to make sure something else outside of the selected view
4005 // has focus
4006 final View focused = getFocusedChild();
4007 if (focused != null) {
4008 focused.clearFocus();
4009 }
4011 positionSelector(INVALID_POSITION, selected);
4012 } else {
4013 selected.setSelected(false);
4014 mSelectorRect.setEmpty();
4015 }
4016 } else {
4017 positionSelector(INVALID_POSITION, selected);
4018 }
4020 mSelectedStart = (mIsVertical ? selected.getTop() : selected.getLeft());
4021 } else {
4022 if (mTouchMode > TOUCH_MODE_DOWN && mTouchMode < TOUCH_MODE_DRAGGING) {
4023 View child = getChildAt(mMotionPosition - mFirstPosition);
4025 if (child != null) {
4026 positionSelector(mMotionPosition, child);
4027 }
4028 } else {
4029 mSelectedStart = 0;
4030 mSelectorRect.setEmpty();
4031 }
4033 // Even if there is not selected position, we may need to restore
4034 // focus (i.e. something focusable in touch mode)
4035 if (hasFocus() && focusLayoutRestoreView != null) {
4036 focusLayoutRestoreView.requestFocus();
4037 }
4038 }
4040 // Tell focus view we are done mucking with it, if it is still in
4041 // our view hierarchy.
4042 if (focusLayoutRestoreView != null
4043 && focusLayoutRestoreView.getWindowToken() != null) {
4044 focusLayoutRestoreView.onFinishTemporaryDetach();
4045 }
4047 mLayoutMode = LAYOUT_NORMAL;
4048 mDataChanged = false;
4049 mNeedSync = false;
4051 setNextSelectedPositionInt(mSelectedPosition);
4052 if (mItemCount > 0) {
4053 checkSelectionChanged();
4054 }
4056 invokeOnItemScrollListener();
4057 } finally {
4058 if (!blockLayoutRequests) {
4059 mBlockLayoutRequests = false;
4060 mDataChanged = false;
4061 }
4062 }
4063 }
4065 protected boolean recycleOnMeasure() {
4066 return true;
4067 }
4069 private void offsetChildren(int offset) {
4070 final int childCount = getChildCount();
4072 for (int i = 0; i < childCount; i++) {
4073 final View child = getChildAt(i);
4075 if (mIsVertical) {
4076 child.offsetTopAndBottom(offset);
4077 } else {
4078 child.offsetLeftAndRight(offset);
4079 }
4080 }
4081 }
4083 private View moveSelection(View oldSelected, View newSelected, int delta, int start,
4084 int end) {
4085 final int selectedPosition = mSelectedPosition;
4087 final int oldSelectedStart = (mIsVertical ? oldSelected.getTop() : oldSelected.getLeft());
4088 final int oldSelectedEnd = (mIsVertical ? oldSelected.getBottom() : oldSelected.getRight());
4090 View selected = null;
4092 if (delta > 0) {
4093 /*
4094 * Case 1: Scrolling down.
4095 */
4097 /*
4098 * Before After
4099 * | | | |
4100 * +-------+ +-------+
4101 * | A | | A |
4102 * | 1 | => +-------+
4103 * +-------+ | B |
4104 * | B | | 2 |
4105 * +-------+ +-------+
4106 * | | | |
4107 *
4108 * Try to keep the top of the previously selected item where it was.
4109 * oldSelected = A
4110 * selected = B
4111 */
4113 // Put oldSelected (A) where it belongs
4114 oldSelected = makeAndAddView(selectedPosition - 1, oldSelectedStart, true, false);
4116 final int itemMargin = mItemMargin;
4118 // Now put the new selection (B) below that
4119 selected = makeAndAddView(selectedPosition, oldSelectedEnd + itemMargin, true, true);
4121 final int selectedStart = (mIsVertical ? selected.getTop() : selected.getLeft());
4122 final int selectedEnd = (mIsVertical ? selected.getBottom() : selected.getRight());
4124 // Some of the newly selected item extends below the bottom of the list
4125 if (selectedEnd > end) {
4126 // Find space available above the selection into which we can scroll upwards
4127 final int spaceBefore = selectedStart - start;
4129 // Find space required to bring the bottom of the selected item fully into view
4130 final int spaceAfter = selectedEnd - end;
4132 // Don't scroll more than half the size of the list
4133 final int halfSpace = (end - start) / 2;
4134 int offset = Math.min(spaceBefore, spaceAfter);
4135 offset = Math.min(offset, halfSpace);
4137 if (mIsVertical) {
4138 oldSelected.offsetTopAndBottom(-offset);
4139 selected.offsetTopAndBottom(-offset);
4140 } else {
4141 oldSelected.offsetLeftAndRight(-offset);
4142 selected.offsetLeftAndRight(-offset);
4143 }
4144 }
4146 // Fill in views before and after
4147 fillBefore(mSelectedPosition - 2, selectedStart - itemMargin);
4148 adjustViewsStartOrEnd();
4149 fillAfter(mSelectedPosition + 1, selectedEnd + itemMargin);
4150 } else if (delta < 0) {
4151 /*
4152 * Case 2: Scrolling up.
4153 */
4155 /*
4156 * Before After
4157 * | | | |
4158 * +-------+ +-------+
4159 * | A | | A |
4160 * +-------+ => | 1 |
4161 * | B | +-------+
4162 * | 2 | | B |
4163 * +-------+ +-------+
4164 * | | | |
4165 *
4166 * Try to keep the top of the item about to become selected where it was.
4167 * newSelected = A
4168 * olSelected = B
4169 */
4171 if (newSelected != null) {
4172 // Try to position the top of newSel (A) where it was before it was selected
4173 final int newSelectedStart = (mIsVertical ? newSelected.getTop() : newSelected.getLeft());
4174 selected = makeAndAddView(selectedPosition, newSelectedStart, true, true);
4175 } else {
4176 // If (A) was not on screen and so did not have a view, position
4177 // it above the oldSelected (B)
4178 selected = makeAndAddView(selectedPosition, oldSelectedStart, false, true);
4179 }
4181 final int selectedStart = (mIsVertical ? selected.getTop() : selected.getLeft());
4182 final int selectedEnd = (mIsVertical ? selected.getBottom() : selected.getRight());
4184 // Some of the newly selected item extends above the top of the list
4185 if (selectedStart < start) {
4186 // Find space required to bring the top of the selected item fully into view
4187 final int spaceBefore = start - selectedStart;
4189 // Find space available below the selection into which we can scroll downwards
4190 final int spaceAfter = end - selectedEnd;
4192 // Don't scroll more than half the height of the list
4193 final int halfSpace = (end - start) / 2;
4194 int offset = Math.min(spaceBefore, spaceAfter);
4195 offset = Math.min(offset, halfSpace);
4197 if (mIsVertical) {
4198 selected.offsetTopAndBottom(offset);
4199 } else {
4200 selected.offsetLeftAndRight(offset);
4201 }
4202 }
4204 // Fill in views above and below
4205 fillBeforeAndAfter(selected, selectedPosition);
4206 } else {
4207 /*
4208 * Case 3: Staying still
4209 */
4211 selected = makeAndAddView(selectedPosition, oldSelectedStart, true, true);
4213 final int selectedStart = (mIsVertical ? selected.getTop() : selected.getLeft());
4214 final int selectedEnd = (mIsVertical ? selected.getBottom() : selected.getRight());
4216 // We're staying still...
4217 if (oldSelectedStart < start) {
4218 // ... but the top of the old selection was off screen.
4219 // (This can happen if the data changes size out from under us)
4220 int newEnd = selectedEnd;
4221 if (newEnd < start + 20) {
4222 // Not enough visible -- bring it onscreen
4223 if (mIsVertical) {
4224 selected.offsetTopAndBottom(start - selectedStart);
4225 } else {
4226 selected.offsetLeftAndRight(start - selectedStart);
4227 }
4228 }
4229 }
4231 // Fill in views above and below
4232 fillBeforeAndAfter(selected, selectedPosition);
4233 }
4235 return selected;
4236 }
4238 void confirmCheckedPositionsById() {
4239 // Clear out the positional check states, we'll rebuild it below from IDs.
4240 mCheckStates.clear();
4242 for (int checkedIndex = 0; checkedIndex < mCheckedIdStates.size(); checkedIndex++) {
4243 final long id = mCheckedIdStates.keyAt(checkedIndex);
4244 final int lastPos = mCheckedIdStates.valueAt(checkedIndex);
4246 final long lastPosId = mAdapter.getItemId(lastPos);
4247 if (id != lastPosId) {
4248 // Look around to see if the ID is nearby. If not, uncheck it.
4249 final int start = Math.max(0, lastPos - CHECK_POSITION_SEARCH_DISTANCE);
4250 final int end = Math.min(lastPos + CHECK_POSITION_SEARCH_DISTANCE, mItemCount);
4251 boolean found = false;
4253 for (int searchPos = start; searchPos < end; searchPos++) {
4254 final long searchId = mAdapter.getItemId(searchPos);
4255 if (id == searchId) {
4256 found = true;
4257 mCheckStates.put(searchPos, true);
4258 mCheckedIdStates.setValueAt(checkedIndex, searchPos);
4259 break;
4260 }
4261 }
4263 if (!found) {
4264 mCheckedIdStates.delete(id);
4265 checkedIndex--;
4266 mCheckedItemCount--;
4267 }
4268 } else {
4269 mCheckStates.put(lastPos, true);
4270 }
4271 }
4272 }
4274 private void handleDataChanged() {
4275 if (mChoiceMode.compareTo(ChoiceMode.NONE) != 0 && mAdapter != null && mAdapter.hasStableIds()) {
4276 confirmCheckedPositionsById();
4277 }
4279 mRecycler.clearTransientStateViews();
4281 final int itemCount = mItemCount;
4282 if (itemCount > 0) {
4283 int newPos;
4284 int selectablePos;
4286 // Find the row we are supposed to sync to
4287 if (mNeedSync) {
4288 // Update this first, since setNextSelectedPositionInt inspects it
4289 mNeedSync = false;
4290 mPendingSync = null;
4292 switch (mSyncMode) {
4293 case SYNC_SELECTED_POSITION:
4294 if (isInTouchMode()) {
4295 // We saved our state when not in touch mode. (We know this because
4296 // mSyncMode is SYNC_SELECTED_POSITION.) Now we are trying to
4297 // restore in touch mode. Just leave mSyncPosition as it is (possibly
4298 // adjusting if the available range changed) and return.
4299 mLayoutMode = LAYOUT_SYNC;
4300 mSyncPosition = Math.min(Math.max(0, mSyncPosition), itemCount - 1);
4302 return;
4303 } else {
4304 // See if we can find a position in the new data with the same
4305 // id as the old selection. This will change mSyncPosition.
4306 newPos = findSyncPosition();
4307 if (newPos >= 0) {
4308 // Found it. Now verify that new selection is still selectable
4309 selectablePos = lookForSelectablePosition(newPos, true);
4310 if (selectablePos == newPos) {
4311 // Same row id is selected
4312 mSyncPosition = newPos;
4314 if (mSyncHeight == getHeight()) {
4315 // If we are at the same height as when we saved state, try
4316 // to restore the scroll position too.
4317 mLayoutMode = LAYOUT_SYNC;
4318 } else {
4319 // We are not the same height as when the selection was saved, so
4320 // don't try to restore the exact position
4321 mLayoutMode = LAYOUT_SET_SELECTION;
4322 }
4324 // Restore selection
4325 setNextSelectedPositionInt(newPos);
4326 return;
4327 }
4328 }
4329 }
4330 break;
4332 case SYNC_FIRST_POSITION:
4333 // Leave mSyncPosition as it is -- just pin to available range
4334 mLayoutMode = LAYOUT_SYNC;
4335 mSyncPosition = Math.min(Math.max(0, mSyncPosition), itemCount - 1);
4337 return;
4338 }
4339 }
4341 if (!isInTouchMode()) {
4342 // We couldn't find matching data -- try to use the same position
4343 newPos = getSelectedItemPosition();
4345 // Pin position to the available range
4346 if (newPos >= itemCount) {
4347 newPos = itemCount - 1;
4348 }
4349 if (newPos < 0) {
4350 newPos = 0;
4351 }
4353 // Make sure we select something selectable -- first look down
4354 selectablePos = lookForSelectablePosition(newPos, true);
4356 if (selectablePos >= 0) {
4357 setNextSelectedPositionInt(selectablePos);
4358 return;
4359 } else {
4360 // Looking down didn't work -- try looking up
4361 selectablePos = lookForSelectablePosition(newPos, false);
4362 if (selectablePos >= 0) {
4363 setNextSelectedPositionInt(selectablePos);
4364 return;
4365 }
4366 }
4367 } else {
4368 // We already know where we want to resurrect the selection
4369 if (mResurrectToPosition >= 0) {
4370 return;
4371 }
4372 }
4373 }
4375 // Nothing is selected. Give up and reset everything.
4376 mLayoutMode = LAYOUT_FORCE_TOP;
4377 mSelectedPosition = INVALID_POSITION;
4378 mSelectedRowId = INVALID_ROW_ID;
4379 mNextSelectedPosition = INVALID_POSITION;
4380 mNextSelectedRowId = INVALID_ROW_ID;
4381 mNeedSync = false;
4382 mPendingSync = null;
4383 mSelectorPosition = INVALID_POSITION;
4385 checkSelectionChanged();
4386 }
4388 private int reconcileSelectedPosition() {
4389 int position = mSelectedPosition;
4390 if (position < 0) {
4391 position = mResurrectToPosition;
4392 }
4394 position = Math.max(0, position);
4395 position = Math.min(position, mItemCount - 1);
4397 return position;
4398 }
4400 boolean resurrectSelection() {
4401 final int childCount = getChildCount();
4402 if (childCount <= 0) {
4403 return false;
4404 }
4406 int selectedStart = 0;
4407 int selectedPosition;
4409 final int start = (mIsVertical ? getPaddingTop() : getPaddingLeft());
4410 final int end =
4411 (mIsVertical ? getHeight() - getPaddingBottom() : getWidth() - getPaddingRight());
4413 final int firstPosition = mFirstPosition;
4414 final int toPosition = mResurrectToPosition;
4415 boolean down = true;
4417 if (toPosition >= firstPosition && toPosition < firstPosition + childCount) {
4418 selectedPosition = toPosition;
4420 final View selected = getChildAt(selectedPosition - mFirstPosition);
4421 selectedStart = (mIsVertical ? selected.getTop() : selected.getLeft());
4422 } else if (toPosition < firstPosition) {
4423 // Default to selecting whatever is first
4424 selectedPosition = firstPosition;
4426 for (int i = 0; i < childCount; i++) {
4427 final View child = getChildAt(i);
4428 final int childStart = (mIsVertical ? child.getTop() : child.getLeft());
4430 if (i == 0) {
4431 // Remember the position of the first item
4432 selectedStart = childStart;
4433 }
4435 if (childStart >= start) {
4436 // Found a view whose top is fully visible
4437 selectedPosition = firstPosition + i;
4438 selectedStart = childStart;
4439 break;
4440 }
4441 }
4442 } else {
4443 selectedPosition = firstPosition + childCount - 1;
4444 down = false;
4446 for (int i = childCount - 1; i >= 0; i--) {
4447 final View child = getChildAt(i);
4448 final int childStart = (mIsVertical ? child.getTop() : child.getLeft());
4449 final int childEnd = (mIsVertical ? child.getBottom() : child.getRight());
4451 if (i == childCount - 1) {
4452 selectedStart = childStart;
4453 }
4455 if (childEnd <= end) {
4456 selectedPosition = firstPosition + i;
4457 selectedStart = childStart;
4458 break;
4459 }
4460 }
4461 }
4463 mResurrectToPosition = INVALID_POSITION;
4464 mTouchMode = TOUCH_MODE_REST;
4465 reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
4467 mSpecificStart = selectedStart;
4469 selectedPosition = lookForSelectablePosition(selectedPosition, down);
4470 if (selectedPosition >= firstPosition && selectedPosition <= getLastVisiblePosition()) {
4471 mLayoutMode = LAYOUT_SPECIFIC;
4472 updateSelectorState();
4473 setSelectionInt(selectedPosition);
4474 invokeOnItemScrollListener();
4475 } else {
4476 selectedPosition = INVALID_POSITION;
4477 }
4479 return selectedPosition >= 0;
4480 }
4482 /**
4483 * If there is a selection returns false.
4484 * Otherwise resurrects the selection and returns true if resurrected.
4485 */
4486 boolean resurrectSelectionIfNeeded() {
4487 if (mSelectedPosition < 0 && resurrectSelection()) {
4488 updateSelectorState();
4489 return true;
4490 }
4492 return false;
4493 }
4495 private int getChildWidthMeasureSpec(LayoutParams lp) {
4496 if (!mIsVertical && lp.width == LayoutParams.WRAP_CONTENT) {
4497 return MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
4498 } else if (mIsVertical) {
4499 final int maxWidth = getWidth() - getPaddingLeft() - getPaddingRight();
4500 return MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.EXACTLY);
4501 } else {
4502 return MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY);
4503 }
4504 }
4506 private int getChildHeightMeasureSpec(LayoutParams lp) {
4507 if (mIsVertical && lp.height == LayoutParams.WRAP_CONTENT) {
4508 return MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
4509 } else if (!mIsVertical) {
4510 final int maxHeight = getHeight() - getPaddingTop() - getPaddingBottom();
4511 return MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.EXACTLY);
4512 } else {
4513 return MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY);
4514 }
4515 }
4517 private void measureChild(View child) {
4518 measureChild(child, (LayoutParams) child.getLayoutParams());
4519 }
4521 private void measureChild(View child, LayoutParams lp) {
4522 final int widthSpec = getChildWidthMeasureSpec(lp);
4523 final int heightSpec = getChildHeightMeasureSpec(lp);
4524 child.measure(widthSpec, heightSpec);
4525 }
4527 private void relayoutMeasuredChild(View child) {
4528 final int w = child.getMeasuredWidth();
4529 final int h = child.getMeasuredHeight();
4531 final int childLeft = getPaddingLeft();
4532 final int childRight = childLeft + w;
4533 final int childTop = child.getTop();
4534 final int childBottom = childTop + h;
4536 child.layout(childLeft, childTop, childRight, childBottom);
4537 }
4539 private void measureScrapChild(View scrapChild, int position, int secondaryMeasureSpec) {
4540 LayoutParams lp = (LayoutParams) scrapChild.getLayoutParams();
4541 if (lp == null) {
4542 lp = generateDefaultLayoutParams();
4543 scrapChild.setLayoutParams(lp);
4544 }
4546 lp.viewType = mAdapter.getItemViewType(position);
4547 lp.forceAdd = true;
4549 final int widthMeasureSpec;
4550 final int heightMeasureSpec;
4551 if (mIsVertical) {
4552 widthMeasureSpec = secondaryMeasureSpec;
4553 heightMeasureSpec = getChildHeightMeasureSpec(lp);
4554 } else {
4555 widthMeasureSpec = getChildWidthMeasureSpec(lp);
4556 heightMeasureSpec = secondaryMeasureSpec;
4557 }
4559 scrapChild.measure(widthMeasureSpec, heightMeasureSpec);
4560 }
4562 /**
4563 * Measures the height of the given range of children (inclusive) and
4564 * returns the height with this TwoWayView's padding and item margin heights
4565 * included. If maxHeight is provided, the measuring will stop when the
4566 * current height reaches maxHeight.
4567 *
4568 * @param widthMeasureSpec The width measure spec to be given to a child's
4569 * {@link View#measure(int, int)}.
4570 * @param startPosition The position of the first child to be shown.
4571 * @param endPosition The (inclusive) position of the last child to be
4572 * shown. Specify {@link #NO_POSITION} if the last child should be
4573 * the last available child from the adapter.
4574 * @param maxHeight The maximum height that will be returned (if all the
4575 * children don't fit in this value, this value will be
4576 * returned).
4577 * @param disallowPartialChildPosition In general, whether the returned
4578 * height should only contain entire children. This is more
4579 * powerful--it is the first inclusive position at which partial
4580 * children will not be allowed. Example: it looks nice to have
4581 * at least 3 completely visible children, and in portrait this
4582 * will most likely fit; but in landscape there could be times
4583 * when even 2 children can not be completely shown, so a value
4584 * of 2 (remember, inclusive) would be good (assuming
4585 * startPosition is 0).
4586 * @return The height of this TwoWayView with the given children.
4587 */
4588 private int measureHeightOfChildren(int widthMeasureSpec, int startPosition, int endPosition,
4589 final int maxHeight, int disallowPartialChildPosition) {
4591 final int paddingTop = getPaddingTop();
4592 final int paddingBottom = getPaddingBottom();
4594 final ListAdapter adapter = mAdapter;
4595 if (adapter == null) {
4596 return paddingTop + paddingBottom;
4597 }
4599 // Include the padding of the list
4600 int returnedHeight = paddingTop + paddingBottom;
4601 final int itemMargin = mItemMargin;
4603 // The previous height value that was less than maxHeight and contained
4604 // no partial children
4605 int prevHeightWithoutPartialChild = 0;
4606 int i;
4607 View child;
4609 // mItemCount - 1 since endPosition parameter is inclusive
4610 endPosition = (endPosition == NO_POSITION) ? adapter.getCount() - 1 : endPosition;
4611 final RecycleBin recycleBin = mRecycler;
4612 final boolean shouldRecycle = recycleOnMeasure();
4613 final boolean[] isScrap = mIsScrap;
4615 for (i = startPosition; i <= endPosition; ++i) {
4616 child = obtainView(i, isScrap);
4618 measureScrapChild(child, i, widthMeasureSpec);
4620 if (i > 0) {
4621 // Count the item margin for all but one child
4622 returnedHeight += itemMargin;
4623 }
4625 // Recycle the view before we possibly return from the method
4626 if (shouldRecycle) {
4627 recycleBin.addScrapView(child, -1);
4628 }
4630 returnedHeight += child.getMeasuredHeight();
4632 if (returnedHeight >= maxHeight) {
4633 // We went over, figure out which height to return. If returnedHeight > maxHeight,
4634 // then the i'th position did not fit completely.
4635 return (disallowPartialChildPosition >= 0) // Disallowing is enabled (> -1)
4636 && (i > disallowPartialChildPosition) // We've past the min pos
4637 && (prevHeightWithoutPartialChild > 0) // We have a prev height
4638 && (returnedHeight != maxHeight) // i'th child did not fit completely
4639 ? prevHeightWithoutPartialChild
4640 : maxHeight;
4641 }
4643 if ((disallowPartialChildPosition >= 0) && (i >= disallowPartialChildPosition)) {
4644 prevHeightWithoutPartialChild = returnedHeight;
4645 }
4646 }
4648 // At this point, we went through the range of children, and they each
4649 // completely fit, so return the returnedHeight
4650 return returnedHeight;
4651 }
4653 /**
4654 * Measures the width of the given range of children (inclusive) and
4655 * returns the width with this TwoWayView's padding and item margin widths
4656 * included. If maxWidth is provided, the measuring will stop when the
4657 * current width reaches maxWidth.
4658 *
4659 * @param heightMeasureSpec The height measure spec to be given to a child's
4660 * {@link View#measure(int, int)}.
4661 * @param startPosition The position of the first child to be shown.
4662 * @param endPosition The (inclusive) position of the last child to be
4663 * shown. Specify {@link #NO_POSITION} if the last child should be
4664 * the last available child from the adapter.
4665 * @param maxWidth The maximum width that will be returned (if all the
4666 * children don't fit in this value, this value will be
4667 * returned).
4668 * @param disallowPartialChildPosition In general, whether the returned
4669 * width should only contain entire children. This is more
4670 * powerful--it is the first inclusive position at which partial
4671 * children will not be allowed. Example: it looks nice to have
4672 * at least 3 completely visible children, and in portrait this
4673 * will most likely fit; but in landscape there could be times
4674 * when even 2 children can not be completely shown, so a value
4675 * of 2 (remember, inclusive) would be good (assuming
4676 * startPosition is 0).
4677 * @return The width of this TwoWayView with the given children.
4678 */
4679 private int measureWidthOfChildren(int heightMeasureSpec, int startPosition, int endPosition,
4680 final int maxWidth, int disallowPartialChildPosition) {
4682 final int paddingLeft = getPaddingLeft();
4683 final int paddingRight = getPaddingRight();
4685 final ListAdapter adapter = mAdapter;
4686 if (adapter == null) {
4687 return paddingLeft + paddingRight;
4688 }
4690 // Include the padding of the list
4691 int returnedWidth = paddingLeft + paddingRight;
4692 final int itemMargin = mItemMargin;
4694 // The previous height value that was less than maxHeight and contained
4695 // no partial children
4696 int prevWidthWithoutPartialChild = 0;
4697 int i;
4698 View child;
4700 // mItemCount - 1 since endPosition parameter is inclusive
4701 endPosition = (endPosition == NO_POSITION) ? adapter.getCount() - 1 : endPosition;
4702 final RecycleBin recycleBin = mRecycler;
4703 final boolean shouldRecycle = recycleOnMeasure();
4704 final boolean[] isScrap = mIsScrap;
4706 for (i = startPosition; i <= endPosition; ++i) {
4707 child = obtainView(i, isScrap);
4709 measureScrapChild(child, i, heightMeasureSpec);
4711 if (i > 0) {
4712 // Count the item margin for all but one child
4713 returnedWidth += itemMargin;
4714 }
4716 // Recycle the view before we possibly return from the method
4717 if (shouldRecycle) {
4718 recycleBin.addScrapView(child, -1);
4719 }
4721 returnedWidth += child.getMeasuredHeight();
4723 if (returnedWidth >= maxWidth) {
4724 // We went over, figure out which width to return. If returnedWidth > maxWidth,
4725 // then the i'th position did not fit completely.
4726 return (disallowPartialChildPosition >= 0) // Disallowing is enabled (> -1)
4727 && (i > disallowPartialChildPosition) // We've past the min pos
4728 && (prevWidthWithoutPartialChild > 0) // We have a prev width
4729 && (returnedWidth != maxWidth) // i'th child did not fit completely
4730 ? prevWidthWithoutPartialChild
4731 : maxWidth;
4732 }
4734 if ((disallowPartialChildPosition >= 0) && (i >= disallowPartialChildPosition)) {
4735 prevWidthWithoutPartialChild = returnedWidth;
4736 }
4737 }
4739 // At this point, we went through the range of children, and they each
4740 // completely fit, so return the returnedWidth
4741 return returnedWidth;
4742 }
4744 private View makeAndAddView(int position, int offset, boolean flow, boolean selected) {
4745 final int top;
4746 final int left;
4748 if (mIsVertical) {
4749 top = offset;
4750 left = getPaddingLeft();
4751 } else {
4752 top = getPaddingTop();
4753 left = offset;
4754 }
4756 if (!mDataChanged) {
4757 // Try to use an existing view for this position
4758 final View activeChild = mRecycler.getActiveView(position);
4759 if (activeChild != null) {
4760 // Found it -- we're using an existing child
4761 // This just needs to be positioned
4762 setupChild(activeChild, position, top, left, flow, selected, true);
4764 return activeChild;
4765 }
4766 }
4768 // Make a new view for this position, or convert an unused view if possible
4769 final View child = obtainView(position, mIsScrap);
4771 // This needs to be positioned and measured
4772 setupChild(child, position, top, left, flow, selected, mIsScrap[0]);
4774 return child;
4775 }
4777 @TargetApi(11)
4778 private void setupChild(View child, int position, int top, int left,
4779 boolean flow, boolean selected, boolean recycled) {
4780 final boolean isSelected = selected && shouldShowSelector();
4781 final boolean updateChildSelected = isSelected != child.isSelected();
4782 final int touchMode = mTouchMode;
4784 final boolean isPressed = touchMode > TOUCH_MODE_DOWN && touchMode < TOUCH_MODE_DRAGGING &&
4785 mMotionPosition == position;
4787 final boolean updateChildPressed = isPressed != child.isPressed();
4788 final boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested();
4790 // Respect layout params that are already in the view. Otherwise make some up...
4791 LayoutParams lp = (LayoutParams) child.getLayoutParams();
4792 if (lp == null) {
4793 lp = generateDefaultLayoutParams();
4794 }
4796 lp.viewType = mAdapter.getItemViewType(position);
4798 if (recycled && !lp.forceAdd) {
4799 attachViewToParent(child, (flow ? -1 : 0), lp);
4800 } else {
4801 lp.forceAdd = false;
4802 addViewInLayout(child, (flow ? -1 : 0), lp, true);
4803 }
4805 if (updateChildSelected) {
4806 child.setSelected(isSelected);
4807 }
4809 if (updateChildPressed) {
4810 child.setPressed(isPressed);
4811 }
4813 if (mChoiceMode.compareTo(ChoiceMode.NONE) != 0 && mCheckStates != null) {
4814 if (child instanceof Checkable) {
4815 ((Checkable) child).setChecked(mCheckStates.get(position));
4816 } else if (getContext().getApplicationInfo().targetSdkVersion
4817 >= Build.VERSION_CODES.HONEYCOMB) {
4818 child.setActivated(mCheckStates.get(position));
4819 }
4820 }
4822 if (needToMeasure) {
4823 measureChild(child, lp);
4824 } else {
4825 cleanupLayoutState(child);
4826 }
4828 final int w = child.getMeasuredWidth();
4829 final int h = child.getMeasuredHeight();
4831 final int childTop = (mIsVertical && !flow ? top - h : top);
4832 final int childLeft = (!mIsVertical && !flow ? left - w : left);
4834 if (needToMeasure) {
4835 final int childRight = childLeft + w;
4836 final int childBottom = childTop + h;
4838 child.layout(childLeft, childTop, childRight, childBottom);
4839 } else {
4840 child.offsetLeftAndRight(childLeft - child.getLeft());
4841 child.offsetTopAndBottom(childTop - child.getTop());
4842 }
4843 }
4845 void fillGap(boolean down) {
4846 final int childCount = getChildCount();
4848 if (down) {
4849 final int paddingStart = (mIsVertical ? getPaddingTop() : getPaddingLeft());
4851 final int lastEnd;
4852 if (mIsVertical) {
4853 lastEnd = getChildAt(childCount - 1).getBottom();
4854 } else {
4855 lastEnd = getChildAt(childCount - 1).getRight();
4856 }
4858 final int offset = (childCount > 0 ? lastEnd + mItemMargin : paddingStart);
4859 fillAfter(mFirstPosition + childCount, offset);
4860 correctTooHigh(getChildCount());
4861 } else {
4862 final int end;
4863 final int firstStart;
4865 if (mIsVertical) {
4866 end = getHeight() - getPaddingBottom();
4867 firstStart = getChildAt(0).getTop();
4868 } else {
4869 end = getWidth() - getPaddingRight();
4870 firstStart = getChildAt(0).getLeft();
4871 }
4873 final int offset = (childCount > 0 ? firstStart - mItemMargin : end);
4874 fillBefore(mFirstPosition - 1, offset);
4875 correctTooLow(getChildCount());
4876 }
4877 }
4879 private View fillBefore(int pos, int nextOffset) {
4880 View selectedView = null;
4882 final int start = (mIsVertical ? getPaddingTop() : getPaddingLeft());
4884 while (nextOffset > start && pos >= 0) {
4885 boolean isSelected = (pos == mSelectedPosition);
4886 View child = makeAndAddView(pos, nextOffset, false, isSelected);
4888 if (mIsVertical) {
4889 nextOffset = child.getTop() - mItemMargin;
4890 } else {
4891 nextOffset = child.getLeft() - mItemMargin;
4892 }
4894 if (isSelected) {
4895 selectedView = child;
4896 }
4898 pos--;
4899 }
4901 mFirstPosition = pos + 1;
4903 return selectedView;
4904 }
4906 private View fillAfter(int pos, int nextOffset) {
4907 View selectedView = null;
4909 final int end =
4910 (mIsVertical ? getHeight() - getPaddingBottom() : getWidth() - getPaddingRight());
4912 while (nextOffset < end && pos < mItemCount) {
4913 boolean selected = (pos == mSelectedPosition);
4915 View child = makeAndAddView(pos, nextOffset, true, selected);
4917 if (mIsVertical) {
4918 nextOffset = child.getBottom() + mItemMargin;
4919 } else {
4920 nextOffset = child.getRight() + mItemMargin;
4921 }
4923 if (selected) {
4924 selectedView = child;
4925 }
4927 pos++;
4928 }
4930 return selectedView;
4931 }
4933 private View fillSpecific(int position, int offset) {
4934 final boolean tempIsSelected = (position == mSelectedPosition);
4935 View temp = makeAndAddView(position, offset, true, tempIsSelected);
4937 // Possibly changed again in fillBefore if we add rows above this one.
4938 mFirstPosition = position;
4940 final int itemMargin = mItemMargin;
4942 final int offsetBefore;
4943 if (mIsVertical) {
4944 offsetBefore = temp.getTop() - itemMargin;
4945 } else {
4946 offsetBefore = temp.getLeft() - itemMargin;
4947 }
4948 final View before = fillBefore(position - 1, offsetBefore);
4950 // This will correct for the top of the first view not touching the top of the list
4951 adjustViewsStartOrEnd();
4953 final int offsetAfter;
4954 if (mIsVertical) {
4955 offsetAfter = temp.getBottom() + itemMargin;
4956 } else {
4957 offsetAfter = temp.getRight() + itemMargin;
4958 }
4959 final View after = fillAfter(position + 1, offsetAfter);
4961 final int childCount = getChildCount();
4962 if (childCount > 0) {
4963 correctTooHigh(childCount);
4964 }
4966 if (tempIsSelected) {
4967 return temp;
4968 } else if (before != null) {
4969 return before;
4970 } else {
4971 return after;
4972 }
4973 }
4975 private View fillFromOffset(int nextOffset) {
4976 mFirstPosition = Math.min(mFirstPosition, mSelectedPosition);
4977 mFirstPosition = Math.min(mFirstPosition, mItemCount - 1);
4979 if (mFirstPosition < 0) {
4980 mFirstPosition = 0;
4981 }
4983 return fillAfter(mFirstPosition, nextOffset);
4984 }
4986 private View fillFromMiddle(int start, int end) {
4987 final int size = end - start;
4988 int position = reconcileSelectedPosition();
4990 View selected = makeAndAddView(position, start, true, true);
4991 mFirstPosition = position;
4993 if (mIsVertical) {
4994 int selectedHeight = selected.getMeasuredHeight();
4995 if (selectedHeight <= size) {
4996 selected.offsetTopAndBottom((size - selectedHeight) / 2);
4997 }
4998 } else {
4999 int selectedWidth = selected.getMeasuredWidth();
5000 if (selectedWidth <= size) {
5001 selected.offsetLeftAndRight((size - selectedWidth) / 2);
5002 }
5003 }
5005 fillBeforeAndAfter(selected, position);
5006 correctTooHigh(getChildCount());
5008 return selected;
5009 }
5011 private void fillBeforeAndAfter(View selected, int position) {
5012 final int itemMargin = mItemMargin;
5014 final int offsetBefore;
5015 if (mIsVertical) {
5016 offsetBefore = selected.getTop() - itemMargin;
5017 } else {
5018 offsetBefore = selected.getLeft() - itemMargin;
5019 }
5021 fillBefore(position - 1, offsetBefore);
5023 adjustViewsStartOrEnd();
5025 final int offsetAfter;
5026 if (mIsVertical) {
5027 offsetAfter = selected.getBottom() + itemMargin;
5028 } else {
5029 offsetAfter = selected.getRight() + itemMargin;
5030 }
5032 fillAfter(position + 1, offsetAfter);
5033 }
5035 private View fillFromSelection(int selectedTop, int start, int end) {
5036 final int selectedPosition = mSelectedPosition;
5037 View selected;
5039 selected = makeAndAddView(selectedPosition, selectedTop, true, true);
5041 final int selectedStart = (mIsVertical ? selected.getTop() : selected.getLeft());
5042 final int selectedEnd = (mIsVertical ? selected.getBottom() : selected.getRight());
5044 // Some of the newly selected item extends below the bottom of the list
5045 if (selectedEnd > end) {
5046 // Find space available above the selection into which we can scroll
5047 // upwards
5048 final int spaceAbove = selectedStart - start;
5050 // Find space required to bring the bottom of the selected item
5051 // fully into view
5052 final int spaceBelow = selectedEnd - end;
5054 final int offset = Math.min(spaceAbove, spaceBelow);
5056 // Now offset the selected item to get it into view
5057 selected.offsetTopAndBottom(-offset);
5058 } else if (selectedStart < start) {
5059 // Find space required to bring the top of the selected item fully
5060 // into view
5061 final int spaceAbove = start - selectedStart;
5063 // Find space available below the selection into which we can scroll
5064 // downwards
5065 final int spaceBelow = end - selectedEnd;
5067 final int offset = Math.min(spaceAbove, spaceBelow);
5069 // Offset the selected item to get it into view
5070 selected.offsetTopAndBottom(offset);
5071 }
5073 // Fill in views above and below
5074 fillBeforeAndAfter(selected, selectedPosition);
5075 correctTooHigh(getChildCount());
5077 return selected;
5078 }
5080 private void correctTooHigh(int childCount) {
5081 // First see if the last item is visible. If it is not, it is OK for the
5082 // top of the list to be pushed up.
5083 final int lastPosition = mFirstPosition + childCount - 1;
5084 if (lastPosition != mItemCount - 1 || childCount == 0) {
5085 return;
5086 }
5088 // Get the last child ...
5089 final View lastChild = getChildAt(childCount - 1);
5091 // ... and its end edge
5092 final int lastEnd;
5093 if (mIsVertical) {
5094 lastEnd = lastChild.getBottom();
5095 } else {
5096 lastEnd = lastChild.getRight();
5097 }
5099 // This is bottom of our drawable area
5100 final int start = (mIsVertical ? getPaddingTop() : getPaddingLeft());
5101 final int end =
5102 (mIsVertical ? getHeight() - getPaddingBottom() : getWidth() - getPaddingRight());
5104 // This is how far the end edge of the last view is from the end of the
5105 // drawable area
5106 int endOffset = end - lastEnd;
5108 View firstChild = getChildAt(0);
5109 int firstStart = (mIsVertical ? firstChild.getTop() : firstChild.getLeft());
5111 // Make sure we are 1) Too high, and 2) Either there are more rows above the
5112 // first row or the first row is scrolled off the top of the drawable area
5113 if (endOffset > 0 && (mFirstPosition > 0 || firstStart < start)) {
5114 if (mFirstPosition == 0) {
5115 // Don't pull the top too far down
5116 endOffset = Math.min(endOffset, start - firstStart);
5117 }
5119 // Move everything down
5120 offsetChildren(endOffset);
5122 if (mFirstPosition > 0) {
5123 firstStart = (mIsVertical ? firstChild.getTop() : firstChild.getLeft());
5125 // Fill the gap that was opened above mFirstPosition with more rows, if
5126 // possible
5127 fillBefore(mFirstPosition - 1, firstStart - mItemMargin);
5129 // Close up the remaining gap
5130 adjustViewsStartOrEnd();
5131 }
5132 }
5133 }
5135 private void correctTooLow(int childCount) {
5136 // First see if the first item is visible. If it is not, it is OK for the
5137 // bottom of the list to be pushed down.
5138 if (mFirstPosition != 0 || childCount == 0) {
5139 return;
5140 }
5142 final View first = getChildAt(0);
5143 final int firstStart = (mIsVertical ? first.getTop() : first.getLeft());
5145 final int start = (mIsVertical ? getPaddingTop() : getPaddingLeft());
5147 final int end;
5148 if (mIsVertical) {
5149 end = getHeight() - getPaddingBottom();
5150 } else {
5151 end = getWidth() - getPaddingRight();
5152 }
5154 // This is how far the start edge of the first view is from the start of the
5155 // drawable area
5156 int startOffset = firstStart - start;
5158 View last = getChildAt(childCount - 1);
5159 int lastEnd = (mIsVertical ? last.getBottom() : last.getRight());
5161 int lastPosition = mFirstPosition + childCount - 1;
5163 // Make sure we are 1) Too low, and 2) Either there are more columns/rows below the
5164 // last column/row or the last column/row is scrolled off the end of the
5165 // drawable area
5166 if (startOffset > 0) {
5167 if (lastPosition < mItemCount - 1 || lastEnd > end) {
5168 if (lastPosition == mItemCount - 1) {
5169 // Don't pull the bottom too far up
5170 startOffset = Math.min(startOffset, lastEnd - end);
5171 }
5173 // Move everything up
5174 offsetChildren(-startOffset);
5176 if (lastPosition < mItemCount - 1) {
5177 lastEnd = (mIsVertical ? last.getBottom() : last.getRight());
5179 // Fill the gap that was opened below the last position with more rows, if
5180 // possible
5181 fillAfter(lastPosition + 1, lastEnd + mItemMargin);
5183 // Close up the remaining gap
5184 adjustViewsStartOrEnd();
5185 }
5186 } else if (lastPosition == mItemCount - 1) {
5187 adjustViewsStartOrEnd();
5188 }
5189 }
5190 }
5192 private void adjustViewsStartOrEnd() {
5193 if (getChildCount() == 0) {
5194 return;
5195 }
5197 final View firstChild = getChildAt(0);
5199 int delta;
5200 if (mIsVertical) {
5201 delta = firstChild.getTop() - getPaddingTop() - mItemMargin;
5202 } else {
5203 delta = firstChild.getLeft() - getPaddingLeft() - mItemMargin;
5204 }
5206 if (delta < 0) {
5207 // We only are looking to see if we are too low, not too high
5208 delta = 0;
5209 }
5211 if (delta != 0) {
5212 offsetChildren(-delta);
5213 }
5214 }
5216 @TargetApi(14)
5217 private SparseBooleanArray cloneCheckStates() {
5218 if (mCheckStates == null) {
5219 return null;
5220 }
5222 SparseBooleanArray checkedStates;
5224 if (Build.VERSION.SDK_INT >= 14) {
5225 checkedStates = mCheckStates.clone();
5226 } else {
5227 checkedStates = new SparseBooleanArray();
5229 for (int i = 0; i < mCheckStates.size(); i++) {
5230 checkedStates.put(mCheckStates.keyAt(i), mCheckStates.valueAt(i));
5231 }
5232 }
5234 return checkedStates;
5235 }
5237 private int findSyncPosition() {
5238 int itemCount = mItemCount;
5240 if (itemCount == 0) {
5241 return INVALID_POSITION;
5242 }
5244 final long idToMatch = mSyncRowId;
5246 // If there isn't a selection don't hunt for it
5247 if (idToMatch == INVALID_ROW_ID) {
5248 return INVALID_POSITION;
5249 }
5251 // Pin seed to reasonable values
5252 int seed = mSyncPosition;
5253 seed = Math.max(0, seed);
5254 seed = Math.min(itemCount - 1, seed);
5256 long endTime = SystemClock.uptimeMillis() + SYNC_MAX_DURATION_MILLIS;
5258 long rowId;
5260 // first position scanned so far
5261 int first = seed;
5263 // last position scanned so far
5264 int last = seed;
5266 // True if we should move down on the next iteration
5267 boolean next = false;
5269 // True when we have looked at the first item in the data
5270 boolean hitFirst;
5272 // True when we have looked at the last item in the data
5273 boolean hitLast;
5275 // Get the item ID locally (instead of getItemIdAtPosition), so
5276 // we need the adapter
5277 final ListAdapter adapter = mAdapter;
5278 if (adapter == null) {
5279 return INVALID_POSITION;
5280 }
5282 while (SystemClock.uptimeMillis() <= endTime) {
5283 rowId = adapter.getItemId(seed);
5284 if (rowId == idToMatch) {
5285 // Found it!
5286 return seed;
5287 }
5289 hitLast = (last == itemCount - 1);
5290 hitFirst = (first == 0);
5292 if (hitLast && hitFirst) {
5293 // Looked at everything
5294 break;
5295 }
5297 if (hitFirst || (next && !hitLast)) {
5298 // Either we hit the top, or we are trying to move down
5299 last++;
5300 seed = last;
5302 // Try going up next time
5303 next = false;
5304 } else if (hitLast || (!next && !hitFirst)) {
5305 // Either we hit the bottom, or we are trying to move up
5306 first--;
5307 seed = first;
5309 // Try going down next time
5310 next = true;
5311 }
5312 }
5314 return INVALID_POSITION;
5315 }
5317 @TargetApi(16)
5318 private View obtainView(int position, boolean[] isScrap) {
5319 isScrap[0] = false;
5321 View scrapView = mRecycler.getTransientStateView(position);
5322 if (scrapView != null) {
5323 return scrapView;
5324 }
5326 scrapView = mRecycler.getScrapView(position);
5328 final View child;
5329 if (scrapView != null) {
5330 child = mAdapter.getView(position, scrapView, this);
5332 if (child != scrapView) {
5333 mRecycler.addScrapView(scrapView, position);
5334 } else {
5335 isScrap[0] = true;
5336 }
5337 } else {
5338 child = mAdapter.getView(position, null, this);
5339 }
5341 if (ViewCompat.getImportantForAccessibility(child) == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
5342 ViewCompat.setImportantForAccessibility(child, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
5343 }
5345 if (mHasStableIds) {
5346 LayoutParams lp = (LayoutParams) child.getLayoutParams();
5348 if (lp == null) {
5349 lp = generateDefaultLayoutParams();
5350 } else if (!checkLayoutParams(lp)) {
5351 lp = generateLayoutParams(lp);
5352 }
5354 lp.id = mAdapter.getItemId(position);
5356 child.setLayoutParams(lp);
5357 }
5359 if (mAccessibilityDelegate == null) {
5360 mAccessibilityDelegate = new ListItemAccessibilityDelegate();
5361 }
5363 ViewCompat.setAccessibilityDelegate(child, mAccessibilityDelegate);
5365 return child;
5366 }
5368 void resetState() {
5369 removeAllViewsInLayout();
5371 mSelectedStart = 0;
5372 mFirstPosition = 0;
5373 mDataChanged = false;
5374 mNeedSync = false;
5375 mPendingSync = null;
5376 mOldSelectedPosition = INVALID_POSITION;
5377 mOldSelectedRowId = INVALID_ROW_ID;
5379 mOverScroll = 0;
5381 setSelectedPositionInt(INVALID_POSITION);
5382 setNextSelectedPositionInt(INVALID_POSITION);
5384 mSelectorPosition = INVALID_POSITION;
5385 mSelectorRect.setEmpty();
5387 invalidate();
5388 }
5390 private void rememberSyncState() {
5391 if (getChildCount() == 0) {
5392 return;
5393 }
5395 mNeedSync = true;
5397 if (mSelectedPosition >= 0) {
5398 View child = getChildAt(mSelectedPosition - mFirstPosition);
5400 mSyncRowId = mNextSelectedRowId;
5401 mSyncPosition = mNextSelectedPosition;
5403 if (child != null) {
5404 mSpecificStart = (mIsVertical ? child.getTop() : child.getLeft());
5405 }
5407 mSyncMode = SYNC_SELECTED_POSITION;
5408 } else {
5409 // Sync the based on the offset of the first view
5410 View child = getChildAt(0);
5411 ListAdapter adapter = getAdapter();
5413 if (mFirstPosition >= 0 && mFirstPosition < adapter.getCount()) {
5414 mSyncRowId = adapter.getItemId(mFirstPosition);
5415 } else {
5416 mSyncRowId = NO_ID;
5417 }
5419 mSyncPosition = mFirstPosition;
5421 if (child != null) {
5422 mSpecificStart = child.getTop();
5423 }
5425 mSyncMode = SYNC_FIRST_POSITION;
5426 }
5427 }
5429 private ContextMenuInfo createContextMenuInfo(View view, int position, long id) {
5430 return new AdapterContextMenuInfo(view, position, id);
5431 }
5433 @TargetApi(11)
5434 private void updateOnScreenCheckedViews() {
5435 final int firstPos = mFirstPosition;
5436 final int count = getChildCount();
5438 final boolean useActivated = getContext().getApplicationInfo().targetSdkVersion
5439 >= Build.VERSION_CODES.HONEYCOMB;
5441 for (int i = 0; i < count; i++) {
5442 final View child = getChildAt(i);
5443 final int position = firstPos + i;
5445 if (child instanceof Checkable) {
5446 ((Checkable) child).setChecked(mCheckStates.get(position));
5447 } else if (useActivated) {
5448 child.setActivated(mCheckStates.get(position));
5449 }
5450 }
5451 }
5453 @Override
5454 public boolean performItemClick(View view, int position, long id) {
5455 boolean checkedStateChanged = false;
5457 if (mChoiceMode.compareTo(ChoiceMode.MULTIPLE) == 0) {
5458 boolean checked = !mCheckStates.get(position, false);
5459 mCheckStates.put(position, checked);
5461 if (mCheckedIdStates != null && mAdapter.hasStableIds()) {
5462 if (checked) {
5463 mCheckedIdStates.put(mAdapter.getItemId(position), position);
5464 } else {
5465 mCheckedIdStates.delete(mAdapter.getItemId(position));
5466 }
5467 }
5469 if (checked) {
5470 mCheckedItemCount++;
5471 } else {
5472 mCheckedItemCount--;
5473 }
5475 checkedStateChanged = true;
5476 } else if (mChoiceMode.compareTo(ChoiceMode.SINGLE) == 0) {
5477 boolean checked = !mCheckStates.get(position, false);
5478 if (checked) {
5479 mCheckStates.clear();
5480 mCheckStates.put(position, true);
5482 if (mCheckedIdStates != null && mAdapter.hasStableIds()) {
5483 mCheckedIdStates.clear();
5484 mCheckedIdStates.put(mAdapter.getItemId(position), position);
5485 }
5487 mCheckedItemCount = 1;
5488 } else if (mCheckStates.size() == 0 || !mCheckStates.valueAt(0)) {
5489 mCheckedItemCount = 0;
5490 }
5492 checkedStateChanged = true;
5493 }
5495 if (checkedStateChanged) {
5496 updateOnScreenCheckedViews();
5497 }
5499 return super.performItemClick(view, position, id);
5500 }
5502 private boolean performLongPress(final View child,
5503 final int longPressPosition, final long longPressId) {
5504 // CHOICE_MODE_MULTIPLE_MODAL takes over long press.
5505 boolean handled = false;
5507 OnItemLongClickListener listener = getOnItemLongClickListener();
5508 if (listener != null) {
5509 handled = listener.onItemLongClick(TwoWayView.this, child,
5510 longPressPosition, longPressId);
5511 }
5513 if (!handled) {
5514 mContextMenuInfo = createContextMenuInfo(child, longPressPosition, longPressId);
5515 handled = super.showContextMenuForChild(TwoWayView.this);
5516 }
5518 if (handled) {
5519 performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
5520 }
5522 return handled;
5523 }
5525 @Override
5526 protected LayoutParams generateDefaultLayoutParams() {
5527 if (mIsVertical) {
5528 return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
5529 } else {
5530 return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
5531 }
5532 }
5534 @Override
5535 protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
5536 return new LayoutParams(lp);
5537 }
5539 @Override
5540 protected boolean checkLayoutParams(ViewGroup.LayoutParams lp) {
5541 return lp instanceof LayoutParams;
5542 }
5544 @Override
5545 public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
5546 return new LayoutParams(getContext(), attrs);
5547 }
5549 @Override
5550 protected ContextMenuInfo getContextMenuInfo() {
5551 return mContextMenuInfo;
5552 }
5554 @Override
5555 public Parcelable onSaveInstanceState() {
5556 Parcelable superState = super.onSaveInstanceState();
5557 SavedState ss = new SavedState(superState);
5559 if (mPendingSync != null) {
5560 ss.selectedId = mPendingSync.selectedId;
5561 ss.firstId = mPendingSync.firstId;
5562 ss.viewStart = mPendingSync.viewStart;
5563 ss.position = mPendingSync.position;
5564 ss.height = mPendingSync.height;
5566 return ss;
5567 }
5569 boolean haveChildren = (getChildCount() > 0 && mItemCount > 0);
5570 long selectedId = getSelectedItemId();
5571 ss.selectedId = selectedId;
5572 ss.height = getHeight();
5574 if (selectedId >= 0) {
5575 ss.viewStart = mSelectedStart;
5576 ss.position = getSelectedItemPosition();
5577 ss.firstId = INVALID_POSITION;
5578 } else if (haveChildren && mFirstPosition > 0) {
5579 // Remember the position of the first child.
5580 // We only do this if we are not currently at the top of
5581 // the list, for two reasons:
5582 //
5583 // (1) The list may be in the process of becoming empty, in
5584 // which case mItemCount may not be 0, but if we try to
5585 // ask for any information about position 0 we will crash.
5586 //
5587 // (2) Being "at the top" seems like a special case, anyway,
5588 // and the user wouldn't expect to end up somewhere else when
5589 // they revisit the list even if its content has changed.
5591 View child = getChildAt(0);
5592 ss.viewStart = (mIsVertical ? child.getTop() : child.getLeft());
5594 int firstPos = mFirstPosition;
5595 if (firstPos >= mItemCount) {
5596 firstPos = mItemCount - 1;
5597 }
5599 ss.position = firstPos;
5600 ss.firstId = mAdapter.getItemId(firstPos);
5601 } else {
5602 ss.viewStart = 0;
5603 ss.firstId = INVALID_POSITION;
5604 ss.position = 0;
5605 }
5607 if (mCheckStates != null) {
5608 ss.checkState = cloneCheckStates();
5609 }
5611 if (mCheckedIdStates != null) {
5612 final LongSparseArray<Integer> idState = new LongSparseArray<Integer>();
5614 final int count = mCheckedIdStates.size();
5615 for (int i = 0; i < count; i++) {
5616 idState.put(mCheckedIdStates.keyAt(i), mCheckedIdStates.valueAt(i));
5617 }
5619 ss.checkIdState = idState;
5620 }
5622 ss.checkedItemCount = mCheckedItemCount;
5624 return ss;
5625 }
5627 @Override
5628 public void onRestoreInstanceState(Parcelable state) {
5629 SavedState ss = (SavedState) state;
5630 super.onRestoreInstanceState(ss.getSuperState());
5632 mDataChanged = true;
5633 mSyncHeight = ss.height;
5635 if (ss.selectedId >= 0) {
5636 mNeedSync = true;
5637 mPendingSync = ss;
5638 mSyncRowId = ss.selectedId;
5639 mSyncPosition = ss.position;
5640 mSpecificStart = ss.viewStart;
5641 mSyncMode = SYNC_SELECTED_POSITION;
5642 } else if (ss.firstId >= 0) {
5643 setSelectedPositionInt(INVALID_POSITION);
5645 // Do this before setting mNeedSync since setNextSelectedPosition looks at mNeedSync
5646 setNextSelectedPositionInt(INVALID_POSITION);
5648 mSelectorPosition = INVALID_POSITION;
5649 mNeedSync = true;
5650 mPendingSync = ss;
5651 mSyncRowId = ss.firstId;
5652 mSyncPosition = ss.position;
5653 mSpecificStart = ss.viewStart;
5654 mSyncMode = SYNC_FIRST_POSITION;
5655 }
5657 if (ss.checkState != null) {
5658 mCheckStates = ss.checkState;
5659 }
5661 if (ss.checkIdState != null) {
5662 mCheckedIdStates = ss.checkIdState;
5663 }
5665 mCheckedItemCount = ss.checkedItemCount;
5667 requestLayout();
5668 }
5670 public static class LayoutParams extends ViewGroup.LayoutParams {
5671 /**
5672 * Type of this view as reported by the adapter
5673 */
5674 int viewType;
5676 /**
5677 * The stable ID of the item this view displays
5678 */
5679 long id = -1;
5681 /**
5682 * The position the view was removed from when pulled out of the
5683 * scrap heap.
5684 * @hide
5685 */
5686 int scrappedFromPosition;
5688 /**
5689 * When a TwoWayView is measured with an AT_MOST measure spec, it needs
5690 * to obtain children views to measure itself. When doing so, the children
5691 * are not attached to the window, but put in the recycler which assumes
5692 * they've been attached before. Setting this flag will force the reused
5693 * view to be attached to the window rather than just attached to the
5694 * parent.
5695 */
5696 boolean forceAdd;
5698 public LayoutParams(int width, int height) {
5699 super(width, height);
5701 if (this.width == MATCH_PARENT) {
5702 Log.w(LOGTAG, "Constructing LayoutParams with width FILL_PARENT " +
5703 "does not make much sense as the view might change orientation. " +
5704 "Falling back to WRAP_CONTENT");
5705 this.width = WRAP_CONTENT;
5706 }
5708 if (this.height == MATCH_PARENT) {
5709 Log.w(LOGTAG, "Constructing LayoutParams with height FILL_PARENT " +
5710 "does not make much sense as the view might change orientation. " +
5711 "Falling back to WRAP_CONTENT");
5712 this.height = WRAP_CONTENT;
5713 }
5714 }
5716 public LayoutParams(Context c, AttributeSet attrs) {
5717 super(c, attrs);
5719 if (this.width == MATCH_PARENT) {
5720 Log.w(LOGTAG, "Inflation setting LayoutParams width to MATCH_PARENT - " +
5721 "does not make much sense as the view might change orientation. " +
5722 "Falling back to WRAP_CONTENT");
5723 this.width = MATCH_PARENT;
5724 }
5726 if (this.height == MATCH_PARENT) {
5727 Log.w(LOGTAG, "Inflation setting LayoutParams height to MATCH_PARENT - " +
5728 "does not make much sense as the view might change orientation. " +
5729 "Falling back to WRAP_CONTENT");
5730 this.height = WRAP_CONTENT;
5731 }
5732 }
5734 public LayoutParams(ViewGroup.LayoutParams other) {
5735 super(other);
5737 if (this.width == MATCH_PARENT) {
5738 Log.w(LOGTAG, "Constructing LayoutParams with height MATCH_PARENT - " +
5739 "does not make much sense as the view might change orientation. " +
5740 "Falling back to WRAP_CONTENT");
5741 this.width = WRAP_CONTENT;
5742 }
5744 if (this.height == MATCH_PARENT) {
5745 Log.w(LOGTAG, "Constructing LayoutParams with height MATCH_PARENT - " +
5746 "does not make much sense as the view might change orientation. " +
5747 "Falling back to WRAP_CONTENT");
5748 this.height = WRAP_CONTENT;
5749 }
5750 }
5751 }
5753 class RecycleBin {
5754 private RecyclerListener mRecyclerListener;
5755 private int mFirstActivePosition;
5756 private View[] mActiveViews = new View[0];
5757 private ArrayList<View>[] mScrapViews;
5758 private int mViewTypeCount;
5759 private ArrayList<View> mCurrentScrap;
5760 private SparseArrayCompat<View> mTransientStateViews;
5762 public void setViewTypeCount(int viewTypeCount) {
5763 if (viewTypeCount < 1) {
5764 throw new IllegalArgumentException("Can't have a viewTypeCount < 1");
5765 }
5767 @SuppressWarnings({"unchecked", "rawtypes"})
5768 ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount];
5769 for (int i = 0; i < viewTypeCount; i++) {
5770 scrapViews[i] = new ArrayList<View>();
5771 }
5773 mViewTypeCount = viewTypeCount;
5774 mCurrentScrap = scrapViews[0];
5775 mScrapViews = scrapViews;
5776 }
5778 public void markChildrenDirty() {
5779 if (mViewTypeCount == 1) {
5780 final ArrayList<View> scrap = mCurrentScrap;
5781 final int scrapCount = scrap.size();
5783 for (int i = 0; i < scrapCount; i++) {
5784 scrap.get(i).forceLayout();
5785 }
5786 } else {
5787 final int typeCount = mViewTypeCount;
5788 for (int i = 0; i < typeCount; i++) {
5789 final ArrayList<View> scrap = mScrapViews[i];
5790 final int scrapCount = scrap.size();
5792 for (int j = 0; j < scrapCount; j++) {
5793 scrap.get(j).forceLayout();
5794 }
5795 }
5796 }
5798 if (mTransientStateViews != null) {
5799 final int count = mTransientStateViews.size();
5800 for (int i = 0; i < count; i++) {
5801 mTransientStateViews.valueAt(i).forceLayout();
5802 }
5803 }
5804 }
5806 public boolean shouldRecycleViewType(int viewType) {
5807 return viewType >= 0;
5808 }
5810 void clear() {
5811 if (mViewTypeCount == 1) {
5812 final ArrayList<View> scrap = mCurrentScrap;
5813 final int scrapCount = scrap.size();
5815 for (int i = 0; i < scrapCount; i++) {
5816 removeDetachedView(scrap.remove(scrapCount - 1 - i), false);
5817 }
5818 } else {
5819 final int typeCount = mViewTypeCount;
5820 for (int i = 0; i < typeCount; i++) {
5821 final ArrayList<View> scrap = mScrapViews[i];
5822 final int scrapCount = scrap.size();
5824 for (int j = 0; j < scrapCount; j++) {
5825 removeDetachedView(scrap.remove(scrapCount - 1 - j), false);
5826 }
5827 }
5828 }
5830 if (mTransientStateViews != null) {
5831 mTransientStateViews.clear();
5832 }
5833 }
5835 void fillActiveViews(int childCount, int firstActivePosition) {
5836 if (mActiveViews.length < childCount) {
5837 mActiveViews = new View[childCount];
5838 }
5840 mFirstActivePosition = firstActivePosition;
5842 final View[] activeViews = mActiveViews;
5843 for (int i = 0; i < childCount; i++) {
5844 View child = getChildAt(i);
5846 // Note: We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in active views.
5847 // However, we will NOT place them into scrap views.
5848 activeViews[i] = child;
5849 }
5850 }
5852 View getActiveView(int position) {
5853 final int index = position - mFirstActivePosition;
5854 final View[] activeViews = mActiveViews;
5856 if (index >= 0 && index < activeViews.length) {
5857 final View match = activeViews[index];
5858 activeViews[index] = null;
5860 return match;
5861 }
5863 return null;
5864 }
5866 View getTransientStateView(int position) {
5867 if (mTransientStateViews == null) {
5868 return null;
5869 }
5871 final int index = mTransientStateViews.indexOfKey(position);
5872 if (index < 0) {
5873 return null;
5874 }
5876 final View result = mTransientStateViews.valueAt(index);
5877 mTransientStateViews.removeAt(index);
5879 return result;
5880 }
5882 void clearTransientStateViews() {
5883 if (mTransientStateViews != null) {
5884 mTransientStateViews.clear();
5885 }
5886 }
5888 View getScrapView(int position) {
5889 if (mViewTypeCount == 1) {
5890 return retrieveFromScrap(mCurrentScrap, position);
5891 } else {
5892 int whichScrap = mAdapter.getItemViewType(position);
5893 if (whichScrap >= 0 && whichScrap < mScrapViews.length) {
5894 return retrieveFromScrap(mScrapViews[whichScrap], position);
5895 }
5896 }
5898 return null;
5899 }
5901 @TargetApi(14)
5902 void addScrapView(View scrap, int position) {
5903 LayoutParams lp = (LayoutParams) scrap.getLayoutParams();
5904 if (lp == null) {
5905 return;
5906 }
5908 lp.scrappedFromPosition = position;
5910 final int viewType = lp.viewType;
5911 final boolean scrapHasTransientState = ViewCompat.hasTransientState(scrap);
5913 // Don't put views that should be ignored into the scrap heap
5914 if (!shouldRecycleViewType(viewType) || scrapHasTransientState) {
5915 if (scrapHasTransientState) {
5916 if (mTransientStateViews == null) {
5917 mTransientStateViews = new SparseArrayCompat<View>();
5918 }
5920 mTransientStateViews.put(position, scrap);
5921 }
5923 return;
5924 }
5926 if (mViewTypeCount == 1) {
5927 mCurrentScrap.add(scrap);
5928 } else {
5929 mScrapViews[viewType].add(scrap);
5930 }
5932 // FIXME: Unfortunately, ViewCompat.setAccessibilityDelegate() doesn't accept
5933 // null delegates.
5934 if (Build.VERSION.SDK_INT >= 14) {
5935 scrap.setAccessibilityDelegate(null);
5936 }
5938 if (mRecyclerListener != null) {
5939 mRecyclerListener.onMovedToScrapHeap(scrap);
5940 }
5941 }
5943 @TargetApi(14)
5944 void scrapActiveViews() {
5945 final View[] activeViews = mActiveViews;
5946 final boolean multipleScraps = (mViewTypeCount > 1);
5948 ArrayList<View> scrapViews = mCurrentScrap;
5949 final int count = activeViews.length;
5951 for (int i = count - 1; i >= 0; i--) {
5952 final View victim = activeViews[i];
5953 if (victim != null) {
5954 final LayoutParams lp = (LayoutParams) victim.getLayoutParams();
5955 int whichScrap = lp.viewType;
5957 activeViews[i] = null;
5959 final boolean scrapHasTransientState = ViewCompat.hasTransientState(victim);
5960 if (!shouldRecycleViewType(whichScrap) || scrapHasTransientState) {
5961 if (scrapHasTransientState) {
5962 removeDetachedView(victim, false);
5964 if (mTransientStateViews == null) {
5965 mTransientStateViews = new SparseArrayCompat<View>();
5966 }
5968 mTransientStateViews.put(mFirstActivePosition + i, victim);
5969 }
5971 continue;
5972 }
5974 if (multipleScraps) {
5975 scrapViews = mScrapViews[whichScrap];
5976 }
5978 lp.scrappedFromPosition = mFirstActivePosition + i;
5979 scrapViews.add(victim);
5981 // FIXME: Unfortunately, ViewCompat.setAccessibilityDelegate() doesn't accept
5982 // null delegates.
5983 if (Build.VERSION.SDK_INT >= 14) {
5984 victim.setAccessibilityDelegate(null);
5985 }
5987 if (mRecyclerListener != null) {
5988 mRecyclerListener.onMovedToScrapHeap(victim);
5989 }
5990 }
5991 }
5993 pruneScrapViews();
5994 }
5996 private void pruneScrapViews() {
5997 final int maxViews = mActiveViews.length;
5998 final int viewTypeCount = mViewTypeCount;
5999 final ArrayList<View>[] scrapViews = mScrapViews;
6001 for (int i = 0; i < viewTypeCount; ++i) {
6002 final ArrayList<View> scrapPile = scrapViews[i];
6003 int size = scrapPile.size();
6004 final int extras = size - maxViews;
6006 size--;
6008 for (int j = 0; j < extras; j++) {
6009 removeDetachedView(scrapPile.remove(size--), false);
6010 }
6011 }
6013 if (mTransientStateViews != null) {
6014 for (int i = 0; i < mTransientStateViews.size(); i++) {
6015 final View v = mTransientStateViews.valueAt(i);
6016 if (!ViewCompat.hasTransientState(v)) {
6017 mTransientStateViews.removeAt(i);
6018 i--;
6019 }
6020 }
6021 }
6022 }
6024 void reclaimScrapViews(List<View> views) {
6025 if (mViewTypeCount == 1) {
6026 views.addAll(mCurrentScrap);
6027 } else {
6028 final int viewTypeCount = mViewTypeCount;
6029 final ArrayList<View>[] scrapViews = mScrapViews;
6031 for (int i = 0; i < viewTypeCount; ++i) {
6032 final ArrayList<View> scrapPile = scrapViews[i];
6033 views.addAll(scrapPile);
6034 }
6035 }
6036 }
6038 View retrieveFromScrap(ArrayList<View> scrapViews, int position) {
6039 int size = scrapViews.size();
6040 if (size <= 0) {
6041 return null;
6042 }
6044 for (int i = 0; i < size; i++) {
6045 final View scrapView = scrapViews.get(i);
6046 final LayoutParams lp = (LayoutParams) scrapView.getLayoutParams();
6048 if (lp.scrappedFromPosition == position) {
6049 scrapViews.remove(i);
6050 return scrapView;
6051 }
6052 }
6054 return scrapViews.remove(size - 1);
6055 }
6056 }
6058 @Override
6059 public void setEmptyView(View emptyView) {
6060 super.setEmptyView(emptyView);
6061 mEmptyView = emptyView;
6062 updateEmptyStatus();
6063 }
6065 @Override
6066 public void setFocusable(boolean focusable) {
6067 final ListAdapter adapter = getAdapter();
6068 final boolean empty = (adapter == null || adapter.getCount() == 0);
6070 mDesiredFocusableState = focusable;
6071 if (!focusable) {
6072 mDesiredFocusableInTouchModeState = false;
6073 }
6075 super.setFocusable(focusable && !empty);
6076 }
6078 @Override
6079 public void setFocusableInTouchMode(boolean focusable) {
6080 final ListAdapter adapter = getAdapter();
6081 final boolean empty = (adapter == null || adapter.getCount() == 0);
6083 mDesiredFocusableInTouchModeState = focusable;
6084 if (focusable) {
6085 mDesiredFocusableState = true;
6086 }
6088 super.setFocusableInTouchMode(focusable && !empty);
6089 }
6091 private void checkFocus() {
6092 final ListAdapter adapter = getAdapter();
6093 final boolean focusable = (adapter != null && adapter.getCount() > 0);
6095 // The order in which we set focusable in touch mode/focusable may matter
6096 // for the client, see View.setFocusableInTouchMode() comments for more
6097 // details
6098 super.setFocusableInTouchMode(focusable && mDesiredFocusableInTouchModeState);
6099 super.setFocusable(focusable && mDesiredFocusableState);
6101 if (mEmptyView != null) {
6102 updateEmptyStatus();
6103 }
6104 }
6106 private void updateEmptyStatus() {
6107 final boolean isEmpty = (mAdapter == null || mAdapter.isEmpty());
6109 if (isEmpty) {
6110 if (mEmptyView != null) {
6111 mEmptyView.setVisibility(View.VISIBLE);
6112 setVisibility(View.GONE);
6113 } else {
6114 // If the caller just removed our empty view, make sure the list
6115 // view is visible
6116 setVisibility(View.VISIBLE);
6117 }
6119 // We are now GONE, so pending layouts will not be dispatched.
6120 // Force one here to make sure that the state of the list matches
6121 // the state of the adapter.
6122 if (mDataChanged) {
6123 onLayout(false, getLeft(), getTop(), getRight(), getBottom());
6124 }
6125 } else {
6126 if (mEmptyView != null) {
6127 mEmptyView.setVisibility(View.GONE);
6128 }
6130 setVisibility(View.VISIBLE);
6131 }
6132 }
6134 private class AdapterDataSetObserver extends DataSetObserver {
6135 private Parcelable mInstanceState = null;
6137 @Override
6138 public void onChanged() {
6139 mDataChanged = true;
6140 mOldItemCount = mItemCount;
6141 mItemCount = getAdapter().getCount();
6143 // Detect the case where a cursor that was previously invalidated has
6144 // been re-populated with new data.
6145 if (TwoWayView.this.mHasStableIds && mInstanceState != null
6146 && mOldItemCount == 0 && mItemCount > 0) {
6147 TwoWayView.this.onRestoreInstanceState(mInstanceState);
6148 mInstanceState = null;
6149 } else {
6150 rememberSyncState();
6151 }
6153 checkFocus();
6154 requestLayout();
6155 }
6157 @Override
6158 public void onInvalidated() {
6159 mDataChanged = true;
6161 if (TwoWayView.this.mHasStableIds) {
6162 // Remember the current state for the case where our hosting activity is being
6163 // stopped and later restarted
6164 mInstanceState = TwoWayView.this.onSaveInstanceState();
6165 }
6167 // Data is invalid so we should reset our state
6168 mOldItemCount = mItemCount;
6169 mItemCount = 0;
6171 mSelectedPosition = INVALID_POSITION;
6172 mSelectedRowId = INVALID_ROW_ID;
6174 mNextSelectedPosition = INVALID_POSITION;
6175 mNextSelectedRowId = INVALID_ROW_ID;
6177 mNeedSync = false;
6179 checkFocus();
6180 requestLayout();
6181 }
6182 }
6184 static class SavedState extends BaseSavedState {
6185 long selectedId;
6186 long firstId;
6187 int viewStart;
6188 int position;
6189 int height;
6190 int checkedItemCount;
6191 SparseBooleanArray checkState;
6192 LongSparseArray<Integer> checkIdState;
6194 /**
6195 * Constructor called from {@link TwoWayView#onSaveInstanceState()}
6196 */
6197 SavedState(Parcelable superState) {
6198 super(superState);
6199 }
6201 /**
6202 * Constructor called from {@link #CREATOR}
6203 */
6204 private SavedState(Parcel in) {
6205 super(in);
6207 selectedId = in.readLong();
6208 firstId = in.readLong();
6209 viewStart = in.readInt();
6210 position = in.readInt();
6211 height = in.readInt();
6213 checkedItemCount = in.readInt();
6214 checkState = in.readSparseBooleanArray();
6216 final int N = in.readInt();
6217 if (N > 0) {
6218 checkIdState = new LongSparseArray<Integer>();
6219 for (int i = 0; i < N; i++) {
6220 final long key = in.readLong();
6221 final int value = in.readInt();
6222 checkIdState.put(key, value);
6223 }
6224 }
6225 }
6227 @Override
6228 public void writeToParcel(Parcel out, int flags) {
6229 super.writeToParcel(out, flags);
6231 out.writeLong(selectedId);
6232 out.writeLong(firstId);
6233 out.writeInt(viewStart);
6234 out.writeInt(position);
6235 out.writeInt(height);
6237 out.writeInt(checkedItemCount);
6238 out.writeSparseBooleanArray(checkState);
6240 final int N = checkIdState != null ? checkIdState.size() : 0;
6241 out.writeInt(N);
6243 for (int i = 0; i < N; i++) {
6244 out.writeLong(checkIdState.keyAt(i));
6245 out.writeInt(checkIdState.valueAt(i));
6246 }
6247 }
6249 @Override
6250 public String toString() {
6251 return "TwoWayView.SavedState{"
6252 + Integer.toHexString(System.identityHashCode(this))
6253 + " selectedId=" + selectedId
6254 + " firstId=" + firstId
6255 + " viewStart=" + viewStart
6256 + " height=" + height
6257 + " position=" + position
6258 + " checkState=" + checkState + "}";
6259 }
6261 public static final Parcelable.Creator<SavedState> CREATOR
6262 = new Parcelable.Creator<SavedState>() {
6263 @Override
6264 public SavedState createFromParcel(Parcel in) {
6265 return new SavedState(in);
6266 }
6268 @Override
6269 public SavedState[] newArray(int size) {
6270 return new SavedState[size];
6271 }
6272 };
6273 }
6275 private class SelectionNotifier implements Runnable {
6276 @Override
6277 public void run() {
6278 if (mDataChanged) {
6279 // Data has changed between when this SelectionNotifier
6280 // was posted and now. We need to wait until the AdapterView
6281 // has been synched to the new data.
6282 if (mAdapter != null) {
6283 post(this);
6284 }
6285 } else {
6286 fireOnSelected();
6287 performAccessibilityActionsOnSelected();
6288 }
6289 }
6290 }
6292 private class WindowRunnnable {
6293 private int mOriginalAttachCount;
6295 public void rememberWindowAttachCount() {
6296 mOriginalAttachCount = getWindowAttachCount();
6297 }
6299 public boolean sameWindow() {
6300 return hasWindowFocus() && getWindowAttachCount() == mOriginalAttachCount;
6301 }
6302 }
6304 private class PerformClick extends WindowRunnnable implements Runnable {
6305 int mClickMotionPosition;
6307 @Override
6308 public void run() {
6309 if (mDataChanged) {
6310 return;
6311 }
6313 final ListAdapter adapter = mAdapter;
6314 final int motionPosition = mClickMotionPosition;
6316 if (adapter != null && mItemCount > 0 &&
6317 motionPosition != INVALID_POSITION &&
6318 motionPosition < adapter.getCount() && sameWindow()) {
6320 final View child = getChildAt(motionPosition - mFirstPosition);
6321 if (child != null) {
6322 performItemClick(child, motionPosition, adapter.getItemId(motionPosition));
6323 }
6324 }
6325 }
6326 }
6328 private final class CheckForTap implements Runnable {
6329 @Override
6330 public void run() {
6331 if (mTouchMode != TOUCH_MODE_DOWN) {
6332 return;
6333 }
6335 mTouchMode = TOUCH_MODE_TAP;
6337 final View child = getChildAt(mMotionPosition - mFirstPosition);
6338 if (child != null && !child.hasFocusable()) {
6339 mLayoutMode = LAYOUT_NORMAL;
6341 if (!mDataChanged) {
6342 setPressed(true);
6343 child.setPressed(true);
6345 layoutChildren();
6346 positionSelector(mMotionPosition, child);
6347 refreshDrawableState();
6349 positionSelector(mMotionPosition, child);
6350 refreshDrawableState();
6352 final boolean longClickable = isLongClickable();
6354 if (mSelector != null) {
6355 Drawable d = mSelector.getCurrent();
6357 if (d != null && d instanceof TransitionDrawable) {
6358 if (longClickable) {
6359 final int longPressTimeout = ViewConfiguration.getLongPressTimeout();
6360 ((TransitionDrawable) d).startTransition(longPressTimeout);
6361 } else {
6362 ((TransitionDrawable) d).resetTransition();
6363 }
6364 }
6365 }
6367 if (longClickable) {
6368 triggerCheckForLongPress();
6369 } else {
6370 mTouchMode = TOUCH_MODE_DONE_WAITING;
6371 }
6372 } else {
6373 mTouchMode = TOUCH_MODE_DONE_WAITING;
6374 }
6375 }
6376 }
6377 }
6379 private class CheckForLongPress extends WindowRunnnable implements Runnable {
6380 @Override
6381 public void run() {
6382 final int motionPosition = mMotionPosition;
6383 final View child = getChildAt(motionPosition - mFirstPosition);
6385 if (child != null) {
6386 final long longPressId = mAdapter.getItemId(mMotionPosition);
6388 boolean handled = false;
6389 if (sameWindow() && !mDataChanged) {
6390 handled = performLongPress(child, motionPosition, longPressId);
6391 }
6393 if (handled) {
6394 mTouchMode = TOUCH_MODE_REST;
6395 setPressed(false);
6396 child.setPressed(false);
6397 } else {
6398 mTouchMode = TOUCH_MODE_DONE_WAITING;
6399 }
6400 }
6401 }
6402 }
6404 private class CheckForKeyLongPress extends WindowRunnnable implements Runnable {
6405 public void run() {
6406 if (!isPressed() || mSelectedPosition < 0) {
6407 return;
6408 }
6410 final int index = mSelectedPosition - mFirstPosition;
6411 final View v = getChildAt(index);
6413 if (!mDataChanged) {
6414 boolean handled = false;
6416 if (sameWindow()) {
6417 handled = performLongPress(v, mSelectedPosition, mSelectedRowId);
6418 }
6420 if (handled) {
6421 setPressed(false);
6422 v.setPressed(false);
6423 }
6424 } else {
6425 setPressed(false);
6427 if (v != null) {
6428 v.setPressed(false);
6429 }
6430 }
6431 }
6432 }
6434 private static class ArrowScrollFocusResult {
6435 private int mSelectedPosition;
6436 private int mAmountToScroll;
6438 /**
6439 * How {@link TwoWayView#arrowScrollFocused} returns its values.
6440 */
6441 void populate(int selectedPosition, int amountToScroll) {
6442 mSelectedPosition = selectedPosition;
6443 mAmountToScroll = amountToScroll;
6444 }
6446 public int getSelectedPosition() {
6447 return mSelectedPosition;
6448 }
6450 public int getAmountToScroll() {
6451 return mAmountToScroll;
6452 }
6453 }
6455 private class ListItemAccessibilityDelegate extends AccessibilityDelegateCompat {
6456 @Override
6457 public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
6458 super.onInitializeAccessibilityNodeInfo(host, info);
6460 final int position = getPositionForView(host);
6461 final ListAdapter adapter = getAdapter();
6463 // Cannot perform actions on invalid items
6464 if (position == INVALID_POSITION || adapter == null) {
6465 return;
6466 }
6468 // Cannot perform actions on disabled items
6469 if (!isEnabled() || !adapter.isEnabled(position)) {
6470 return;
6471 }
6473 if (position == getSelectedItemPosition()) {
6474 info.setSelected(true);
6475 info.addAction(AccessibilityNodeInfoCompat.ACTION_CLEAR_SELECTION);
6476 } else {
6477 info.addAction(AccessibilityNodeInfoCompat.ACTION_SELECT);
6478 }
6480 if (isClickable()) {
6481 info.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
6482 info.setClickable(true);
6483 }
6485 if (isLongClickable()) {
6486 info.addAction(AccessibilityNodeInfoCompat.ACTION_LONG_CLICK);
6487 info.setLongClickable(true);
6488 }
6489 }
6491 @Override
6492 public boolean performAccessibilityAction(View host, int action, Bundle arguments) {
6493 if (super.performAccessibilityAction(host, action, arguments)) {
6494 return true;
6495 }
6497 final int position = getPositionForView(host);
6498 final ListAdapter adapter = getAdapter();
6500 // Cannot perform actions on invalid items
6501 if (position == INVALID_POSITION || adapter == null) {
6502 return false;
6503 }
6505 // Cannot perform actions on disabled items
6506 if (!isEnabled() || !adapter.isEnabled(position)) {
6507 return false;
6508 }
6510 final long id = getItemIdAtPosition(position);
6512 switch (action) {
6513 case AccessibilityNodeInfoCompat.ACTION_CLEAR_SELECTION:
6514 if (getSelectedItemPosition() == position) {
6515 setSelection(INVALID_POSITION);
6516 return true;
6517 }
6518 return false;
6520 case AccessibilityNodeInfoCompat.ACTION_SELECT:
6521 if (getSelectedItemPosition() != position) {
6522 setSelection(position);
6523 return true;
6524 }
6525 return false;
6527 case AccessibilityNodeInfoCompat.ACTION_CLICK:
6528 if (isClickable()) {
6529 return performItemClick(host, position, id);
6530 }
6531 return false;
6533 case AccessibilityNodeInfoCompat.ACTION_LONG_CLICK:
6534 if (isLongClickable()) {
6535 return performLongPress(host, position, id);
6536 }
6537 return false;
6538 }
6540 return false;
6541 }
6542 }
6543 }