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