1 /* 2 * Copyright (C) 2006 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.widget; 18 19 import android.annotation.ColorInt; 20 import android.annotation.DrawableRes; 21 import android.annotation.NonNull; 22 import android.annotation.TestApi; 23 import android.app.ActivityThread; 24 import android.compat.annotation.UnsupportedAppUsage; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.res.Configuration; 28 import android.content.res.TypedArray; 29 import android.graphics.Canvas; 30 import android.graphics.Rect; 31 import android.graphics.drawable.Drawable; 32 import android.graphics.drawable.TransitionDrawable; 33 import android.os.Build; 34 import android.os.Bundle; 35 import android.os.Debug; 36 import android.os.Handler; 37 import android.os.Parcel; 38 import android.os.Parcelable; 39 import android.os.StrictMode; 40 import android.os.Trace; 41 import android.provider.DeviceConfig; 42 import android.text.Editable; 43 import android.text.InputType; 44 import android.text.TextUtils; 45 import android.text.TextWatcher; 46 import android.util.AttributeSet; 47 import android.util.Log; 48 import android.util.LongSparseArray; 49 import android.util.SparseArray; 50 import android.util.SparseBooleanArray; 51 import android.util.StateSet; 52 import android.view.ActionMode; 53 import android.view.ContextMenu.ContextMenuInfo; 54 import android.view.Gravity; 55 import android.view.HapticFeedbackConstants; 56 import android.view.InputDevice; 57 import android.view.KeyEvent; 58 import android.view.LayoutInflater; 59 import android.view.Menu; 60 import android.view.MenuItem; 61 import android.view.MotionEvent; 62 import android.view.PointerIcon; 63 import android.view.VelocityTracker; 64 import android.view.View; 65 import android.view.ViewConfiguration; 66 import android.view.ViewDebug; 67 import android.view.ViewGroup; 68 import android.view.ViewHierarchyEncoder; 69 import android.view.ViewParent; 70 import android.view.ViewStructure; 71 import android.view.ViewTreeObserver; 72 import android.view.accessibility.AccessibilityEvent; 73 import android.view.accessibility.AccessibilityManager; 74 import android.view.accessibility.AccessibilityNodeInfo; 75 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; 76 import android.view.accessibility.AccessibilityNodeInfo.CollectionInfo; 77 import android.view.animation.Interpolator; 78 import android.view.animation.LinearInterpolator; 79 import android.view.autofill.AutofillId; 80 import android.view.contentcapture.ContentCaptureManager; 81 import android.view.contentcapture.ContentCaptureSession; 82 import android.view.inputmethod.BaseInputConnection; 83 import android.view.inputmethod.CompletionInfo; 84 import android.view.inputmethod.CorrectionInfo; 85 import android.view.inputmethod.EditorInfo; 86 import android.view.inputmethod.ExtractedText; 87 import android.view.inputmethod.ExtractedTextRequest; 88 import android.view.inputmethod.InputConnection; 89 import android.view.inputmethod.InputContentInfo; 90 import android.view.inputmethod.InputMethodManager; 91 import android.view.inputmethod.SurroundingText; 92 import android.view.inspector.InspectableProperty; 93 import android.view.inspector.InspectableProperty.EnumEntry; 94 import android.widget.RemoteViews.InteractionHandler; 95 96 import com.android.internal.R; 97 import com.android.internal.annotations.VisibleForTesting; 98 99 import java.util.ArrayList; 100 import java.util.List; 101 102 /** 103 * Base class that can be used to implement virtualized lists of items. A list does 104 * not have a spatial definition here. For instance, subclasses of this class can 105 * display the content of the list in a grid, in a carousel, as stack, etc. 106 * 107 * @attr ref android.R.styleable#AbsListView_listSelector 108 * @attr ref android.R.styleable#AbsListView_drawSelectorOnTop 109 * @attr ref android.R.styleable#AbsListView_stackFromBottom 110 * @attr ref android.R.styleable#AbsListView_scrollingCache 111 * @attr ref android.R.styleable#AbsListView_textFilterEnabled 112 * @attr ref android.R.styleable#AbsListView_transcriptMode 113 * @attr ref android.R.styleable#AbsListView_cacheColorHint 114 * @attr ref android.R.styleable#AbsListView_fastScrollEnabled 115 * @attr ref android.R.styleable#AbsListView_smoothScrollbar 116 * @attr ref android.R.styleable#AbsListView_choiceMode 117 */ 118 public abstract class AbsListView extends AdapterView<ListAdapter> implements TextWatcher, 119 ViewTreeObserver.OnGlobalLayoutListener, Filter.FilterListener, 120 ViewTreeObserver.OnTouchModeChangeListener, 121 RemoteViewsAdapter.RemoteAdapterConnectionCallback { 122 123 @SuppressWarnings("UnusedDeclaration") 124 private static final String TAG = "AbsListView"; 125 126 /** 127 * Disables the transcript mode. 128 * 129 * @see #setTranscriptMode(int) 130 */ 131 public static final int TRANSCRIPT_MODE_DISABLED = 0; 132 133 /** 134 * The list will automatically scroll to the bottom when a data set change 135 * notification is received and only if the last item is already visible 136 * on screen. 137 * 138 * @see #setTranscriptMode(int) 139 */ 140 public static final int TRANSCRIPT_MODE_NORMAL = 1; 141 142 /** 143 * The list will automatically scroll to the bottom, no matter what items 144 * are currently visible. 145 * 146 * @see #setTranscriptMode(int) 147 */ 148 public static final int TRANSCRIPT_MODE_ALWAYS_SCROLL = 2; 149 150 /** 151 * Indicates that we are not in the middle of a touch gesture 152 */ 153 static final int TOUCH_MODE_REST = -1; 154 155 /** 156 * Indicates we just received the touch event and we are waiting to see if the it is a tap or a 157 * scroll gesture. 158 */ 159 static final int TOUCH_MODE_DOWN = 0; 160 161 /** 162 * Indicates the touch has been recognized as a tap and we are now waiting to see if the touch 163 * is a longpress 164 */ 165 static final int TOUCH_MODE_TAP = 1; 166 167 /** 168 * Indicates we have waited for everything we can wait for, but the user's finger is still down 169 */ 170 static final int TOUCH_MODE_DONE_WAITING = 2; 171 172 /** 173 * Indicates the touch gesture is a scroll 174 */ 175 static final int TOUCH_MODE_SCROLL = 3; 176 177 /** 178 * Indicates the view is in the process of being flung 179 */ 180 static final int TOUCH_MODE_FLING = 4; 181 182 /** 183 * Indicates the touch gesture is an overscroll - a scroll beyond the beginning or end. 184 */ 185 static final int TOUCH_MODE_OVERSCROLL = 5; 186 187 /** 188 * Indicates the view is being flung outside of normal content bounds 189 * and will spring back. 190 */ 191 static final int TOUCH_MODE_OVERFLING = 6; 192 193 /** 194 * Regular layout - usually an unsolicited layout from the view system 195 */ 196 static final int LAYOUT_NORMAL = 0; 197 198 /** 199 * Show the first item 200 */ 201 static final int LAYOUT_FORCE_TOP = 1; 202 203 /** 204 * Force the selected item to be on somewhere on the screen 205 */ 206 static final int LAYOUT_SET_SELECTION = 2; 207 208 /** 209 * Show the last item 210 */ 211 static final int LAYOUT_FORCE_BOTTOM = 3; 212 213 /** 214 * Make a mSelectedItem appear in a specific location and build the rest of 215 * the views from there. The top is specified by mSpecificTop. 216 */ 217 static final int LAYOUT_SPECIFIC = 4; 218 219 /** 220 * Layout to sync as a result of a data change. Restore mSyncPosition to have its top 221 * at mSpecificTop 222 */ 223 static final int LAYOUT_SYNC = 5; 224 225 /** 226 * Layout as a result of using the navigation keys 227 */ 228 static final int LAYOUT_MOVE_SELECTION = 6; 229 230 /** 231 * Normal list that does not indicate choices 232 */ 233 public static final int CHOICE_MODE_NONE = 0; 234 235 /** 236 * The list allows up to one choice 237 */ 238 public static final int CHOICE_MODE_SINGLE = 1; 239 240 /** 241 * The list allows multiple choices 242 */ 243 public static final int CHOICE_MODE_MULTIPLE = 2; 244 245 /** 246 * The list allows multiple choices in a modal selection mode 247 */ 248 public static final int CHOICE_MODE_MULTIPLE_MODAL = 3; 249 250 /** 251 * The thread that created this view. 252 */ 253 private final Thread mOwnerThread; 254 255 /** 256 * Controls if/how the user may choose/check items in the list 257 */ 258 int mChoiceMode = CHOICE_MODE_NONE; 259 260 /** 261 * Controls CHOICE_MODE_MULTIPLE_MODAL. null when inactive. 262 */ 263 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 264 ActionMode mChoiceActionMode; 265 266 /** 267 * Wrapper for the multiple choice mode callback; AbsListView needs to perform 268 * a few extra actions around what application code does. 269 */ 270 MultiChoiceModeWrapper mMultiChoiceModeCallback; 271 272 /** 273 * Running count of how many items are currently checked 274 */ 275 int mCheckedItemCount; 276 277 /** 278 * Running state of which positions are currently checked 279 */ 280 SparseBooleanArray mCheckStates; 281 282 /** 283 * Running state of which IDs are currently checked. 284 * If there is a value for a given key, the checked state for that ID is true 285 * and the value holds the last known position in the adapter for that id. 286 */ 287 LongSparseArray<Integer> mCheckedIdStates; 288 289 /** 290 * Controls how the next layout will happen 291 */ 292 @UnsupportedAppUsage 293 int mLayoutMode = LAYOUT_NORMAL; 294 295 /** 296 * Should be used by subclasses to listen to changes in the dataset 297 */ 298 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 299 AdapterDataSetObserver mDataSetObserver; 300 301 /** 302 * The adapter containing the data to be displayed by this view 303 */ 304 @UnsupportedAppUsage 305 ListAdapter mAdapter; 306 307 /** 308 * The remote adapter containing the data to be displayed by this view to be set 309 */ 310 private RemoteViewsAdapter mRemoteAdapter; 311 312 /** 313 * If mAdapter != null, whenever this is true the adapter has stable IDs. 314 */ 315 boolean mAdapterHasStableIds; 316 317 /** 318 * This flag indicates the a full notify is required when the RemoteViewsAdapter connects 319 */ 320 private boolean mDeferNotifyDataSetChanged = false; 321 322 /** 323 * Indicates whether the list selector should be drawn on top of the children or behind 324 */ 325 boolean mDrawSelectorOnTop = false; 326 327 /** 328 * The drawable used to draw the selector 329 */ 330 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) 331 Drawable mSelector; 332 333 /** 334 * The current position of the selector in the list. 335 */ 336 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) 337 int mSelectorPosition = INVALID_POSITION; 338 339 /** 340 * Defines the selector's location and dimension at drawing time 341 */ 342 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) 343 Rect mSelectorRect = new Rect(); 344 345 /** 346 * The data set used to store unused views that should be reused during the next layout 347 * to avoid creating new ones 348 */ 349 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123769398) 350 final RecycleBin mRecycler = new RecycleBin(); 351 352 /** 353 * The selection's left padding 354 */ 355 int mSelectionLeftPadding = 0; 356 357 /** 358 * The selection's top padding 359 */ 360 @UnsupportedAppUsage 361 int mSelectionTopPadding = 0; 362 363 /** 364 * The selection's right padding 365 */ 366 int mSelectionRightPadding = 0; 367 368 /** 369 * The selection's bottom padding 370 */ 371 @UnsupportedAppUsage 372 int mSelectionBottomPadding = 0; 373 374 /** 375 * This view's padding 376 */ 377 Rect mListPadding = new Rect(); 378 379 /** 380 * Subclasses must retain their measure spec from onMeasure() into this member 381 */ 382 int mWidthMeasureSpec = 0; 383 384 /** 385 * The top scroll indicator 386 */ 387 View mScrollUp; 388 389 /** 390 * The down scroll indicator 391 */ 392 View mScrollDown; 393 394 /** 395 * When the view is scrolling, this flag is set to true to indicate subclasses that 396 * the drawing cache was enabled on the children 397 */ 398 boolean mCachingStarted; 399 boolean mCachingActive; 400 401 /** 402 * The position of the view that received the down motion event 403 */ 404 @UnsupportedAppUsage 405 int mMotionPosition; 406 407 /** 408 * The offset to the top of the mMotionPosition view when the down motion event was received 409 */ 410 int mMotionViewOriginalTop; 411 412 /** 413 * The desired offset to the top of the mMotionPosition view after a scroll 414 */ 415 int mMotionViewNewTop; 416 417 /** 418 * The X value associated with the the down motion event 419 */ 420 int mMotionX; 421 422 /** 423 * The Y value associated with the the down motion event 424 */ 425 @UnsupportedAppUsage 426 int mMotionY; 427 428 /** 429 * One of TOUCH_MODE_REST, TOUCH_MODE_DOWN, TOUCH_MODE_TAP, TOUCH_MODE_SCROLL, or 430 * TOUCH_MODE_DONE_WAITING 431 */ 432 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123769413) 433 int mTouchMode = TOUCH_MODE_REST; 434 435 /** 436 * Y value from on the previous motion event (if any) 437 */ 438 int mLastY; 439 440 /** 441 * How far the finger moved before we started scrolling 442 */ 443 int mMotionCorrection; 444 445 /** 446 * Determines speed during touch scrolling 447 */ 448 @UnsupportedAppUsage 449 private VelocityTracker mVelocityTracker; 450 451 /** 452 * Handles one frame of a fling 453 * 454 * To interrupt a fling early you should use smoothScrollBy(0,0) instead 455 */ 456 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) 457 private FlingRunnable mFlingRunnable; 458 459 /** 460 * Handles scrolling between positions within the list. 461 */ 462 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 463 AbsPositionScroller mPositionScroller; 464 465 /** 466 * The offset in pixels form the top of the AdapterView to the top 467 * of the currently selected view. Used to save and restore state. 468 */ 469 int mSelectedTop = 0; 470 471 /** 472 * Indicates whether the list is stacked from the bottom edge or 473 * the top edge. 474 */ 475 boolean mStackFromBottom; 476 477 /** 478 * When set to true, the list automatically discards the children's 479 * bitmap cache after scrolling. 480 */ 481 boolean mScrollingCacheEnabled; 482 483 /** 484 * Whether or not to enable the fast scroll feature on this list 485 */ 486 boolean mFastScrollEnabled; 487 488 /** 489 * Whether or not to always show the fast scroll feature on this list 490 */ 491 boolean mFastScrollAlwaysVisible; 492 493 /** 494 * Optional callback to notify client when scroll position has changed 495 */ 496 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123769353) 497 private OnScrollListener mOnScrollListener; 498 499 /** 500 * Keeps track of our accessory window 501 */ 502 @UnsupportedAppUsage 503 PopupWindow mPopup; 504 505 /** 506 * Used with type filter window 507 */ 508 EditText mTextFilter; 509 510 /** 511 * Indicates whether to use pixels-based or position-based scrollbar 512 * properties. 513 */ 514 private boolean mSmoothScrollbarEnabled = true; 515 516 /** 517 * Indicates that this view supports filtering 518 */ 519 private boolean mTextFilterEnabled; 520 521 /** 522 * Indicates that this view is currently displaying a filtered view of the data 523 */ 524 private boolean mFiltered; 525 526 /** 527 * Rectangle used for hit testing children 528 */ 529 private Rect mTouchFrame; 530 531 /** 532 * The position to resurrect the selected position to. 533 */ 534 int mResurrectToPosition = INVALID_POSITION; 535 536 @UnsupportedAppUsage 537 private ContextMenuInfo mContextMenuInfo = null; 538 539 /** 540 * Maximum distance to record overscroll 541 */ 542 int mOverscrollMax; 543 544 /** 545 * Content height divided by this is the overscroll limit. 546 */ 547 static final int OVERSCROLL_LIMIT_DIVISOR = 3; 548 549 /** 550 * How many positions in either direction we will search to try to 551 * find a checked item with a stable ID that moved position across 552 * a data set change. If the item isn't found it will be unselected. 553 */ 554 private static final int CHECK_POSITION_SEARCH_DISTANCE = 20; 555 556 /** 557 * Used to request a layout when we changed touch mode 558 */ 559 private static final int TOUCH_MODE_UNKNOWN = -1; 560 private static final int TOUCH_MODE_ON = 0; 561 private static final int TOUCH_MODE_OFF = 1; 562 563 private int mLastTouchMode = TOUCH_MODE_UNKNOWN; 564 565 private static final boolean PROFILE_SCROLLING = false; 566 private boolean mScrollProfilingStarted = false; 567 568 private static final boolean PROFILE_FLINGING = false; 569 private boolean mFlingProfilingStarted = false; 570 571 /** 572 * The StrictMode "critical time span" objects to catch animation 573 * stutters. Non-null when a time-sensitive animation is 574 * in-flight. Must call finish() on them when done animating. 575 * These are no-ops on user builds. 576 */ 577 private StrictMode.Span mScrollStrictSpan = null; 578 private StrictMode.Span mFlingStrictSpan = null; 579 580 /** 581 * The last CheckForLongPress runnable we posted, if any 582 */ 583 @UnsupportedAppUsage 584 private CheckForLongPress mPendingCheckForLongPress; 585 586 /** 587 * The last CheckForTap runnable we posted, if any 588 */ 589 @UnsupportedAppUsage 590 private CheckForTap mPendingCheckForTap; 591 592 /** 593 * The last CheckForKeyLongPress runnable we posted, if any 594 */ 595 private CheckForKeyLongPress mPendingCheckForKeyLongPress; 596 597 /** 598 * Acts upon click 599 */ 600 private AbsListView.PerformClick mPerformClick; 601 602 /** 603 * Delayed action for touch mode. 604 */ 605 private Runnable mTouchModeReset; 606 607 /** 608 * Whether the most recent touch event stream resulted in a successful 609 * long-press action. This is reset on TOUCH_DOWN. 610 */ 611 private boolean mHasPerformedLongPress; 612 613 /** 614 * This view is in transcript mode -- it shows the bottom of the list when the data 615 * changes 616 */ 617 private int mTranscriptMode; 618 619 /** 620 * Indicates that this list is always drawn on top of a solid, single-color, opaque 621 * background 622 */ 623 private int mCacheColorHint; 624 625 /** 626 * The select child's view (from the adapter's getView) is enabled. 627 */ 628 @UnsupportedAppUsage 629 private boolean mIsChildViewEnabled; 630 631 /** 632 * The cached drawable state for the selector. Accounts for child enabled 633 * state, but otherwise identical to the view's own drawable state. 634 */ 635 private int[] mSelectorState; 636 637 /** 638 * The last scroll state reported to clients through {@link OnScrollListener}. 639 */ 640 private int mLastScrollState = OnScrollListener.SCROLL_STATE_IDLE; 641 642 /** 643 * Indicates that reporting positions of child views to content capture is enabled via 644 * DeviceConfig. 645 */ 646 private static boolean sContentCaptureReportingEnabledByDeviceConfig = false; 647 648 /** 649 * Listens for changes to DeviceConfig properties and updates stored values accordingly. 650 */ 651 private static DeviceConfig.OnPropertiesChangedListener sDeviceConfigChangeListener = null; 652 653 /** 654 * Indicates that child positions of views should be reported to Content Capture the next time 655 * that active views are refreshed. 656 */ 657 private boolean mReportChildrenToContentCaptureOnNextUpdate = true; 658 659 /** 660 * Helper object that renders and controls the fast scroll thumb. 661 */ 662 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123768941) 663 private FastScroller mFastScroll; 664 665 /** 666 * Temporary holder for fast scroller style until a FastScroller object 667 * is created. 668 */ 669 private int mFastScrollStyle; 670 671 private boolean mGlobalLayoutListenerAddedFilter; 672 673 @UnsupportedAppUsage 674 private int mTouchSlop; 675 private float mDensityScale; 676 677 private float mVerticalScrollFactor; 678 679 private InputConnection mDefInputConnection; 680 private InputConnectionWrapper mPublicInputConnection; 681 682 private Runnable mClearScrollingCache; 683 Runnable mPositionScrollAfterLayout; 684 private int mMinimumVelocity; 685 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 124051740) 686 private int mMaximumVelocity; 687 private float mVelocityScale = 1.0f; 688 689 final boolean[] mIsScrap = new boolean[1]; 690 691 private final int[] mScrollOffset = new int[2]; 692 private final int[] mScrollConsumed = new int[2]; 693 694 private final float[] mTmpPoint = new float[2]; 695 696 // Used for offsetting MotionEvents that we feed to the VelocityTracker. 697 // In the future it would be nice to be able to give this to the VelocityTracker 698 // directly, or alternatively put a VT into absolute-positioning mode that only 699 // reads the raw screen-coordinate x/y values. 700 private int mNestedYOffset = 0; 701 702 // True when the popup should be hidden because of a call to 703 // dispatchDisplayHint() 704 private boolean mPopupHidden; 705 706 /** 707 * ID of the active pointer. This is used to retain consistency during 708 * drags/flings if multiple pointers are used. 709 */ 710 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) 711 private int mActivePointerId = INVALID_POINTER; 712 713 /** 714 * Sentinel value for no current active pointer. 715 * Used by {@link #mActivePointerId}. 716 */ 717 private static final int INVALID_POINTER = -1; 718 719 /** 720 * Maximum distance to overscroll by during edge effects 721 */ 722 @UnsupportedAppUsage 723 int mOverscrollDistance; 724 725 /** 726 * Maximum distance to overfling during edge effects 727 */ 728 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123769379) 729 int mOverflingDistance; 730 731 // These two EdgeGlows are always set and used together. 732 // Checking one for null is as good as checking both. 733 734 /** 735 * Tracks the state of the top edge glow. 736 * 737 * Even though this field is practically final, we cannot make it final because there are apps 738 * setting it via reflection and they need to keep working until they target Q. 739 * @hide 740 */ 741 @NonNull 742 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123769408) 743 @VisibleForTesting 744 public EdgeEffect mEdgeGlowTop; 745 746 /** 747 * Tracks the state of the bottom edge glow. 748 * 749 * Even though this field is practically final, we cannot make it final because there are apps 750 * setting it via reflection and they need to keep working until they target Q. 751 * @hide 752 */ 753 @NonNull 754 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123768444) 755 @VisibleForTesting 756 public EdgeEffect mEdgeGlowBottom; 757 758 /** 759 * An estimate of how many pixels are between the top of the list and 760 * the top of the first position in the adapter, based on the last time 761 * we saw it. Used to hint where to draw edge glows. 762 */ 763 private int mFirstPositionDistanceGuess; 764 765 /** 766 * An estimate of how many pixels are between the bottom of the list and 767 * the bottom of the last position in the adapter, based on the last time 768 * we saw it. Used to hint where to draw edge glows. 769 */ 770 private int mLastPositionDistanceGuess; 771 772 /** 773 * Used for determining when to cancel out of overscroll. 774 */ 775 private int mDirection = 0; 776 777 /** 778 * Tracked on measurement in transcript mode. Makes sure that we can still pin to 779 * the bottom correctly on resizes. 780 */ 781 private boolean mForceTranscriptScroll; 782 783 /** 784 * Used for interacting with list items from an accessibility service. 785 */ 786 private ListItemAccessibilityDelegate mAccessibilityDelegate; 787 788 /** 789 * Track the item count from the last time we handled a data change. 790 */ 791 private int mLastHandledItemCount; 792 793 /** 794 * Used for smooth scrolling at a consistent rate 795 */ 796 static final Interpolator sLinearInterpolator = new LinearInterpolator(); 797 798 /** 799 * The saved state that we will be restoring from when we next sync. 800 * Kept here so that if we happen to be asked to save our state before 801 * the sync happens, we can return this existing data rather than losing 802 * it. 803 */ 804 private SavedState mPendingSync; 805 806 /** 807 * Whether the view is in the process of detaching from its window. 808 */ 809 private boolean mIsDetaching; 810 811 /** 812 * Interface definition for a callback to be invoked when the list or grid 813 * has been scrolled. 814 */ 815 public interface OnScrollListener { 816 817 /** 818 * The view is not scrolling. Note navigating the list using the trackball counts as 819 * being in the idle state since these transitions are not animated. 820 */ 821 public static int SCROLL_STATE_IDLE = 0; 822 823 /** 824 * The user is scrolling using touch, and their finger is still on the screen 825 */ 826 public static int SCROLL_STATE_TOUCH_SCROLL = 1; 827 828 /** 829 * The user had previously been scrolling using touch and had performed a fling. The 830 * animation is now coasting to a stop 831 */ 832 public static int SCROLL_STATE_FLING = 2; 833 834 /** 835 * Callback method to be invoked while the list view or grid view is being scrolled. If the 836 * view is being scrolled, this method will be called before the next frame of the scroll is 837 * rendered. In particular, it will be called before any calls to 838 * {@link Adapter#getView(int, View, ViewGroup)}. 839 * 840 * @param view The view whose scroll state is being reported 841 * 842 * @param scrollState The current scroll state. One of 843 * {@link #SCROLL_STATE_TOUCH_SCROLL} or {@link #SCROLL_STATE_IDLE}. 844 */ onScrollStateChanged(AbsListView view, int scrollState)845 public void onScrollStateChanged(AbsListView view, int scrollState); 846 847 /** 848 * Callback method to be invoked when the list or grid has been scrolled. This will be 849 * called after the scroll has completed 850 * @param view The view whose scroll state is being reported 851 * @param firstVisibleItem the index of the first visible cell (ignore if 852 * visibleItemCount == 0) 853 * @param visibleItemCount the number of visible cells 854 * @param totalItemCount the number of items in the list adapter 855 */ onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)856 public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, 857 int totalItemCount); 858 } 859 860 /** 861 * The top-level view of a list item can implement this interface to allow 862 * itself to modify the bounds of the selection shown for that item. 863 */ 864 public interface SelectionBoundsAdjuster { 865 /** 866 * Called to allow the list item to adjust the bounds shown for 867 * its selection. 868 * 869 * @param bounds On call, this contains the bounds the list has 870 * selected for the item (that is the bounds of the entire view). The 871 * values can be modified as desired. 872 */ adjustListItemSelectionBounds(Rect bounds)873 public void adjustListItemSelectionBounds(Rect bounds); 874 } 875 876 private static class DeviceConfigChangeListener 877 implements DeviceConfig.OnPropertiesChangedListener { 878 @Override onPropertiesChanged( @onNull DeviceConfig.Properties properties)879 public void onPropertiesChanged( 880 @NonNull DeviceConfig.Properties properties) { 881 if (!DeviceConfig.NAMESPACE_CONTENT_CAPTURE.equals(properties.getNamespace())) { 882 return; 883 } 884 885 for (String key : properties.getKeyset()) { 886 if (!ContentCaptureManager.DEVICE_CONFIG_PROPERTY_REPORT_LIST_VIEW_CHILDREN 887 .equals(key)) { 888 continue; 889 } 890 891 sContentCaptureReportingEnabledByDeviceConfig = properties.getBoolean(key, 892 false); 893 } 894 } 895 } 896 setupDeviceConfigProperties()897 private static void setupDeviceConfigProperties() { 898 if (sDeviceConfigChangeListener == null) { 899 sContentCaptureReportingEnabledByDeviceConfig = DeviceConfig.getBoolean( 900 DeviceConfig.NAMESPACE_CONTENT_CAPTURE, 901 ContentCaptureManager.DEVICE_CONFIG_PROPERTY_REPORT_LIST_VIEW_CHILDREN, 902 false); 903 sDeviceConfigChangeListener = new DeviceConfigChangeListener(); 904 DeviceConfig.addOnPropertiesChangedListener( 905 DeviceConfig.NAMESPACE_CONTENT_CAPTURE, 906 ActivityThread.currentApplication().getMainExecutor(), 907 sDeviceConfigChangeListener); 908 } 909 } 910 AbsListView(Context context)911 public AbsListView(Context context) { 912 super(context); 913 setupDeviceConfigProperties(); 914 mEdgeGlowBottom = new EdgeEffect(context); 915 mEdgeGlowTop = new EdgeEffect(context); 916 initAbsListView(); 917 918 mOwnerThread = Thread.currentThread(); 919 920 setVerticalScrollBarEnabled(true); 921 TypedArray a = context.obtainStyledAttributes(R.styleable.View); 922 initializeScrollbarsInternal(a); 923 a.recycle(); 924 } 925 AbsListView(Context context, AttributeSet attrs)926 public AbsListView(Context context, AttributeSet attrs) { 927 this(context, attrs, com.android.internal.R.attr.absListViewStyle); 928 } 929 AbsListView(Context context, AttributeSet attrs, int defStyleAttr)930 public AbsListView(Context context, AttributeSet attrs, int defStyleAttr) { 931 this(context, attrs, defStyleAttr, 0); 932 } 933 AbsListView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)934 public AbsListView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 935 super(context, attrs, defStyleAttr, defStyleRes); 936 setupDeviceConfigProperties(); 937 mEdgeGlowBottom = new EdgeEffect(context, attrs); 938 mEdgeGlowTop = new EdgeEffect(context, attrs); 939 initAbsListView(); 940 941 mOwnerThread = Thread.currentThread(); 942 943 final TypedArray a = context.obtainStyledAttributes( 944 attrs, R.styleable.AbsListView, defStyleAttr, defStyleRes); 945 saveAttributeDataForStyleable(context, R.styleable.AbsListView, attrs, a, defStyleAttr, 946 defStyleRes); 947 948 final Drawable selector = a.getDrawable(R.styleable.AbsListView_listSelector); 949 if (selector != null) { 950 setSelector(selector); 951 } 952 953 mDrawSelectorOnTop = a.getBoolean(R.styleable.AbsListView_drawSelectorOnTop, false); 954 955 setStackFromBottom(a.getBoolean( 956 R.styleable.AbsListView_stackFromBottom, false)); 957 setScrollingCacheEnabled(a.getBoolean( 958 R.styleable.AbsListView_scrollingCache, true)); 959 setTextFilterEnabled(a.getBoolean( 960 R.styleable.AbsListView_textFilterEnabled, false)); 961 setTranscriptMode(a.getInt( 962 R.styleable.AbsListView_transcriptMode, TRANSCRIPT_MODE_DISABLED)); 963 setCacheColorHint(a.getColor( 964 R.styleable.AbsListView_cacheColorHint, 0)); 965 setSmoothScrollbarEnabled(a.getBoolean( 966 R.styleable.AbsListView_smoothScrollbar, true)); 967 setChoiceMode(a.getInt( 968 R.styleable.AbsListView_choiceMode, CHOICE_MODE_NONE)); 969 970 setFastScrollEnabled(a.getBoolean( 971 R.styleable.AbsListView_fastScrollEnabled, false)); 972 setFastScrollStyle(a.getResourceId( 973 R.styleable.AbsListView_fastScrollStyle, 0)); 974 setFastScrollAlwaysVisible(a.getBoolean( 975 R.styleable.AbsListView_fastScrollAlwaysVisible, false)); 976 977 a.recycle(); 978 979 if (context.getResources().getConfiguration().uiMode == Configuration.UI_MODE_TYPE_WATCH) { 980 setRevealOnFocusHint(false); 981 } 982 } 983 initAbsListView()984 private void initAbsListView() { 985 // Setting focusable in touch mode will set the focusable property to true 986 setClickable(true); 987 setFocusableInTouchMode(true); 988 setWillNotDraw(false); 989 setAlwaysDrawnWithCacheEnabled(false); 990 setScrollingCacheEnabled(true); 991 992 final ViewConfiguration configuration = ViewConfiguration.get(mContext); 993 mTouchSlop = configuration.getScaledTouchSlop(); 994 mVerticalScrollFactor = configuration.getScaledVerticalScrollFactor(); 995 mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); 996 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); 997 mOverscrollDistance = configuration.getScaledOverscrollDistance(); 998 mOverflingDistance = configuration.getScaledOverflingDistance(); 999 1000 mDensityScale = getContext().getResources().getDisplayMetrics().density; 1001 } 1002 1003 /** 1004 * {@inheritDoc} 1005 */ 1006 @Override setAdapter(ListAdapter adapter)1007 public void setAdapter(ListAdapter adapter) { 1008 if (adapter != null) { 1009 mAdapterHasStableIds = mAdapter.hasStableIds(); 1010 if (mChoiceMode != CHOICE_MODE_NONE && mAdapterHasStableIds && 1011 mCheckedIdStates == null) { 1012 mCheckedIdStates = new LongSparseArray<Integer>(); 1013 } 1014 } 1015 clearChoices(); 1016 } 1017 1018 /** 1019 * Returns the number of items currently selected. This will only be valid 1020 * if the choice mode is not {@link #CHOICE_MODE_NONE} (default). 1021 * 1022 * <p>To determine the specific items that are currently selected, use one of 1023 * the <code>getChecked*</code> methods. 1024 * 1025 * @return The number of items currently selected 1026 * 1027 * @see #getCheckedItemPosition() 1028 * @see #getCheckedItemPositions() 1029 * @see #getCheckedItemIds() 1030 */ getCheckedItemCount()1031 public int getCheckedItemCount() { 1032 return mCheckedItemCount; 1033 } 1034 1035 /** 1036 * Returns the checked state of the specified position. The result is only 1037 * valid if the choice mode has been set to {@link #CHOICE_MODE_SINGLE} 1038 * or {@link #CHOICE_MODE_MULTIPLE}. 1039 * 1040 * @param position The item whose checked state to return 1041 * @return The item's checked state or <code>false</code> if choice mode 1042 * is invalid 1043 * 1044 * @see #setChoiceMode(int) 1045 */ isItemChecked(int position)1046 public boolean isItemChecked(int position) { 1047 if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null) { 1048 return mCheckStates.get(position); 1049 } 1050 1051 return false; 1052 } 1053 1054 /** 1055 * Returns the currently checked item. The result is only valid if the choice 1056 * mode has been set to {@link #CHOICE_MODE_SINGLE}. 1057 * 1058 * @return The position of the currently checked item or 1059 * {@link #INVALID_POSITION} if nothing is selected 1060 * 1061 * @see #setChoiceMode(int) 1062 */ getCheckedItemPosition()1063 public int getCheckedItemPosition() { 1064 if (mChoiceMode == CHOICE_MODE_SINGLE && mCheckStates != null && mCheckStates.size() == 1) { 1065 return mCheckStates.keyAt(0); 1066 } 1067 1068 return INVALID_POSITION; 1069 } 1070 1071 /** 1072 * Returns the set of checked items in the list. The result is only valid if 1073 * the choice mode has not been set to {@link #CHOICE_MODE_NONE}. 1074 * 1075 * @return A SparseBooleanArray which will return true for each call to 1076 * get(int position) where position is a checked position in the 1077 * list and false otherwise, or <code>null</code> if the choice 1078 * mode is set to {@link #CHOICE_MODE_NONE}. 1079 */ getCheckedItemPositions()1080 public SparseBooleanArray getCheckedItemPositions() { 1081 if (mChoiceMode != CHOICE_MODE_NONE) { 1082 return mCheckStates; 1083 } 1084 return null; 1085 } 1086 1087 /** 1088 * Returns the set of checked items ids. The result is only valid if the 1089 * choice mode has not been set to {@link #CHOICE_MODE_NONE} and the adapter 1090 * has stable IDs. ({@link ListAdapter#hasStableIds()} == {@code true}) 1091 * 1092 * @return A new array which contains the id of each checked item in the 1093 * list. 1094 */ getCheckedItemIds()1095 public long[] getCheckedItemIds() { 1096 if (mChoiceMode == CHOICE_MODE_NONE || mCheckedIdStates == null || mAdapter == null) { 1097 return new long[0]; 1098 } 1099 1100 final LongSparseArray<Integer> idStates = mCheckedIdStates; 1101 final int count = idStates.size(); 1102 final long[] ids = new long[count]; 1103 1104 for (int i = 0; i < count; i++) { 1105 ids[i] = idStates.keyAt(i); 1106 } 1107 1108 return ids; 1109 } 1110 1111 /** 1112 * Clear any choices previously set 1113 */ clearChoices()1114 public void clearChoices() { 1115 if (mCheckStates != null) { 1116 mCheckStates.clear(); 1117 } 1118 if (mCheckedIdStates != null) { 1119 mCheckedIdStates.clear(); 1120 } 1121 mCheckedItemCount = 0; 1122 } 1123 1124 /** 1125 * Sets the checked state of the specified position. The is only valid if 1126 * the choice mode has been set to {@link #CHOICE_MODE_SINGLE} or 1127 * {@link #CHOICE_MODE_MULTIPLE}. 1128 * 1129 * @param position The item whose checked state is to be checked 1130 * @param value The new checked state for the item 1131 */ setItemChecked(int position, boolean value)1132 public void setItemChecked(int position, boolean value) { 1133 if (mChoiceMode == CHOICE_MODE_NONE) { 1134 return; 1135 } 1136 1137 // Start selection mode if needed. We don't need to if we're unchecking something. 1138 if (value && mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL && mChoiceActionMode == null) { 1139 if (mMultiChoiceModeCallback == null || 1140 !mMultiChoiceModeCallback.hasWrappedCallback()) { 1141 throw new IllegalStateException("AbsListView: attempted to start selection mode " + 1142 "for CHOICE_MODE_MULTIPLE_MODAL but no choice mode callback was " + 1143 "supplied. Call setMultiChoiceModeListener to set a callback."); 1144 } 1145 mChoiceActionMode = startActionMode(mMultiChoiceModeCallback); 1146 } 1147 1148 final boolean itemCheckChanged; 1149 if (mChoiceMode == CHOICE_MODE_MULTIPLE || mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL) { 1150 boolean oldValue = mCheckStates.get(position); 1151 mCheckStates.put(position, value); 1152 if (mCheckedIdStates != null && mAdapter.hasStableIds()) { 1153 if (value) { 1154 mCheckedIdStates.put(mAdapter.getItemId(position), position); 1155 } else { 1156 mCheckedIdStates.delete(mAdapter.getItemId(position)); 1157 } 1158 } 1159 itemCheckChanged = oldValue != value; 1160 if (itemCheckChanged) { 1161 if (value) { 1162 mCheckedItemCount++; 1163 } else { 1164 mCheckedItemCount--; 1165 } 1166 } 1167 if (mChoiceActionMode != null) { 1168 final long id = mAdapter.getItemId(position); 1169 mMultiChoiceModeCallback.onItemCheckedStateChanged(mChoiceActionMode, 1170 position, id, value); 1171 } 1172 } else { 1173 boolean updateIds = mCheckedIdStates != null && mAdapter.hasStableIds(); 1174 // Clear all values if we're checking something, or unchecking the currently 1175 // selected item 1176 itemCheckChanged = isItemChecked(position) != value; 1177 if (value || isItemChecked(position)) { 1178 mCheckStates.clear(); 1179 if (updateIds) { 1180 mCheckedIdStates.clear(); 1181 } 1182 } 1183 // this may end up selecting the value we just cleared but this way 1184 // we ensure length of mCheckStates is 1, a fact getCheckedItemPosition relies on 1185 if (value) { 1186 mCheckStates.put(position, true); 1187 if (updateIds) { 1188 mCheckedIdStates.put(mAdapter.getItemId(position), position); 1189 } 1190 mCheckedItemCount = 1; 1191 } else if (mCheckStates.size() == 0 || !mCheckStates.valueAt(0)) { 1192 mCheckedItemCount = 0; 1193 } 1194 } 1195 1196 // Do not generate a data change while we are in the layout phase or data has not changed 1197 if (!mInLayout && !mBlockLayoutRequests && itemCheckChanged) { 1198 mDataChanged = true; 1199 rememberSyncState(); 1200 requestLayout(); 1201 } 1202 } 1203 1204 @Override performItemClick(View view, int position, long id)1205 public boolean performItemClick(View view, int position, long id) { 1206 boolean handled = false; 1207 boolean dispatchItemClick = true; 1208 1209 if (mChoiceMode != CHOICE_MODE_NONE) { 1210 handled = true; 1211 boolean checkedStateChanged = false; 1212 1213 if (mChoiceMode == CHOICE_MODE_MULTIPLE || 1214 (mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL && mChoiceActionMode != null)) { 1215 boolean checked = !mCheckStates.get(position, false); 1216 mCheckStates.put(position, checked); 1217 if (mCheckedIdStates != null && mAdapter.hasStableIds()) { 1218 if (checked) { 1219 mCheckedIdStates.put(mAdapter.getItemId(position), position); 1220 } else { 1221 mCheckedIdStates.delete(mAdapter.getItemId(position)); 1222 } 1223 } 1224 if (checked) { 1225 mCheckedItemCount++; 1226 } else { 1227 mCheckedItemCount--; 1228 } 1229 if (mChoiceActionMode != null) { 1230 mMultiChoiceModeCallback.onItemCheckedStateChanged(mChoiceActionMode, 1231 position, id, checked); 1232 dispatchItemClick = false; 1233 } 1234 checkedStateChanged = true; 1235 } else if (mChoiceMode == CHOICE_MODE_SINGLE) { 1236 boolean checked = !mCheckStates.get(position, false); 1237 if (checked) { 1238 mCheckStates.clear(); 1239 mCheckStates.put(position, true); 1240 if (mCheckedIdStates != null && mAdapter.hasStableIds()) { 1241 mCheckedIdStates.clear(); 1242 mCheckedIdStates.put(mAdapter.getItemId(position), position); 1243 } 1244 mCheckedItemCount = 1; 1245 } else if (mCheckStates.size() == 0 || !mCheckStates.valueAt(0)) { 1246 mCheckedItemCount = 0; 1247 } 1248 checkedStateChanged = true; 1249 } 1250 1251 if (checkedStateChanged) { 1252 updateOnScreenCheckedViews(); 1253 } 1254 } 1255 1256 if (dispatchItemClick) { 1257 handled |= super.performItemClick(view, position, id); 1258 } 1259 1260 return handled; 1261 } 1262 1263 /** 1264 * Perform a quick, in-place update of the checked or activated state 1265 * on all visible item views. This should only be called when a valid 1266 * choice mode is active. 1267 */ updateOnScreenCheckedViews()1268 private void updateOnScreenCheckedViews() { 1269 final int firstPos = mFirstPosition; 1270 final int count = getChildCount(); 1271 final boolean useActivated = getContext().getApplicationInfo().targetSdkVersion 1272 >= android.os.Build.VERSION_CODES.HONEYCOMB; 1273 for (int i = 0; i < count; i++) { 1274 final View child = getChildAt(i); 1275 final int position = firstPos + i; 1276 1277 if (child instanceof Checkable) { 1278 ((Checkable) child).setChecked(mCheckStates.get(position)); 1279 } else if (useActivated) { 1280 child.setActivated(mCheckStates.get(position)); 1281 } 1282 } 1283 } 1284 1285 /** 1286 * @see #setChoiceMode(int) 1287 * 1288 * @return The current choice mode 1289 */ 1290 @InspectableProperty(enumMapping = { 1291 @EnumEntry(value = CHOICE_MODE_NONE, name = "none"), 1292 @EnumEntry(value = CHOICE_MODE_SINGLE, name = "singleChoice"), 1293 @InspectableProperty.EnumEntry(value = CHOICE_MODE_MULTIPLE, name = "multipleChoice"), 1294 @EnumEntry(value = CHOICE_MODE_MULTIPLE_MODAL, name = "multipleChoiceModal") 1295 }) getChoiceMode()1296 public int getChoiceMode() { 1297 return mChoiceMode; 1298 } 1299 1300 /** 1301 * Defines the choice behavior for the List. By default, Lists do not have any choice behavior 1302 * ({@link #CHOICE_MODE_NONE}). By setting the choiceMode to {@link #CHOICE_MODE_SINGLE}, the 1303 * List allows up to one item to be in a chosen state. By setting the choiceMode to 1304 * {@link #CHOICE_MODE_MULTIPLE}, the list allows any number of items to be chosen. 1305 * 1306 * @param choiceMode One of {@link #CHOICE_MODE_NONE}, {@link #CHOICE_MODE_SINGLE}, or 1307 * {@link #CHOICE_MODE_MULTIPLE} 1308 */ setChoiceMode(int choiceMode)1309 public void setChoiceMode(int choiceMode) { 1310 mChoiceMode = choiceMode; 1311 if (mChoiceActionMode != null) { 1312 mChoiceActionMode.finish(); 1313 mChoiceActionMode = null; 1314 } 1315 if (mChoiceMode != CHOICE_MODE_NONE) { 1316 if (mCheckStates == null) { 1317 mCheckStates = new SparseBooleanArray(0); 1318 } 1319 if (mCheckedIdStates == null && mAdapter != null && mAdapter.hasStableIds()) { 1320 mCheckedIdStates = new LongSparseArray<Integer>(0); 1321 } 1322 // Modal multi-choice mode only has choices when the mode is active. Clear them. 1323 if (mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL) { 1324 clearChoices(); 1325 setLongClickable(true); 1326 } 1327 } 1328 } 1329 1330 /** 1331 * Set a {@link MultiChoiceModeListener} that will manage the lifecycle of the 1332 * selection {@link ActionMode}. Only used when the choice mode is set to 1333 * {@link #CHOICE_MODE_MULTIPLE_MODAL}. 1334 * 1335 * @param listener Listener that will manage the selection mode 1336 * 1337 * @see #setChoiceMode(int) 1338 */ setMultiChoiceModeListener(MultiChoiceModeListener listener)1339 public void setMultiChoiceModeListener(MultiChoiceModeListener listener) { 1340 if (mMultiChoiceModeCallback == null) { 1341 mMultiChoiceModeCallback = new MultiChoiceModeWrapper(); 1342 } 1343 mMultiChoiceModeCallback.setWrapped(listener); 1344 } 1345 1346 /** 1347 * @return true if all list content currently fits within the view boundaries 1348 */ contentFits()1349 private boolean contentFits() { 1350 final int childCount = getChildCount(); 1351 if (childCount == 0) return true; 1352 if (childCount != mItemCount) return false; 1353 1354 return getChildAt(0).getTop() >= mListPadding.top && 1355 getChildAt(childCount - 1).getBottom() <= getHeight() - mListPadding.bottom; 1356 } 1357 1358 /** 1359 * Specifies whether fast scrolling is enabled or disabled. 1360 * <p> 1361 * When fast scrolling is enabled, the user can quickly scroll through lists 1362 * by dragging the fast scroll thumb. 1363 * <p> 1364 * If the adapter backing this list implements {@link SectionIndexer}, the 1365 * fast scroller will display section header previews as the user scrolls. 1366 * Additionally, the user will be able to quickly jump between sections by 1367 * tapping along the length of the scroll bar. 1368 * 1369 * @see SectionIndexer 1370 * @see #isFastScrollEnabled() 1371 * @param enabled true to enable fast scrolling, false otherwise 1372 */ setFastScrollEnabled(final boolean enabled)1373 public void setFastScrollEnabled(final boolean enabled) { 1374 if (mFastScrollEnabled != enabled) { 1375 mFastScrollEnabled = enabled; 1376 1377 if (isOwnerThread()) { 1378 setFastScrollerEnabledUiThread(enabled); 1379 } else { 1380 post(new Runnable() { 1381 @Override 1382 public void run() { 1383 setFastScrollerEnabledUiThread(enabled); 1384 } 1385 }); 1386 } 1387 } 1388 } 1389 setFastScrollerEnabledUiThread(boolean enabled)1390 private void setFastScrollerEnabledUiThread(boolean enabled) { 1391 if (mFastScroll != null) { 1392 mFastScroll.setEnabled(enabled); 1393 } else if (enabled) { 1394 mFastScroll = new FastScroller(this, mFastScrollStyle); 1395 mFastScroll.setEnabled(true); 1396 } 1397 1398 resolvePadding(); 1399 1400 if (mFastScroll != null) { 1401 mFastScroll.updateLayout(); 1402 } 1403 } 1404 1405 /** 1406 * Specifies the style of the fast scroller decorations. 1407 * 1408 * @param styleResId style resource containing fast scroller properties 1409 * @see android.R.styleable#FastScroll 1410 */ setFastScrollStyle(int styleResId)1411 public void setFastScrollStyle(int styleResId) { 1412 if (mFastScroll == null) { 1413 mFastScrollStyle = styleResId; 1414 } else { 1415 mFastScroll.setStyle(styleResId); 1416 } 1417 } 1418 1419 /** 1420 * Set whether or not the fast scroller should always be shown in place of 1421 * the standard scroll bars. This will enable fast scrolling if it is not 1422 * already enabled. 1423 * <p> 1424 * Fast scrollers shown in this way will not fade out and will be a 1425 * permanent fixture within the list. This is best combined with an inset 1426 * scroll bar style to ensure the scroll bar does not overlap content. 1427 * 1428 * @param alwaysShow true if the fast scroller should always be displayed, 1429 * false otherwise 1430 * @see #setScrollBarStyle(int) 1431 * @see #setFastScrollEnabled(boolean) 1432 */ setFastScrollAlwaysVisible(final boolean alwaysShow)1433 public void setFastScrollAlwaysVisible(final boolean alwaysShow) { 1434 if (mFastScrollAlwaysVisible != alwaysShow) { 1435 if (alwaysShow && !mFastScrollEnabled) { 1436 setFastScrollEnabled(true); 1437 } 1438 1439 mFastScrollAlwaysVisible = alwaysShow; 1440 1441 if (isOwnerThread()) { 1442 setFastScrollerAlwaysVisibleUiThread(alwaysShow); 1443 } else { 1444 post(new Runnable() { 1445 @Override 1446 public void run() { 1447 setFastScrollerAlwaysVisibleUiThread(alwaysShow); 1448 } 1449 }); 1450 } 1451 } 1452 } 1453 setFastScrollerAlwaysVisibleUiThread(boolean alwaysShow)1454 private void setFastScrollerAlwaysVisibleUiThread(boolean alwaysShow) { 1455 if (mFastScroll != null) { 1456 mFastScroll.setAlwaysShow(alwaysShow); 1457 } 1458 } 1459 1460 /** 1461 * @return whether the current thread is the one that created the view 1462 */ isOwnerThread()1463 private boolean isOwnerThread() { 1464 return mOwnerThread == Thread.currentThread(); 1465 } 1466 1467 /** 1468 * Returns true if the fast scroller is set to always show on this view. 1469 * 1470 * @return true if the fast scroller will always show 1471 * @see #setFastScrollAlwaysVisible(boolean) 1472 */ isFastScrollAlwaysVisible()1473 public boolean isFastScrollAlwaysVisible() { 1474 if (mFastScroll == null) { 1475 return mFastScrollEnabled && mFastScrollAlwaysVisible; 1476 } else { 1477 return mFastScroll.isEnabled() && mFastScroll.isAlwaysShowEnabled(); 1478 } 1479 } 1480 1481 @Override getVerticalScrollbarWidth()1482 public int getVerticalScrollbarWidth() { 1483 if (mFastScroll != null && mFastScroll.isEnabled()) { 1484 return Math.max(super.getVerticalScrollbarWidth(), mFastScroll.getWidth()); 1485 } 1486 return super.getVerticalScrollbarWidth(); 1487 } 1488 1489 /** 1490 * Returns true if the fast scroller is enabled. 1491 * 1492 * @see #setFastScrollEnabled(boolean) 1493 * @return true if fast scroll is enabled, false otherwise 1494 */ 1495 @ViewDebug.ExportedProperty 1496 @InspectableProperty isFastScrollEnabled()1497 public boolean isFastScrollEnabled() { 1498 if (mFastScroll == null) { 1499 return mFastScrollEnabled; 1500 } else { 1501 return mFastScroll.isEnabled(); 1502 } 1503 } 1504 1505 @Override setVerticalScrollbarPosition(int position)1506 public void setVerticalScrollbarPosition(int position) { 1507 super.setVerticalScrollbarPosition(position); 1508 if (mFastScroll != null) { 1509 mFastScroll.setScrollbarPosition(position); 1510 } 1511 } 1512 1513 @Override setScrollBarStyle(int style)1514 public void setScrollBarStyle(int style) { 1515 super.setScrollBarStyle(style); 1516 if (mFastScroll != null) { 1517 mFastScroll.setScrollBarStyle(style); 1518 } 1519 } 1520 1521 /** 1522 * If fast scroll is enabled, then don't draw the vertical scrollbar. 1523 * @hide 1524 */ 1525 @Override 1526 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) isVerticalScrollBarHidden()1527 protected boolean isVerticalScrollBarHidden() { 1528 return isFastScrollEnabled(); 1529 } 1530 1531 /** 1532 * When smooth scrollbar is enabled, the position and size of the scrollbar thumb 1533 * is computed based on the number of visible pixels in the visible items. This 1534 * however assumes that all list items have the same height. If you use a list in 1535 * which items have different heights, the scrollbar will change appearance as the 1536 * user scrolls through the list. To avoid this issue, you need to disable this 1537 * property. 1538 * 1539 * When smooth scrollbar is disabled, the position and size of the scrollbar thumb 1540 * is based solely on the number of items in the adapter and the position of the 1541 * visible items inside the adapter. This provides a stable scrollbar as the user 1542 * navigates through a list of items with varying heights. 1543 * 1544 * @param enabled Whether or not to enable smooth scrollbar. 1545 * 1546 * @see #setSmoothScrollbarEnabled(boolean) 1547 * @attr ref android.R.styleable#AbsListView_smoothScrollbar 1548 */ setSmoothScrollbarEnabled(boolean enabled)1549 public void setSmoothScrollbarEnabled(boolean enabled) { 1550 mSmoothScrollbarEnabled = enabled; 1551 } 1552 1553 /** 1554 * Returns the current state of the fast scroll feature. 1555 * 1556 * @return True if smooth scrollbar is enabled is enabled, false otherwise. 1557 * 1558 * @see #setSmoothScrollbarEnabled(boolean) 1559 */ 1560 @ViewDebug.ExportedProperty 1561 @InspectableProperty(name = "smoothScrollbar") isSmoothScrollbarEnabled()1562 public boolean isSmoothScrollbarEnabled() { 1563 return mSmoothScrollbarEnabled; 1564 } 1565 1566 /** 1567 * Set the listener that will receive notifications every time the list scrolls. 1568 * 1569 * @param l the scroll listener 1570 */ setOnScrollListener(OnScrollListener l)1571 public void setOnScrollListener(OnScrollListener l) { 1572 mOnScrollListener = l; 1573 invokeOnItemScrollListener(); 1574 } 1575 1576 /** 1577 * Notify our scroll listener (if there is one) of a change in scroll state 1578 */ 1579 @UnsupportedAppUsage invokeOnItemScrollListener()1580 void invokeOnItemScrollListener() { 1581 if (mFastScroll != null) { 1582 mFastScroll.onScroll(mFirstPosition, getChildCount(), mItemCount); 1583 } 1584 if (mOnScrollListener != null) { 1585 mOnScrollListener.onScroll(this, mFirstPosition, getChildCount(), mItemCount); 1586 } 1587 // placeholder values, View's implementation does not use these. 1588 onScrollChanged(0, 0, 0, 0); 1589 } 1590 1591 /** 1592 * A TYPE_VIEW_SCROLLED event should be sent whenever a scroll happens, even if the 1593 * mFirstPosition and the child count have not changed. 1594 */ 1595 1596 @Override getAccessibilityClassName()1597 public CharSequence getAccessibilityClassName() { 1598 return AbsListView.class.getName(); 1599 } 1600 1601 /** @hide */ 1602 @Override onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info)1603 public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) { 1604 super.onInitializeAccessibilityNodeInfoInternal(info); 1605 if (isEnabled()) { 1606 if (canScrollUp()) { 1607 info.addAction(AccessibilityAction.ACTION_SCROLL_BACKWARD); 1608 info.addAction(AccessibilityAction.ACTION_SCROLL_UP); 1609 info.setScrollable(true); 1610 } 1611 if (canScrollDown()) { 1612 info.addAction(AccessibilityAction.ACTION_SCROLL_FORWARD); 1613 info.addAction(AccessibilityAction.ACTION_SCROLL_DOWN); 1614 info.setScrollable(true); 1615 } 1616 } 1617 1618 info.removeAction(AccessibilityAction.ACTION_CLICK); 1619 info.setClickable(false); 1620 } 1621 getSelectionModeForAccessibility()1622 int getSelectionModeForAccessibility() { 1623 final int choiceMode = getChoiceMode(); 1624 switch (choiceMode) { 1625 case CHOICE_MODE_NONE: 1626 return CollectionInfo.SELECTION_MODE_NONE; 1627 case CHOICE_MODE_SINGLE: 1628 return CollectionInfo.SELECTION_MODE_SINGLE; 1629 case CHOICE_MODE_MULTIPLE: 1630 case CHOICE_MODE_MULTIPLE_MODAL: 1631 return CollectionInfo.SELECTION_MODE_MULTIPLE; 1632 default: 1633 return CollectionInfo.SELECTION_MODE_NONE; 1634 } 1635 } 1636 1637 /** @hide */ 1638 @Override performAccessibilityActionInternal(int action, Bundle arguments)1639 public boolean performAccessibilityActionInternal(int action, Bundle arguments) { 1640 if (super.performAccessibilityActionInternal(action, arguments)) { 1641 return true; 1642 } 1643 switch (action) { 1644 case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: 1645 case R.id.accessibilityActionScrollDown: { 1646 if (isEnabled() && canScrollDown()) { 1647 final int viewportHeight = getHeight() - mListPadding.top - mListPadding.bottom; 1648 smoothScrollBy(viewportHeight, PositionScroller.SCROLL_DURATION); 1649 return true; 1650 } 1651 } return false; 1652 case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: 1653 case R.id.accessibilityActionScrollUp: { 1654 if (isEnabled() && canScrollUp()) { 1655 final int viewportHeight = getHeight() - mListPadding.top - mListPadding.bottom; 1656 smoothScrollBy(-viewportHeight, PositionScroller.SCROLL_DURATION); 1657 return true; 1658 } 1659 } return false; 1660 } 1661 return false; 1662 } 1663 1664 /** 1665 * Indicates whether the children's drawing cache is used during a scroll. 1666 * By default, the drawing cache is enabled but this will consume more memory. 1667 * 1668 * @return true if the scrolling cache is enabled, false otherwise 1669 * 1670 * @see #setScrollingCacheEnabled(boolean) 1671 * @see View#setDrawingCacheEnabled(boolean) 1672 */ 1673 @ViewDebug.ExportedProperty 1674 @InspectableProperty(name = "scrollingCache") isScrollingCacheEnabled()1675 public boolean isScrollingCacheEnabled() { 1676 return mScrollingCacheEnabled; 1677 } 1678 1679 /** 1680 * Enables or disables the children's drawing cache during a scroll. 1681 * By default, the drawing cache is enabled but this will use more memory. 1682 * 1683 * When the scrolling cache is enabled, the caches are kept after the 1684 * first scrolling. You can manually clear the cache by calling 1685 * {@link android.view.ViewGroup#setChildrenDrawingCacheEnabled(boolean)}. 1686 * 1687 * @param enabled true to enable the scroll cache, false otherwise 1688 * 1689 * @see #isScrollingCacheEnabled() 1690 * @see View#setDrawingCacheEnabled(boolean) 1691 */ setScrollingCacheEnabled(boolean enabled)1692 public void setScrollingCacheEnabled(boolean enabled) { 1693 if (mScrollingCacheEnabled && !enabled) { 1694 clearScrollingCache(); 1695 } 1696 mScrollingCacheEnabled = enabled; 1697 } 1698 1699 /** 1700 * Enables or disables the type filter window. If enabled, typing when 1701 * this view has focus will filter the children to match the users input. 1702 * Note that the {@link Adapter} used by this view must implement the 1703 * {@link Filterable} interface. 1704 * 1705 * @param textFilterEnabled true to enable type filtering, false otherwise 1706 * 1707 * @see Filterable 1708 */ setTextFilterEnabled(boolean textFilterEnabled)1709 public void setTextFilterEnabled(boolean textFilterEnabled) { 1710 mTextFilterEnabled = textFilterEnabled; 1711 } 1712 1713 /** 1714 * Indicates whether type filtering is enabled for this view 1715 * 1716 * @return true if type filtering is enabled, false otherwise 1717 * 1718 * @see #setTextFilterEnabled(boolean) 1719 * @see Filterable 1720 */ 1721 @ViewDebug.ExportedProperty 1722 @InspectableProperty isTextFilterEnabled()1723 public boolean isTextFilterEnabled() { 1724 return mTextFilterEnabled; 1725 } 1726 1727 @Override getFocusedRect(Rect r)1728 public void getFocusedRect(Rect r) { 1729 View view = getSelectedView(); 1730 if (view != null && view.getParent() == this) { 1731 // the focused rectangle of the selected view offset into the 1732 // coordinate space of this view. 1733 view.getFocusedRect(r); 1734 offsetDescendantRectToMyCoords(view, r); 1735 } else { 1736 // otherwise, just the norm 1737 super.getFocusedRect(r); 1738 } 1739 } 1740 useDefaultSelector()1741 private void useDefaultSelector() { 1742 setSelector(getContext().getDrawable( 1743 com.android.internal.R.drawable.list_selector_background)); 1744 } 1745 1746 /** 1747 * Indicates whether the content of this view is pinned to, or stacked from, 1748 * the bottom edge. 1749 * 1750 * @return true if the content is stacked from the bottom edge, false otherwise 1751 */ 1752 @ViewDebug.ExportedProperty 1753 @InspectableProperty isStackFromBottom()1754 public boolean isStackFromBottom() { 1755 return mStackFromBottom; 1756 } 1757 1758 /** 1759 * When stack from bottom is set to true, the list fills its content starting from 1760 * the bottom of the view. 1761 * 1762 * @param stackFromBottom true to pin the view's content to the bottom edge, 1763 * false to pin the view's content to the top edge 1764 */ setStackFromBottom(boolean stackFromBottom)1765 public void setStackFromBottom(boolean stackFromBottom) { 1766 if (mStackFromBottom != stackFromBottom) { 1767 mStackFromBottom = stackFromBottom; 1768 requestLayoutIfNecessary(); 1769 } 1770 } 1771 requestLayoutIfNecessary()1772 void requestLayoutIfNecessary() { 1773 if (getChildCount() > 0) { 1774 resetList(); 1775 requestLayout(); 1776 invalidate(); 1777 } 1778 } 1779 1780 static class SavedState extends BaseSavedState { 1781 long selectedId; 1782 @UnsupportedAppUsage 1783 long firstId; 1784 @UnsupportedAppUsage 1785 int viewTop; 1786 int position; 1787 int height; 1788 String filter; 1789 boolean inActionMode; 1790 int checkedItemCount; 1791 SparseBooleanArray checkState; 1792 LongSparseArray<Integer> checkIdState; 1793 1794 /** 1795 * Constructor called from {@link AbsListView#onSaveInstanceState()} 1796 */ SavedState(Parcelable superState)1797 SavedState(Parcelable superState) { 1798 super(superState); 1799 } 1800 1801 /** 1802 * Constructor called from {@link #CREATOR} 1803 */ SavedState(Parcel in)1804 private SavedState(Parcel in) { 1805 super(in); 1806 selectedId = in.readLong(); 1807 firstId = in.readLong(); 1808 viewTop = in.readInt(); 1809 position = in.readInt(); 1810 height = in.readInt(); 1811 filter = in.readString(); 1812 inActionMode = in.readByte() != 0; 1813 checkedItemCount = in.readInt(); 1814 checkState = in.readSparseBooleanArray(); 1815 final int N = in.readInt(); 1816 if (N > 0) { 1817 checkIdState = new LongSparseArray<Integer>(); 1818 for (int i=0; i<N; i++) { 1819 final long key = in.readLong(); 1820 final int value = in.readInt(); 1821 checkIdState.put(key, value); 1822 } 1823 } 1824 } 1825 1826 @Override writeToParcel(Parcel out, int flags)1827 public void writeToParcel(Parcel out, int flags) { 1828 super.writeToParcel(out, flags); 1829 out.writeLong(selectedId); 1830 out.writeLong(firstId); 1831 out.writeInt(viewTop); 1832 out.writeInt(position); 1833 out.writeInt(height); 1834 out.writeString(filter); 1835 out.writeByte((byte) (inActionMode ? 1 : 0)); 1836 out.writeInt(checkedItemCount); 1837 out.writeSparseBooleanArray(checkState); 1838 final int N = checkIdState != null ? checkIdState.size() : 0; 1839 out.writeInt(N); 1840 for (int i=0; i<N; i++) { 1841 out.writeLong(checkIdState.keyAt(i)); 1842 out.writeInt(checkIdState.valueAt(i)); 1843 } 1844 } 1845 1846 @Override toString()1847 public String toString() { 1848 return "AbsListView.SavedState{" 1849 + Integer.toHexString(System.identityHashCode(this)) 1850 + " selectedId=" + selectedId 1851 + " firstId=" + firstId 1852 + " viewTop=" + viewTop 1853 + " position=" + position 1854 + " height=" + height 1855 + " filter=" + filter 1856 + " checkState=" + checkState + "}"; 1857 } 1858 1859 public static final @android.annotation.NonNull Parcelable.Creator<SavedState> CREATOR 1860 = new Parcelable.Creator<SavedState>() { 1861 @Override 1862 public SavedState createFromParcel(Parcel in) { 1863 return new SavedState(in); 1864 } 1865 1866 @Override 1867 public SavedState[] newArray(int size) { 1868 return new SavedState[size]; 1869 } 1870 }; 1871 } 1872 1873 @Override onSaveInstanceState()1874 public Parcelable onSaveInstanceState() { 1875 /* 1876 * This doesn't really make sense as the place to dismiss the 1877 * popups, but there don't seem to be any other useful hooks 1878 * that happen early enough to keep from getting complaints 1879 * about having leaked the window. 1880 */ 1881 dismissPopup(); 1882 1883 Parcelable superState = super.onSaveInstanceState(); 1884 1885 SavedState ss = new SavedState(superState); 1886 1887 if (mPendingSync != null) { 1888 // Just keep what we last restored. 1889 ss.selectedId = mPendingSync.selectedId; 1890 ss.firstId = mPendingSync.firstId; 1891 ss.viewTop = mPendingSync.viewTop; 1892 ss.position = mPendingSync.position; 1893 ss.height = mPendingSync.height; 1894 ss.filter = mPendingSync.filter; 1895 ss.inActionMode = mPendingSync.inActionMode; 1896 ss.checkedItemCount = mPendingSync.checkedItemCount; 1897 ss.checkState = mPendingSync.checkState; 1898 ss.checkIdState = mPendingSync.checkIdState; 1899 return ss; 1900 } 1901 1902 boolean haveChildren = getChildCount() > 0 && mItemCount > 0; 1903 long selectedId = getSelectedItemId(); 1904 ss.selectedId = selectedId; 1905 ss.height = getHeight(); 1906 1907 if (selectedId >= 0) { 1908 // Remember the selection 1909 ss.viewTop = mSelectedTop; 1910 ss.position = getSelectedItemPosition(); 1911 ss.firstId = INVALID_POSITION; 1912 } else { 1913 if (haveChildren && mFirstPosition > 0) { 1914 // Remember the position of the first child. 1915 // We only do this if we are not currently at the top of 1916 // the list, for two reasons: 1917 // (1) The list may be in the process of becoming empty, in 1918 // which case mItemCount may not be 0, but if we try to 1919 // ask for any information about position 0 we will crash. 1920 // (2) Being "at the top" seems like a special case, anyway, 1921 // and the user wouldn't expect to end up somewhere else when 1922 // they revisit the list even if its content has changed. 1923 View v = getChildAt(0); 1924 ss.viewTop = v.getTop(); 1925 int firstPos = mFirstPosition; 1926 if (firstPos >= mItemCount) { 1927 firstPos = mItemCount - 1; 1928 } 1929 ss.position = firstPos; 1930 ss.firstId = mAdapter.getItemId(firstPos); 1931 } else { 1932 ss.viewTop = 0; 1933 ss.firstId = INVALID_POSITION; 1934 ss.position = 0; 1935 } 1936 } 1937 1938 ss.filter = null; 1939 if (mFiltered) { 1940 final EditText textFilter = mTextFilter; 1941 if (textFilter != null) { 1942 Editable filterText = textFilter.getText(); 1943 if (filterText != null) { 1944 ss.filter = filterText.toString(); 1945 } 1946 } 1947 } 1948 1949 ss.inActionMode = mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL && mChoiceActionMode != null; 1950 1951 if (mCheckStates != null) { 1952 ss.checkState = mCheckStates.clone(); 1953 } 1954 if (mCheckedIdStates != null) { 1955 final LongSparseArray<Integer> idState = new LongSparseArray<Integer>(); 1956 final int count = mCheckedIdStates.size(); 1957 for (int i = 0; i < count; i++) { 1958 idState.put(mCheckedIdStates.keyAt(i), mCheckedIdStates.valueAt(i)); 1959 } 1960 ss.checkIdState = idState; 1961 } 1962 ss.checkedItemCount = mCheckedItemCount; 1963 1964 if (mRemoteAdapter != null) { 1965 mRemoteAdapter.saveRemoteViewsCache(); 1966 } 1967 1968 return ss; 1969 } 1970 1971 @Override onRestoreInstanceState(Parcelable state)1972 public void onRestoreInstanceState(Parcelable state) { 1973 SavedState ss = (SavedState) state; 1974 1975 super.onRestoreInstanceState(ss.getSuperState()); 1976 mDataChanged = true; 1977 1978 mSyncHeight = ss.height; 1979 1980 if (ss.selectedId >= 0) { 1981 mNeedSync = true; 1982 mPendingSync = ss; 1983 mSyncRowId = ss.selectedId; 1984 mSyncPosition = ss.position; 1985 mSpecificTop = ss.viewTop; 1986 mSyncMode = SYNC_SELECTED_POSITION; 1987 } else if (ss.firstId >= 0) { 1988 setSelectedPositionInt(INVALID_POSITION); 1989 // Do this before setting mNeedSync since setNextSelectedPosition looks at mNeedSync 1990 setNextSelectedPositionInt(INVALID_POSITION); 1991 mSelectorPosition = INVALID_POSITION; 1992 mNeedSync = true; 1993 mPendingSync = ss; 1994 mSyncRowId = ss.firstId; 1995 mSyncPosition = ss.position; 1996 mSpecificTop = ss.viewTop; 1997 mSyncMode = SYNC_FIRST_POSITION; 1998 } 1999 2000 setFilterText(ss.filter); 2001 2002 if (ss.checkState != null) { 2003 mCheckStates = ss.checkState; 2004 } 2005 2006 if (ss.checkIdState != null) { 2007 mCheckedIdStates = ss.checkIdState; 2008 } 2009 2010 mCheckedItemCount = ss.checkedItemCount; 2011 2012 if (ss.inActionMode && mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL && 2013 mMultiChoiceModeCallback != null) { 2014 mChoiceActionMode = startActionMode(mMultiChoiceModeCallback); 2015 } 2016 2017 requestLayout(); 2018 } 2019 acceptFilter()2020 private boolean acceptFilter() { 2021 return mTextFilterEnabled && getAdapter() instanceof Filterable && 2022 ((Filterable) getAdapter()).getFilter() != null; 2023 } 2024 2025 /** 2026 * Sets the initial value for the text filter. 2027 * @param filterText The text to use for the filter. 2028 * 2029 * @see #setTextFilterEnabled 2030 */ setFilterText(String filterText)2031 public void setFilterText(String filterText) { 2032 // TODO: Should we check for acceptFilter()? 2033 if (mTextFilterEnabled && !TextUtils.isEmpty(filterText)) { 2034 createTextFilter(false); 2035 // This is going to call our listener onTextChanged, but we might not 2036 // be ready to bring up a window yet 2037 mTextFilter.setText(filterText); 2038 mTextFilter.setSelection(filterText.length()); 2039 if (mAdapter instanceof Filterable) { 2040 // if mPopup is non-null, then onTextChanged will do the filtering 2041 if (mPopup == null) { 2042 Filter f = ((Filterable) mAdapter).getFilter(); 2043 f.filter(filterText); 2044 } 2045 // Set filtered to true so we will display the filter window when our main 2046 // window is ready 2047 mFiltered = true; 2048 mDataSetObserver.clearSavedState(); 2049 } 2050 } 2051 } 2052 2053 /** 2054 * Returns the list's text filter, if available. 2055 * @return the list's text filter or null if filtering isn't enabled 2056 */ getTextFilter()2057 public CharSequence getTextFilter() { 2058 if (mTextFilterEnabled && mTextFilter != null) { 2059 return mTextFilter.getText(); 2060 } 2061 return null; 2062 } 2063 2064 @Override onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect)2065 protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { 2066 super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); 2067 if (gainFocus && mSelectedPosition < 0 && !isInTouchMode()) { 2068 if (!isAttachedToWindow() && mAdapter != null) { 2069 // Data may have changed while we were detached and it's valid 2070 // to change focus while detached. Refresh so we don't die. 2071 mDataChanged = true; 2072 mOldItemCount = mItemCount; 2073 mItemCount = mAdapter.getCount(); 2074 } 2075 resurrectSelection(); 2076 } 2077 } 2078 2079 @Override requestLayout()2080 public void requestLayout() { 2081 if (!mBlockLayoutRequests && !mInLayout) { 2082 super.requestLayout(); 2083 } 2084 } 2085 2086 /** 2087 * The list is empty. Clear everything out. 2088 */ resetList()2089 void resetList() { 2090 removeAllViewsInLayout(); 2091 mFirstPosition = 0; 2092 mDataChanged = false; 2093 mPositionScrollAfterLayout = null; 2094 mNeedSync = false; 2095 mPendingSync = null; 2096 mOldSelectedPosition = INVALID_POSITION; 2097 mOldSelectedRowId = INVALID_ROW_ID; 2098 setSelectedPositionInt(INVALID_POSITION); 2099 setNextSelectedPositionInt(INVALID_POSITION); 2100 mSelectedTop = 0; 2101 mSelectorPosition = INVALID_POSITION; 2102 mSelectorRect.setEmpty(); 2103 invalidate(); 2104 } 2105 2106 @Override computeVerticalScrollExtent()2107 protected int computeVerticalScrollExtent() { 2108 final int count = getChildCount(); 2109 if (count > 0) { 2110 if (mSmoothScrollbarEnabled) { 2111 int extent = count * 100; 2112 2113 View view = getChildAt(0); 2114 final int top = view.getTop(); 2115 int height = view.getHeight(); 2116 if (height > 0) { 2117 extent += (top * 100) / height; 2118 } 2119 2120 view = getChildAt(count - 1); 2121 final int bottom = view.getBottom(); 2122 height = view.getHeight(); 2123 if (height > 0) { 2124 extent -= ((bottom - getHeight()) * 100) / height; 2125 } 2126 2127 return extent; 2128 } else { 2129 return 1; 2130 } 2131 } 2132 return 0; 2133 } 2134 2135 @Override computeVerticalScrollOffset()2136 protected int computeVerticalScrollOffset() { 2137 final int firstPosition = mFirstPosition; 2138 final int childCount = getChildCount(); 2139 if (firstPosition >= 0 && childCount > 0) { 2140 if (mSmoothScrollbarEnabled) { 2141 final View view = getChildAt(0); 2142 final int top = view.getTop(); 2143 int height = view.getHeight(); 2144 if (height > 0) { 2145 return Math.max(firstPosition * 100 - (top * 100) / height + 2146 (int)((float)mScrollY / getHeight() * mItemCount * 100), 0); 2147 } 2148 } else { 2149 int index; 2150 final int count = mItemCount; 2151 if (firstPosition == 0) { 2152 index = 0; 2153 } else if (firstPosition + childCount == count) { 2154 index = count; 2155 } else { 2156 index = firstPosition + childCount / 2; 2157 } 2158 return (int) (firstPosition + childCount * (index / (float) count)); 2159 } 2160 } 2161 return 0; 2162 } 2163 2164 @Override computeVerticalScrollRange()2165 protected int computeVerticalScrollRange() { 2166 int result; 2167 if (mSmoothScrollbarEnabled) { 2168 result = Math.max(mItemCount * 100, 0); 2169 if (mScrollY != 0) { 2170 // Compensate for overscroll 2171 result += Math.abs((int) ((float) mScrollY / getHeight() * mItemCount * 100)); 2172 } 2173 } else { 2174 result = mItemCount; 2175 } 2176 return result; 2177 } 2178 2179 @Override getTopFadingEdgeStrength()2180 protected float getTopFadingEdgeStrength() { 2181 final int count = getChildCount(); 2182 final float fadeEdge = super.getTopFadingEdgeStrength(); 2183 if (count == 0) { 2184 return fadeEdge; 2185 } else { 2186 if (mFirstPosition > 0) { 2187 return 1.0f; 2188 } 2189 2190 final int top = getChildAt(0).getTop(); 2191 final float fadeLength = getVerticalFadingEdgeLength(); 2192 return top < mPaddingTop ? -(top - mPaddingTop) / fadeLength : fadeEdge; 2193 } 2194 } 2195 2196 @Override getBottomFadingEdgeStrength()2197 protected float getBottomFadingEdgeStrength() { 2198 final int count = getChildCount(); 2199 final float fadeEdge = super.getBottomFadingEdgeStrength(); 2200 if (count == 0) { 2201 return fadeEdge; 2202 } else { 2203 if (mFirstPosition + count - 1 < mItemCount - 1) { 2204 return 1.0f; 2205 } 2206 2207 final int bottom = getChildAt(count - 1).getBottom(); 2208 final int height = getHeight(); 2209 final float fadeLength = getVerticalFadingEdgeLength(); 2210 return bottom > height - mPaddingBottom ? 2211 (bottom - height + mPaddingBottom) / fadeLength : fadeEdge; 2212 } 2213 } 2214 2215 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)2216 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 2217 if (mSelector == null) { 2218 useDefaultSelector(); 2219 } 2220 final Rect listPadding = mListPadding; 2221 listPadding.left = mSelectionLeftPadding + mPaddingLeft; 2222 listPadding.top = mSelectionTopPadding + mPaddingTop; 2223 listPadding.right = mSelectionRightPadding + mPaddingRight; 2224 listPadding.bottom = mSelectionBottomPadding + mPaddingBottom; 2225 2226 // Check if our previous measured size was at a point where we should scroll later. 2227 if (mTranscriptMode == TRANSCRIPT_MODE_NORMAL) { 2228 final int childCount = getChildCount(); 2229 final int listBottom = getHeight() - getPaddingBottom(); 2230 final View lastChild = getChildAt(childCount - 1); 2231 final int lastBottom = lastChild != null ? lastChild.getBottom() : listBottom; 2232 mForceTranscriptScroll = mFirstPosition + childCount >= mLastHandledItemCount && 2233 lastBottom <= listBottom; 2234 } 2235 } 2236 2237 /** 2238 * Subclasses should NOT override this method but 2239 * {@link #layoutChildren()} instead. 2240 */ 2241 @Override onLayout(boolean changed, int l, int t, int r, int b)2242 protected void onLayout(boolean changed, int l, int t, int r, int b) { 2243 super.onLayout(changed, l, t, r, b); 2244 2245 mInLayout = true; 2246 2247 final int childCount = getChildCount(); 2248 if (changed) { 2249 for (int i = 0; i < childCount; i++) { 2250 getChildAt(i).forceLayout(); 2251 } 2252 mRecycler.markChildrenDirty(); 2253 } 2254 2255 layoutChildren(); 2256 2257 mOverscrollMax = (b - t) / OVERSCROLL_LIMIT_DIVISOR; 2258 2259 // TODO: Move somewhere sane. This doesn't belong in onLayout(). 2260 if (mFastScroll != null) { 2261 mFastScroll.onItemCountChanged(getChildCount(), mItemCount); 2262 } 2263 mInLayout = false; 2264 } 2265 2266 /** 2267 * @hide 2268 */ 2269 @Override setFrame(int left, int top, int right, int bottom)2270 protected boolean setFrame(int left, int top, int right, int bottom) { 2271 final boolean changed = super.setFrame(left, top, right, bottom); 2272 2273 if (changed) { 2274 // Reposition the popup when the frame has changed. This includes 2275 // translating the widget, not just changing its dimension. The 2276 // filter popup needs to follow the widget. 2277 final boolean visible = getWindowVisibility() == View.VISIBLE; 2278 if (mFiltered && visible && mPopup != null && mPopup.isShowing()) { 2279 positionPopup(); 2280 } 2281 } 2282 2283 return changed; 2284 } 2285 2286 /** 2287 * Subclasses must override this method to layout their children. 2288 */ layoutChildren()2289 protected void layoutChildren() { 2290 } 2291 2292 /** 2293 * @param focusedView view that holds accessibility focus 2294 * @return direct child that contains accessibility focus, or null if no 2295 * child contains accessibility focus 2296 */ getAccessibilityFocusedChild(View focusedView)2297 View getAccessibilityFocusedChild(View focusedView) { 2298 ViewParent viewParent = focusedView.getParent(); 2299 while ((viewParent instanceof View) && (viewParent != this)) { 2300 focusedView = (View) viewParent; 2301 viewParent = viewParent.getParent(); 2302 } 2303 2304 if (!(viewParent instanceof View)) { 2305 return null; 2306 } 2307 2308 return focusedView; 2309 } 2310 updateScrollIndicators()2311 void updateScrollIndicators() { 2312 if (mScrollUp != null) { 2313 mScrollUp.setVisibility(canScrollUp() ? View.VISIBLE : View.INVISIBLE); 2314 } 2315 2316 if (mScrollDown != null) { 2317 mScrollDown.setVisibility(canScrollDown() ? View.VISIBLE : View.INVISIBLE); 2318 } 2319 } 2320 2321 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) canScrollUp()2322 private boolean canScrollUp() { 2323 boolean canScrollUp; 2324 // 0th element is not visible 2325 canScrollUp = mFirstPosition > 0; 2326 2327 // ... Or top of 0th element is not visible 2328 if (!canScrollUp) { 2329 if (getChildCount() > 0) { 2330 View child = getChildAt(0); 2331 canScrollUp = child.getTop() < mListPadding.top; 2332 } 2333 } 2334 2335 return canScrollUp; 2336 } 2337 2338 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 2339 private boolean canScrollDown() { 2340 boolean canScrollDown; 2341 int count = getChildCount(); 2342 2343 // Last item is not visible 2344 canScrollDown = (mFirstPosition + count) < mItemCount; 2345 2346 // ... Or bottom of the last element is not visible 2347 if (!canScrollDown && count > 0) { 2348 View child = getChildAt(count - 1); 2349 canScrollDown = child.getBottom() > mBottom - mListPadding.bottom; 2350 } 2351 2352 return canScrollDown; 2353 } 2354 2355 @Override 2356 @ViewDebug.ExportedProperty getSelectedView()2357 public View getSelectedView() { 2358 if (mItemCount > 0 && mSelectedPosition >= 0) { 2359 return getChildAt(mSelectedPosition - mFirstPosition); 2360 } else { 2361 return null; 2362 } 2363 } 2364 2365 /** 2366 * List padding is the maximum of the normal view's padding and the padding of the selector. 2367 * 2368 * @see android.view.View#getPaddingTop() 2369 * @see #getSelector() 2370 * 2371 * @return The top list padding. 2372 */ getListPaddingTop()2373 public int getListPaddingTop() { 2374 return mListPadding.top; 2375 } 2376 2377 /** 2378 * List padding is the maximum of the normal view's padding and the padding of the selector. 2379 * 2380 * @see android.view.View#getPaddingBottom() 2381 * @see #getSelector() 2382 * 2383 * @return The bottom list padding. 2384 */ getListPaddingBottom()2385 public int getListPaddingBottom() { 2386 return mListPadding.bottom; 2387 } 2388 2389 /** 2390 * List padding is the maximum of the normal view's padding and the padding of the selector. 2391 * 2392 * @see android.view.View#getPaddingLeft() 2393 * @see #getSelector() 2394 * 2395 * @return The left list padding. 2396 */ getListPaddingLeft()2397 public int getListPaddingLeft() { 2398 return mListPadding.left; 2399 } 2400 2401 /** 2402 * List padding is the maximum of the normal view's padding and the padding of the selector. 2403 * 2404 * @see android.view.View#getPaddingRight() 2405 * @see #getSelector() 2406 * 2407 * @return The right list padding. 2408 */ getListPaddingRight()2409 public int getListPaddingRight() { 2410 return mListPadding.right; 2411 } 2412 2413 /** 2414 * Gets a view and have it show the data associated with the specified 2415 * position. This is called when we have already discovered that the view 2416 * is not available for reuse in the recycle bin. The only choices left are 2417 * converting an old view or making a new one. 2418 * 2419 * @param position the position to display 2420 * @param outMetadata an array of at least 1 boolean where the first entry 2421 * will be set {@code true} if the view is currently 2422 * attached to the window, {@code false} otherwise (e.g. 2423 * newly-inflated or remained scrap for multiple layout 2424 * passes) 2425 * 2426 * @return A view displaying the data associated with the specified position 2427 */ obtainView(int position, boolean[] outMetadata)2428 View obtainView(int position, boolean[] outMetadata) { 2429 Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView"); 2430 2431 outMetadata[0] = false; 2432 2433 // Check whether we have a transient state view. Attempt to re-bind the 2434 // data and discard the view if we fail. 2435 final View transientView = mRecycler.getTransientStateView(position); 2436 if (transientView != null) { 2437 final LayoutParams params = (LayoutParams) transientView.getLayoutParams(); 2438 2439 // If the view type hasn't changed, attempt to re-bind the data. 2440 if (params.viewType == mAdapter.getItemViewType(position)) { 2441 final View updatedView = mAdapter.getView(position, transientView, this); 2442 2443 // If we failed to re-bind the data, scrap the obtained view. 2444 if (updatedView != transientView) { 2445 setItemViewLayoutParams(updatedView, position); 2446 mRecycler.addScrapView(updatedView, position); 2447 } 2448 } 2449 2450 outMetadata[0] = true; 2451 2452 // Finish the temporary detach started in addScrapView(). 2453 transientView.dispatchFinishTemporaryDetach(); 2454 return transientView; 2455 } 2456 2457 final View scrapView = mRecycler.getScrapView(position); 2458 final View child = mAdapter.getView(position, scrapView, this); 2459 if (scrapView != null) { 2460 if (child != scrapView) { 2461 // Failed to re-bind the data, return scrap to the heap. 2462 mRecycler.addScrapView(scrapView, position); 2463 } else if (child.isTemporarilyDetached()) { 2464 outMetadata[0] = true; 2465 2466 // Finish the temporary detach started in addScrapView(). 2467 child.dispatchFinishTemporaryDetach(); 2468 } 2469 } 2470 2471 if (mCacheColorHint != 0) { 2472 child.setDrawingCacheBackgroundColor(mCacheColorHint); 2473 } 2474 2475 if (child.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { 2476 child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); 2477 } 2478 2479 setItemViewLayoutParams(child, position); 2480 2481 if (AccessibilityManager.getInstance(mContext).isEnabled()) { 2482 if (mAccessibilityDelegate == null) { 2483 mAccessibilityDelegate = new ListItemAccessibilityDelegate(); 2484 } 2485 if (child.getAccessibilityDelegate() == null) { 2486 child.setAccessibilityDelegate(mAccessibilityDelegate); 2487 } 2488 } 2489 2490 Trace.traceEnd(Trace.TRACE_TAG_VIEW); 2491 2492 return child; 2493 } 2494 setItemViewLayoutParams(View child, int position)2495 private void setItemViewLayoutParams(View child, int position) { 2496 final ViewGroup.LayoutParams vlp = child.getLayoutParams(); 2497 LayoutParams lp; 2498 if (vlp == null) { 2499 lp = (LayoutParams) generateDefaultLayoutParams(); 2500 } else if (!checkLayoutParams(vlp)) { 2501 lp = (LayoutParams) generateLayoutParams(vlp); 2502 } else { 2503 lp = (LayoutParams) vlp; 2504 } 2505 2506 if (mAdapterHasStableIds) { 2507 lp.itemId = mAdapter.getItemId(position); 2508 } 2509 lp.viewType = mAdapter.getItemViewType(position); 2510 lp.isEnabled = mAdapter.isEnabled(position); 2511 if (lp != vlp) { 2512 child.setLayoutParams(lp); 2513 } 2514 } 2515 2516 class ListItemAccessibilityDelegate extends AccessibilityDelegate { 2517 @Override onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info)2518 public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { 2519 super.onInitializeAccessibilityNodeInfo(host, info); 2520 2521 final int position = getPositionForView(host); 2522 onInitializeAccessibilityNodeInfoForItem(host, position, info); 2523 } 2524 2525 @Override performAccessibilityAction(View host, int action, Bundle arguments)2526 public boolean performAccessibilityAction(View host, int action, Bundle arguments) { 2527 if (super.performAccessibilityAction(host, action, arguments)) { 2528 return true; 2529 } 2530 2531 final int position = getPositionForView(host); 2532 if (position == INVALID_POSITION || mAdapter == null) { 2533 // Cannot perform actions on invalid items. 2534 return false; 2535 } 2536 2537 if (position >= mAdapter.getCount()) { 2538 // The position is no longer valid, likely due to a data set 2539 // change. We could fail here for all data set changes, since 2540 // there is a chance that the data bound to the view may no 2541 // longer exist at the same position within the adapter, but 2542 // it's more consistent with the standard touch interaction to 2543 // click at whatever may have moved into that position. 2544 return false; 2545 } 2546 2547 final boolean isItemEnabled; 2548 final ViewGroup.LayoutParams lp = host.getLayoutParams(); 2549 if (lp instanceof AbsListView.LayoutParams) { 2550 isItemEnabled = ((AbsListView.LayoutParams) lp).isEnabled; 2551 } else { 2552 isItemEnabled = false; 2553 } 2554 2555 if (!isEnabled() || !isItemEnabled) { 2556 // Cannot perform actions on disabled items. 2557 return false; 2558 } 2559 2560 switch (action) { 2561 case AccessibilityNodeInfo.ACTION_CLEAR_SELECTION: { 2562 if (getSelectedItemPosition() == position) { 2563 setSelection(INVALID_POSITION); 2564 return true; 2565 } 2566 } return false; 2567 case AccessibilityNodeInfo.ACTION_SELECT: { 2568 if (getSelectedItemPosition() != position) { 2569 setSelection(position); 2570 return true; 2571 } 2572 } return false; 2573 case AccessibilityNodeInfo.ACTION_CLICK: { 2574 if (isItemClickable(host)) { 2575 final long id = getItemIdAtPosition(position); 2576 return performItemClick(host, position, id); 2577 } 2578 } return false; 2579 case AccessibilityNodeInfo.ACTION_LONG_CLICK: { 2580 if (isLongClickable()) { 2581 final long id = getItemIdAtPosition(position); 2582 return performLongPress(host, position, id); 2583 } 2584 } return false; 2585 } 2586 2587 return false; 2588 } 2589 } 2590 2591 /** 2592 * Initializes an {@link AccessibilityNodeInfo} with information about a 2593 * particular item in the list. 2594 * 2595 * @param view View representing the list item. 2596 * @param position Position of the list item within the adapter. 2597 * @param info Node info to populate. 2598 */ onInitializeAccessibilityNodeInfoForItem( View view, int position, AccessibilityNodeInfo info)2599 public void onInitializeAccessibilityNodeInfoForItem( 2600 View view, int position, AccessibilityNodeInfo info) { 2601 if (position == INVALID_POSITION) { 2602 // The item doesn't exist, so there's not much we can do here. 2603 return; 2604 } 2605 2606 boolean isItemActionable = isEnabled(); 2607 final ViewGroup.LayoutParams lp = view.getLayoutParams(); 2608 if (lp instanceof AbsListView.LayoutParams) { 2609 isItemActionable &= ((AbsListView.LayoutParams) lp).isEnabled; 2610 } 2611 2612 if (position == getSelectedItemPosition()) { 2613 info.setSelected(true); 2614 addAccessibilityActionIfEnabled(info, isItemActionable, 2615 AccessibilityAction.ACTION_CLEAR_SELECTION); 2616 } else { 2617 addAccessibilityActionIfEnabled(info, isItemActionable, 2618 AccessibilityAction.ACTION_SELECT); 2619 } 2620 2621 if (isItemClickable(view)) { 2622 addAccessibilityActionIfEnabled(info, isItemActionable, 2623 AccessibilityAction.ACTION_CLICK); 2624 // A disabled item is a separator which should not be clickable. 2625 info.setClickable(isItemActionable); 2626 } 2627 2628 if (isLongClickable()) { 2629 addAccessibilityActionIfEnabled(info, isItemActionable, 2630 AccessibilityAction.ACTION_LONG_CLICK); 2631 info.setLongClickable(isItemActionable); 2632 } 2633 } 2634 2635 addAccessibilityActionIfEnabled(AccessibilityNodeInfo info, boolean enabled, AccessibilityAction action)2636 private void addAccessibilityActionIfEnabled(AccessibilityNodeInfo info, boolean enabled, 2637 AccessibilityAction action) { 2638 if (enabled) { 2639 info.addAction(action); 2640 } 2641 } 2642 isItemClickable(View view)2643 private boolean isItemClickable(View view) { 2644 return !view.hasExplicitFocusable(); 2645 } 2646 2647 /** 2648 * Positions the selector in a way that mimics touch. 2649 */ positionSelectorLikeTouch(int position, View sel, float x, float y)2650 void positionSelectorLikeTouch(int position, View sel, float x, float y) { 2651 positionSelector(position, sel, true, x, y); 2652 } 2653 2654 /** 2655 * Positions the selector in a way that mimics keyboard focus. 2656 */ positionSelectorLikeFocus(int position, View sel)2657 void positionSelectorLikeFocus(int position, View sel) { 2658 if (mSelector != null && mSelectorPosition != position && position != INVALID_POSITION) { 2659 final Rect bounds = mSelectorRect; 2660 final float x = bounds.exactCenterX(); 2661 final float y = bounds.exactCenterY(); 2662 positionSelector(position, sel, true, x, y); 2663 } else { 2664 positionSelector(position, sel); 2665 } 2666 } 2667 positionSelector(int position, View sel)2668 void positionSelector(int position, View sel) { 2669 positionSelector(position, sel, false, -1, -1); 2670 } 2671 2672 @UnsupportedAppUsage positionSelector(int position, View sel, boolean manageHotspot, float x, float y)2673 private void positionSelector(int position, View sel, boolean manageHotspot, float x, float y) { 2674 final boolean positionChanged = position != mSelectorPosition; 2675 if (position != INVALID_POSITION) { 2676 mSelectorPosition = position; 2677 } 2678 2679 final Rect selectorRect = mSelectorRect; 2680 selectorRect.set(sel.getLeft(), sel.getTop(), sel.getRight(), sel.getBottom()); 2681 if (sel instanceof SelectionBoundsAdjuster) { 2682 ((SelectionBoundsAdjuster)sel).adjustListItemSelectionBounds(selectorRect); 2683 } 2684 2685 // Adjust for selection padding. 2686 selectorRect.left -= mSelectionLeftPadding; 2687 selectorRect.top -= mSelectionTopPadding; 2688 selectorRect.right += mSelectionRightPadding; 2689 selectorRect.bottom += mSelectionBottomPadding; 2690 2691 // Update the child enabled state prior to updating the selector. 2692 final boolean isChildViewEnabled = sel.isEnabled(); 2693 if (mIsChildViewEnabled != isChildViewEnabled) { 2694 mIsChildViewEnabled = isChildViewEnabled; 2695 } 2696 2697 // Update the selector drawable's state and position. 2698 final Drawable selector = mSelector; 2699 if (selector != null) { 2700 if (positionChanged) { 2701 // Wipe out the current selector state so that we can start 2702 // over in the new position with a fresh state. 2703 selector.setVisible(false, false); 2704 selector.setState(StateSet.NOTHING); 2705 } 2706 selector.setBounds(selectorRect); 2707 if (positionChanged) { 2708 if (getVisibility() == VISIBLE) { 2709 selector.setVisible(true, false); 2710 } 2711 updateSelectorState(); 2712 } 2713 if (manageHotspot) { 2714 selector.setHotspot(x, y); 2715 } 2716 } 2717 } 2718 2719 /** 2720 * Returns whether the selected child view (from the adapter's getView) is enabled. 2721 * 2722 * @return true if enabled 2723 */ isSelectedChildViewEnabled()2724 public boolean isSelectedChildViewEnabled() { 2725 return mIsChildViewEnabled; 2726 } 2727 2728 /** 2729 * Set whether the selected child view (from the adapter's getView) is enabled. 2730 * 2731 * When refreshDrawableState is called, AbsListView will control the "enabled" state 2732 * of the selector based on this. 2733 * 2734 * @param selectedChildViewEnabled true if enabled 2735 */ setSelectedChildViewEnabled(boolean selectedChildViewEnabled)2736 public void setSelectedChildViewEnabled(boolean selectedChildViewEnabled) { 2737 mIsChildViewEnabled = selectedChildViewEnabled; 2738 } 2739 2740 @Override dispatchDraw(Canvas canvas)2741 protected void dispatchDraw(Canvas canvas) { 2742 int saveCount = 0; 2743 final boolean clipToPadding = (mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK; 2744 if (clipToPadding) { 2745 saveCount = canvas.save(); 2746 final int scrollX = mScrollX; 2747 final int scrollY = mScrollY; 2748 canvas.clipRect(scrollX + mPaddingLeft, scrollY + mPaddingTop, 2749 scrollX + mRight - mLeft - mPaddingRight, 2750 scrollY + mBottom - mTop - mPaddingBottom); 2751 mGroupFlags &= ~CLIP_TO_PADDING_MASK; 2752 } 2753 2754 final boolean drawSelectorOnTop = mDrawSelectorOnTop; 2755 if (!drawSelectorOnTop) { 2756 drawSelector(canvas); 2757 } 2758 2759 super.dispatchDraw(canvas); 2760 2761 if (drawSelectorOnTop) { 2762 drawSelector(canvas); 2763 } 2764 2765 if (clipToPadding) { 2766 canvas.restoreToCount(saveCount); 2767 mGroupFlags |= CLIP_TO_PADDING_MASK; 2768 } 2769 } 2770 2771 @Override isPaddingOffsetRequired()2772 protected boolean isPaddingOffsetRequired() { 2773 return (mGroupFlags & CLIP_TO_PADDING_MASK) != CLIP_TO_PADDING_MASK; 2774 } 2775 2776 @Override getLeftPaddingOffset()2777 protected int getLeftPaddingOffset() { 2778 return (mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK ? 0 : -mPaddingLeft; 2779 } 2780 2781 @Override getTopPaddingOffset()2782 protected int getTopPaddingOffset() { 2783 return (mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK ? 0 : -mPaddingTop; 2784 } 2785 2786 @Override getRightPaddingOffset()2787 protected int getRightPaddingOffset() { 2788 return (mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK ? 0 : mPaddingRight; 2789 } 2790 2791 @Override getBottomPaddingOffset()2792 protected int getBottomPaddingOffset() { 2793 return (mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK ? 0 : mPaddingBottom; 2794 } 2795 2796 /** 2797 * @hide 2798 */ 2799 @Override internalSetPadding(int left, int top, int right, int bottom)2800 protected void internalSetPadding(int left, int top, int right, int bottom) { 2801 super.internalSetPadding(left, top, right, bottom); 2802 if (isLayoutRequested()) { 2803 handleBoundsChange(); 2804 } 2805 } 2806 2807 @Override onSizeChanged(int w, int h, int oldw, int oldh)2808 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 2809 handleBoundsChange(); 2810 if (mFastScroll != null) { 2811 mFastScroll.onSizeChanged(w, h, oldw, oldh); 2812 } 2813 } 2814 2815 /** 2816 * Called when bounds of the AbsListView are changed. AbsListView marks data set as changed 2817 * and force layouts all children that don't have exact measure specs. 2818 * <p> 2819 * This invalidation is necessary, otherwise, AbsListView may think the children are valid and 2820 * fail to relayout them properly to accommodate for new bounds. 2821 */ handleBoundsChange()2822 void handleBoundsChange() { 2823 if (mInLayout) { 2824 return; 2825 } 2826 final int childCount = getChildCount(); 2827 if (childCount > 0) { 2828 mDataChanged = true; 2829 rememberSyncState(); 2830 for (int i = 0; i < childCount; i++) { 2831 final View child = getChildAt(i); 2832 final ViewGroup.LayoutParams lp = child.getLayoutParams(); 2833 // force layout child unless it has exact specs 2834 if (lp == null || lp.width < 1 || lp.height < 1) { 2835 child.forceLayout(); 2836 } 2837 } 2838 } 2839 } 2840 2841 /** 2842 * @return True if the current touch mode requires that we draw the selector in the pressed 2843 * state. 2844 */ touchModeDrawsInPressedState()2845 boolean touchModeDrawsInPressedState() { 2846 // FIXME use isPressed for this 2847 switch (mTouchMode) { 2848 case TOUCH_MODE_TAP: 2849 case TOUCH_MODE_DONE_WAITING: 2850 return true; 2851 default: 2852 return false; 2853 } 2854 } 2855 2856 /** 2857 * Indicates whether this view is in a state where the selector should be drawn. This will 2858 * happen if we have focus but are not in touch mode, or we are in the middle of displaying 2859 * the pressed state for an item. 2860 * 2861 * @return True if the selector should be shown 2862 */ shouldShowSelector()2863 boolean shouldShowSelector() { 2864 return (isFocused() && !isInTouchMode()) || (touchModeDrawsInPressedState() && isPressed()); 2865 } 2866 drawSelector(Canvas canvas)2867 private void drawSelector(Canvas canvas) { 2868 if (shouldDrawSelector()) { 2869 final Drawable selector = mSelector; 2870 selector.setBounds(mSelectorRect); 2871 selector.draw(canvas); 2872 } 2873 } 2874 2875 /** 2876 * @hide 2877 */ 2878 @TestApi shouldDrawSelector()2879 public final boolean shouldDrawSelector() { 2880 return !mSelectorRect.isEmpty(); 2881 } 2882 2883 /** 2884 * Controls whether the selection highlight drawable should be drawn on top of the item or 2885 * behind it. 2886 * 2887 * @param onTop If true, the selector will be drawn on the item it is highlighting. The default 2888 * is false. 2889 * 2890 * @attr ref android.R.styleable#AbsListView_drawSelectorOnTop 2891 */ setDrawSelectorOnTop(boolean onTop)2892 public void setDrawSelectorOnTop(boolean onTop) { 2893 mDrawSelectorOnTop = onTop; 2894 } 2895 2896 /** 2897 * Returns whether the selection highlight drawable should be drawn on top of the item or 2898 * behind it. 2899 * 2900 * @return true if selector is drawn on top, false otherwise 2901 * @attr ref android.R.styleable#AbsListView_drawSelectorOnTop 2902 */ 2903 @InspectableProperty isDrawSelectorOnTop()2904 public boolean isDrawSelectorOnTop() { 2905 return mDrawSelectorOnTop; 2906 } 2907 2908 /** 2909 * Set a Drawable that should be used to highlight the currently selected item. 2910 * 2911 * @param resID A Drawable resource to use as the selection highlight. 2912 * 2913 * @attr ref android.R.styleable#AbsListView_listSelector 2914 */ setSelector(@rawableRes int resID)2915 public void setSelector(@DrawableRes int resID) { 2916 setSelector(getContext().getDrawable(resID)); 2917 } 2918 setSelector(Drawable sel)2919 public void setSelector(Drawable sel) { 2920 if (mSelector != null) { 2921 mSelector.setCallback(null); 2922 unscheduleDrawable(mSelector); 2923 } 2924 mSelector = sel; 2925 Rect padding = new Rect(); 2926 sel.getPadding(padding); 2927 mSelectionLeftPadding = padding.left; 2928 mSelectionTopPadding = padding.top; 2929 mSelectionRightPadding = padding.right; 2930 mSelectionBottomPadding = padding.bottom; 2931 sel.setCallback(this); 2932 updateSelectorState(); 2933 } 2934 2935 /** 2936 * Returns the selector {@link android.graphics.drawable.Drawable} that is used to draw the 2937 * selection in the list. 2938 * 2939 * @return the drawable used to display the selector 2940 */ 2941 @InspectableProperty(name = "listSelector") getSelector()2942 public Drawable getSelector() { 2943 return mSelector; 2944 } 2945 2946 /** 2947 * Sets the selector state to "pressed" and posts a CheckForKeyLongPress to see if 2948 * this is a long press. 2949 */ keyPressed()2950 void keyPressed() { 2951 if (!isEnabled() || !isClickable()) { 2952 return; 2953 } 2954 2955 Drawable selector = mSelector; 2956 Rect selectorRect = mSelectorRect; 2957 if (selector != null && (isFocused() || touchModeDrawsInPressedState()) 2958 && !selectorRect.isEmpty()) { 2959 2960 final View v = getChildAt(mSelectedPosition - mFirstPosition); 2961 2962 if (v != null) { 2963 if (v.hasExplicitFocusable()) return; 2964 v.setPressed(true); 2965 } 2966 setPressed(true); 2967 2968 final boolean longClickable = isLongClickable(); 2969 Drawable d = selector.getCurrent(); 2970 if (d != null && d instanceof TransitionDrawable) { 2971 if (longClickable) { 2972 ((TransitionDrawable) d).startTransition( 2973 ViewConfiguration.getLongPressTimeout()); 2974 } else { 2975 ((TransitionDrawable) d).resetTransition(); 2976 } 2977 } 2978 if (longClickable && !mDataChanged) { 2979 if (mPendingCheckForKeyLongPress == null) { 2980 mPendingCheckForKeyLongPress = new CheckForKeyLongPress(); 2981 } 2982 mPendingCheckForKeyLongPress.rememberWindowAttachCount(); 2983 postDelayed(mPendingCheckForKeyLongPress, ViewConfiguration.getLongPressTimeout()); 2984 } 2985 } 2986 } 2987 setScrollIndicators(View up, View down)2988 public void setScrollIndicators(View up, View down) { 2989 mScrollUp = up; 2990 mScrollDown = down; 2991 } 2992 2993 @UnsupportedAppUsage updateSelectorState()2994 void updateSelectorState() { 2995 final Drawable selector = mSelector; 2996 if (selector != null && selector.isStateful()) { 2997 if (shouldShowSelector()) { 2998 if (selector.setState(getDrawableStateForSelector())) { 2999 invalidateDrawable(selector); 3000 } 3001 } else { 3002 selector.setState(StateSet.NOTHING); 3003 } 3004 } 3005 } 3006 3007 @Override drawableStateChanged()3008 protected void drawableStateChanged() { 3009 super.drawableStateChanged(); 3010 updateSelectorState(); 3011 } 3012 getDrawableStateForSelector()3013 private int[] getDrawableStateForSelector() { 3014 // If the child view is enabled then do the default behavior. 3015 if (mIsChildViewEnabled) { 3016 // Common case 3017 return super.getDrawableState(); 3018 } 3019 3020 // The selector uses this View's drawable state. The selected child view 3021 // is disabled, so we need to remove the enabled state from the drawable 3022 // states. 3023 final int enabledState = ENABLED_STATE_SET[0]; 3024 3025 // If we don't have any extra space, it will return one of the static 3026 // state arrays, and clearing the enabled state on those arrays is a 3027 // bad thing! If we specify we need extra space, it will create+copy 3028 // into a new array that is safely mutable. 3029 final int[] state = onCreateDrawableState(1); 3030 3031 int enabledPos = -1; 3032 for (int i = state.length - 1; i >= 0; i--) { 3033 if (state[i] == enabledState) { 3034 enabledPos = i; 3035 break; 3036 } 3037 } 3038 3039 // Remove the enabled state 3040 if (enabledPos >= 0) { 3041 System.arraycopy(state, enabledPos + 1, state, enabledPos, 3042 state.length - enabledPos - 1); 3043 } 3044 3045 return state; 3046 } 3047 3048 @Override verifyDrawable(@onNull Drawable dr)3049 public boolean verifyDrawable(@NonNull Drawable dr) { 3050 return mSelector == dr || super.verifyDrawable(dr); 3051 } 3052 3053 @Override jumpDrawablesToCurrentState()3054 public void jumpDrawablesToCurrentState() { 3055 super.jumpDrawablesToCurrentState(); 3056 if (mSelector != null) mSelector.jumpToCurrentState(); 3057 } 3058 3059 @Override onAttachedToWindow()3060 protected void onAttachedToWindow() { 3061 super.onAttachedToWindow(); 3062 3063 final ViewTreeObserver treeObserver = getViewTreeObserver(); 3064 treeObserver.addOnTouchModeChangeListener(this); 3065 if (mTextFilterEnabled && mPopup != null && !mGlobalLayoutListenerAddedFilter) { 3066 treeObserver.addOnGlobalLayoutListener(this); 3067 } 3068 3069 if (mAdapter != null && mDataSetObserver == null) { 3070 mDataSetObserver = new AdapterDataSetObserver(); 3071 mAdapter.registerDataSetObserver(mDataSetObserver); 3072 3073 // Data may have changed while we were detached. Refresh. 3074 mDataChanged = true; 3075 mOldItemCount = mItemCount; 3076 mItemCount = mAdapter.getCount(); 3077 } 3078 } 3079 3080 @Override onDetachedFromWindow()3081 protected void onDetachedFromWindow() { 3082 super.onDetachedFromWindow(); 3083 3084 mIsDetaching = true; 3085 3086 // Dismiss the popup in case onSaveInstanceState() was not invoked 3087 dismissPopup(); 3088 3089 // Detach any view left in the scrap heap 3090 mRecycler.clear(); 3091 3092 final ViewTreeObserver treeObserver = getViewTreeObserver(); 3093 treeObserver.removeOnTouchModeChangeListener(this); 3094 if (mTextFilterEnabled && mPopup != null) { 3095 treeObserver.removeOnGlobalLayoutListener(this); 3096 mGlobalLayoutListenerAddedFilter = false; 3097 } 3098 3099 if (mAdapter != null && mDataSetObserver != null) { 3100 mAdapter.unregisterDataSetObserver(mDataSetObserver); 3101 mDataSetObserver = null; 3102 } 3103 3104 if (mScrollStrictSpan != null) { 3105 mScrollStrictSpan.finish(); 3106 mScrollStrictSpan = null; 3107 } 3108 3109 if (mFlingStrictSpan != null) { 3110 mFlingStrictSpan.finish(); 3111 mFlingStrictSpan = null; 3112 } 3113 3114 if (mFlingRunnable != null) { 3115 removeCallbacks(mFlingRunnable); 3116 } 3117 3118 if (mPositionScroller != null) { 3119 mPositionScroller.stop(); 3120 } 3121 3122 if (mClearScrollingCache != null) { 3123 removeCallbacks(mClearScrollingCache); 3124 } 3125 3126 if (mPerformClick != null) { 3127 removeCallbacks(mPerformClick); 3128 } 3129 3130 if (mTouchModeReset != null) { 3131 removeCallbacks(mTouchModeReset); 3132 mTouchModeReset.run(); 3133 } 3134 3135 mIsDetaching = false; 3136 } 3137 3138 @Override onWindowFocusChanged(boolean hasWindowFocus)3139 public void onWindowFocusChanged(boolean hasWindowFocus) { 3140 super.onWindowFocusChanged(hasWindowFocus); 3141 3142 final int touchMode = isInTouchMode() ? TOUCH_MODE_ON : TOUCH_MODE_OFF; 3143 3144 if (!hasWindowFocus) { 3145 setChildrenDrawingCacheEnabled(false); 3146 if (mFlingRunnable != null) { 3147 removeCallbacks(mFlingRunnable); 3148 // let the fling runnable report its new state which 3149 // should be idle 3150 mFlingRunnable.mSuppressIdleStateChangeCall = false; 3151 mFlingRunnable.endFling(); 3152 if (mPositionScroller != null) { 3153 mPositionScroller.stop(); 3154 } 3155 if (mScrollY != 0) { 3156 mScrollY = 0; 3157 invalidateParentCaches(); 3158 finishGlows(); 3159 invalidate(); 3160 } 3161 } 3162 // Always hide the type filter 3163 dismissPopup(); 3164 3165 if (touchMode == TOUCH_MODE_OFF) { 3166 // Remember the last selected element 3167 mResurrectToPosition = mSelectedPosition; 3168 } 3169 } else { 3170 if (mFiltered && !mPopupHidden) { 3171 // Show the type filter only if a filter is in effect 3172 showPopup(); 3173 } 3174 3175 // If we changed touch mode since the last time we had focus 3176 if (touchMode != mLastTouchMode && mLastTouchMode != TOUCH_MODE_UNKNOWN) { 3177 // If we come back in trackball mode, we bring the selection back 3178 if (touchMode == TOUCH_MODE_OFF) { 3179 // This will trigger a layout 3180 resurrectSelection(); 3181 3182 // If we come back in touch mode, then we want to hide the selector 3183 } else { 3184 hideSelector(); 3185 mLayoutMode = LAYOUT_NORMAL; 3186 layoutChildren(); 3187 } 3188 } 3189 } 3190 3191 mLastTouchMode = touchMode; 3192 } 3193 3194 @Override onRtlPropertiesChanged(int layoutDirection)3195 public void onRtlPropertiesChanged(int layoutDirection) { 3196 super.onRtlPropertiesChanged(layoutDirection); 3197 if (mFastScroll != null) { 3198 mFastScroll.setScrollbarPosition(getVerticalScrollbarPosition()); 3199 } 3200 } 3201 3202 /** 3203 * Creates the ContextMenuInfo returned from {@link #getContextMenuInfo()}. This 3204 * methods knows the view, position and ID of the item that received the 3205 * long press. 3206 * 3207 * @param view The view that received the long press. 3208 * @param position The position of the item that received the long press. 3209 * @param id The ID of the item that received the long press. 3210 * @return The extra information that should be returned by 3211 * {@link #getContextMenuInfo()}. 3212 */ createContextMenuInfo(View view, int position, long id)3213 ContextMenuInfo createContextMenuInfo(View view, int position, long id) { 3214 return new AdapterContextMenuInfo(view, position, id); 3215 } 3216 3217 @Override onCancelPendingInputEvents()3218 public void onCancelPendingInputEvents() { 3219 super.onCancelPendingInputEvents(); 3220 if (mPerformClick != null) { 3221 removeCallbacks(mPerformClick); 3222 } 3223 if (mPendingCheckForTap != null) { 3224 removeCallbacks(mPendingCheckForTap); 3225 } 3226 if (mPendingCheckForLongPress != null) { 3227 removeCallbacks(mPendingCheckForLongPress); 3228 } 3229 if (mPendingCheckForKeyLongPress != null) { 3230 removeCallbacks(mPendingCheckForKeyLongPress); 3231 } 3232 } 3233 3234 /** 3235 * A base class for Runnables that will check that their view is still attached to 3236 * the original window as when the Runnable was created. 3237 * 3238 */ 3239 private class WindowRunnnable { 3240 private int mOriginalAttachCount; 3241 rememberWindowAttachCount()3242 public void rememberWindowAttachCount() { 3243 mOriginalAttachCount = getWindowAttachCount(); 3244 } 3245 sameWindow()3246 public boolean sameWindow() { 3247 return getWindowAttachCount() == mOriginalAttachCount; 3248 } 3249 } 3250 3251 private class PerformClick extends WindowRunnnable implements Runnable { 3252 int mClickMotionPosition; 3253 3254 @Override run()3255 public void run() { 3256 // The data has changed since we posted this action in the event queue, 3257 // bail out before bad things happen 3258 if (mDataChanged) return; 3259 3260 final ListAdapter adapter = mAdapter; 3261 final int motionPosition = mClickMotionPosition; 3262 if (adapter != null && mItemCount > 0 && 3263 motionPosition != INVALID_POSITION && 3264 motionPosition < adapter.getCount() && sameWindow() && 3265 adapter.isEnabled(motionPosition)) { 3266 final View view = getChildAt(motionPosition - mFirstPosition); 3267 // If there is no view, something bad happened (the view scrolled off the 3268 // screen, etc.) and we should cancel the click 3269 if (view != null) { 3270 performItemClick(view, motionPosition, adapter.getItemId(motionPosition)); 3271 } 3272 } 3273 } 3274 } 3275 3276 private class CheckForLongPress extends WindowRunnnable implements Runnable { 3277 private static final int INVALID_COORD = -1; 3278 private float mX = INVALID_COORD; 3279 private float mY = INVALID_COORD; 3280 setCoords(float x, float y)3281 private void setCoords(float x, float y) { 3282 mX = x; 3283 mY = y; 3284 } 3285 3286 @Override run()3287 public void run() { 3288 final int motionPosition = mMotionPosition; 3289 final View child = getChildAt(motionPosition - mFirstPosition); 3290 if (child != null) { 3291 final int longPressPosition = mMotionPosition; 3292 final long longPressId = mAdapter.getItemId(mMotionPosition); 3293 3294 boolean handled = false; 3295 if (sameWindow() && !mDataChanged) { 3296 if (mX != INVALID_COORD && mY != INVALID_COORD) { 3297 handled = performLongPress(child, longPressPosition, longPressId, mX, mY); 3298 } else { 3299 handled = performLongPress(child, longPressPosition, longPressId); 3300 } 3301 } 3302 3303 if (handled) { 3304 mHasPerformedLongPress = true; 3305 mTouchMode = TOUCH_MODE_REST; 3306 setPressed(false); 3307 child.setPressed(false); 3308 } else { 3309 mTouchMode = TOUCH_MODE_DONE_WAITING; 3310 } 3311 } 3312 } 3313 } 3314 3315 private class CheckForKeyLongPress extends WindowRunnnable implements Runnable { 3316 @Override run()3317 public void run() { 3318 if (isPressed() && mSelectedPosition >= 0) { 3319 int index = mSelectedPosition - mFirstPosition; 3320 View v = getChildAt(index); 3321 3322 if (!mDataChanged) { 3323 boolean handled = false; 3324 if (sameWindow()) { 3325 handled = performLongPress(v, mSelectedPosition, mSelectedRowId); 3326 } 3327 if (handled) { 3328 setPressed(false); 3329 v.setPressed(false); 3330 } 3331 } else { 3332 setPressed(false); 3333 if (v != null) v.setPressed(false); 3334 } 3335 } 3336 } 3337 } 3338 performStylusButtonPressAction(MotionEvent ev)3339 private boolean performStylusButtonPressAction(MotionEvent ev) { 3340 if (mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL && mChoiceActionMode == null) { 3341 final View child = getChildAt(mMotionPosition - mFirstPosition); 3342 if (child != null) { 3343 final int longPressPosition = mMotionPosition; 3344 final long longPressId = mAdapter.getItemId(mMotionPosition); 3345 if (performLongPress(child, longPressPosition, longPressId)) { 3346 mTouchMode = TOUCH_MODE_REST; 3347 setPressed(false); 3348 child.setPressed(false); 3349 return true; 3350 } 3351 } 3352 } 3353 return false; 3354 } 3355 3356 @UnsupportedAppUsage performLongPress(final View child, final int longPressPosition, final long longPressId)3357 boolean performLongPress(final View child, 3358 final int longPressPosition, final long longPressId) { 3359 return performLongPress( 3360 child, 3361 longPressPosition, 3362 longPressId, 3363 CheckForLongPress.INVALID_COORD, 3364 CheckForLongPress.INVALID_COORD); 3365 } 3366 3367 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) performLongPress(final View child, final int longPressPosition, final long longPressId, float x, float y)3368 boolean performLongPress(final View child, 3369 final int longPressPosition, final long longPressId, float x, float y) { 3370 // CHOICE_MODE_MULTIPLE_MODAL takes over long press. 3371 if (mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL) { 3372 if (mChoiceActionMode == null && 3373 (mChoiceActionMode = startActionMode(mMultiChoiceModeCallback)) != null) { 3374 setItemChecked(longPressPosition, true); 3375 performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); 3376 } 3377 return true; 3378 } 3379 3380 boolean handled = false; 3381 if (mOnItemLongClickListener != null) { 3382 handled = mOnItemLongClickListener.onItemLongClick(AbsListView.this, child, 3383 longPressPosition, longPressId); 3384 } 3385 if (!handled) { 3386 mContextMenuInfo = createContextMenuInfo(child, longPressPosition, longPressId); 3387 if (x != CheckForLongPress.INVALID_COORD && y != CheckForLongPress.INVALID_COORD) { 3388 handled = super.showContextMenuForChild(AbsListView.this, x, y); 3389 } else { 3390 handled = super.showContextMenuForChild(AbsListView.this); 3391 } 3392 } 3393 if (handled) { 3394 performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); 3395 } 3396 return handled; 3397 } 3398 3399 @Override getContextMenuInfo()3400 protected ContextMenuInfo getContextMenuInfo() { 3401 return mContextMenuInfo; 3402 } 3403 3404 @Override showContextMenu()3405 public boolean showContextMenu() { 3406 return showContextMenuInternal(0, 0, false); 3407 } 3408 3409 @Override showContextMenu(float x, float y)3410 public boolean showContextMenu(float x, float y) { 3411 return showContextMenuInternal(x, y, true); 3412 } 3413 showContextMenuInternal(float x, float y, boolean useOffsets)3414 private boolean showContextMenuInternal(float x, float y, boolean useOffsets) { 3415 final int position = pointToPosition((int)x, (int)y); 3416 if (position != INVALID_POSITION) { 3417 final long id = mAdapter.getItemId(position); 3418 View child = getChildAt(position - mFirstPosition); 3419 if (child != null) { 3420 mContextMenuInfo = createContextMenuInfo(child, position, id); 3421 if (useOffsets) { 3422 return super.showContextMenuForChild(this, x, y); 3423 } else { 3424 return super.showContextMenuForChild(this); 3425 } 3426 } 3427 } 3428 if (useOffsets) { 3429 return super.showContextMenu(x, y); 3430 } else { 3431 return super.showContextMenu(); 3432 } 3433 } 3434 3435 @Override showContextMenuForChild(View originalView)3436 public boolean showContextMenuForChild(View originalView) { 3437 if (isShowingContextMenuWithCoords()) { 3438 return false; 3439 } 3440 return showContextMenuForChildInternal(originalView, 0, 0, false); 3441 } 3442 3443 @Override showContextMenuForChild(View originalView, float x, float y)3444 public boolean showContextMenuForChild(View originalView, float x, float y) { 3445 return showContextMenuForChildInternal(originalView,x, y, true); 3446 } 3447 showContextMenuForChildInternal(View originalView, float x, float y, boolean useOffsets)3448 private boolean showContextMenuForChildInternal(View originalView, float x, float y, 3449 boolean useOffsets) { 3450 final int longPressPosition = getPositionForView(originalView); 3451 if (longPressPosition < 0) { 3452 return false; 3453 } 3454 3455 final long longPressId = mAdapter.getItemId(longPressPosition); 3456 boolean handled = false; 3457 3458 if (mOnItemLongClickListener != null) { 3459 handled = mOnItemLongClickListener.onItemLongClick(this, originalView, 3460 longPressPosition, longPressId); 3461 } 3462 3463 if (!handled) { 3464 final View child = getChildAt(longPressPosition - mFirstPosition); 3465 mContextMenuInfo = createContextMenuInfo(child, longPressPosition, longPressId); 3466 3467 if (useOffsets) { 3468 handled = super.showContextMenuForChild(originalView, x, y); 3469 } else { 3470 handled = super.showContextMenuForChild(originalView); 3471 } 3472 } 3473 3474 return handled; 3475 } 3476 3477 @Override onKeyDown(int keyCode, KeyEvent event)3478 public boolean onKeyDown(int keyCode, KeyEvent event) { 3479 return false; 3480 } 3481 3482 @Override onKeyUp(int keyCode, KeyEvent event)3483 public boolean onKeyUp(int keyCode, KeyEvent event) { 3484 if (KeyEvent.isConfirmKey(keyCode)) { 3485 if (!isEnabled()) { 3486 return true; 3487 } 3488 if (isClickable() && isPressed() && 3489 mSelectedPosition >= 0 && mAdapter != null && 3490 mSelectedPosition < mAdapter.getCount()) { 3491 3492 final View view = getChildAt(mSelectedPosition - mFirstPosition); 3493 if (view != null) { 3494 performItemClick(view, mSelectedPosition, mSelectedRowId); 3495 view.setPressed(false); 3496 } 3497 setPressed(false); 3498 return true; 3499 } 3500 } 3501 return super.onKeyUp(keyCode, event); 3502 } 3503 3504 @Override dispatchSetPressed(boolean pressed)3505 protected void dispatchSetPressed(boolean pressed) { 3506 // Don't dispatch setPressed to our children. We call setPressed on ourselves to 3507 // get the selector in the right state, but we don't want to press each child. 3508 } 3509 3510 @Override dispatchDrawableHotspotChanged(float x, float y)3511 public void dispatchDrawableHotspotChanged(float x, float y) { 3512 // Don't dispatch hotspot changes to children. We'll manually handle 3513 // calling drawableHotspotChanged on the correct child. 3514 } 3515 3516 /** 3517 * Maps a point to a position in the list. 3518 * 3519 * @param x X in local coordinate 3520 * @param y Y in local coordinate 3521 * @return The position of the item which contains the specified point, or 3522 * {@link #INVALID_POSITION} if the point does not intersect an item. 3523 */ pointToPosition(int x, int y)3524 public int pointToPosition(int x, int y) { 3525 Rect frame = mTouchFrame; 3526 if (frame == null) { 3527 mTouchFrame = new Rect(); 3528 frame = mTouchFrame; 3529 } 3530 3531 final int count = getChildCount(); 3532 for (int i = count - 1; i >= 0; i--) { 3533 final View child = getChildAt(i); 3534 if (child.getVisibility() == View.VISIBLE) { 3535 child.getHitRect(frame); 3536 if (frame.contains(x, y)) { 3537 return mFirstPosition + i; 3538 } 3539 } 3540 } 3541 return INVALID_POSITION; 3542 } 3543 3544 3545 /** 3546 * Maps a point to a the rowId of the item which intersects that point. 3547 * 3548 * @param x X in local coordinate 3549 * @param y Y in local coordinate 3550 * @return The rowId of the item which contains the specified point, or {@link #INVALID_ROW_ID} 3551 * if the point does not intersect an item. 3552 */ pointToRowId(int x, int y)3553 public long pointToRowId(int x, int y) { 3554 int position = pointToPosition(x, y); 3555 if (position >= 0) { 3556 return mAdapter.getItemId(position); 3557 } 3558 return INVALID_ROW_ID; 3559 } 3560 3561 private final class CheckForTap implements Runnable { 3562 float x; 3563 float y; 3564 3565 @Override run()3566 public void run() { 3567 if (mTouchMode == TOUCH_MODE_DOWN) { 3568 mTouchMode = TOUCH_MODE_TAP; 3569 final View child = getChildAt(mMotionPosition - mFirstPosition); 3570 if (child != null && !child.hasExplicitFocusable()) { 3571 mLayoutMode = LAYOUT_NORMAL; 3572 3573 if (!mDataChanged) { 3574 final float[] point = mTmpPoint; 3575 point[0] = x; 3576 point[1] = y; 3577 transformPointToViewLocal(point, child); 3578 child.drawableHotspotChanged(point[0], point[1]); 3579 child.setPressed(true); 3580 setPressed(true); 3581 layoutChildren(); 3582 positionSelector(mMotionPosition, child); 3583 refreshDrawableState(); 3584 3585 final int longPressTimeout = ViewConfiguration.getLongPressTimeout(); 3586 final boolean longClickable = isLongClickable(); 3587 3588 if (mSelector != null) { 3589 final Drawable d = mSelector.getCurrent(); 3590 if (d != null && d instanceof TransitionDrawable) { 3591 if (longClickable) { 3592 ((TransitionDrawable) d).startTransition(longPressTimeout); 3593 } else { 3594 ((TransitionDrawable) d).resetTransition(); 3595 } 3596 } 3597 mSelector.setHotspot(x, y); 3598 } 3599 3600 if (longClickable) { 3601 if (mPendingCheckForLongPress == null) { 3602 mPendingCheckForLongPress = new CheckForLongPress(); 3603 } 3604 mPendingCheckForLongPress.setCoords(x, y); 3605 mPendingCheckForLongPress.rememberWindowAttachCount(); 3606 postDelayed(mPendingCheckForLongPress, longPressTimeout); 3607 } else { 3608 mTouchMode = TOUCH_MODE_DONE_WAITING; 3609 } 3610 } else { 3611 mTouchMode = TOUCH_MODE_DONE_WAITING; 3612 } 3613 } 3614 } 3615 } 3616 } 3617 startScrollIfNeeded(int x, int y, MotionEvent vtev)3618 private boolean startScrollIfNeeded(int x, int y, MotionEvent vtev) { 3619 // Check if we have moved far enough that it looks more like a 3620 // scroll than a tap 3621 final int deltaY = y - mMotionY; 3622 final int distance = Math.abs(deltaY); 3623 final boolean overscroll = mScrollY != 0; 3624 if ((overscroll || distance > mTouchSlop) && 3625 (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) { 3626 createScrollingCache(); 3627 if (overscroll) { 3628 mTouchMode = TOUCH_MODE_OVERSCROLL; 3629 mMotionCorrection = 0; 3630 } else { 3631 mTouchMode = TOUCH_MODE_SCROLL; 3632 mMotionCorrection = deltaY > 0 ? mTouchSlop : -mTouchSlop; 3633 } 3634 removeCallbacks(mPendingCheckForLongPress); 3635 setPressed(false); 3636 final View motionView = getChildAt(mMotionPosition - mFirstPosition); 3637 if (motionView != null) { 3638 motionView.setPressed(false); 3639 } 3640 reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); 3641 // Time to start stealing events! Once we've stolen them, don't let anyone 3642 // steal from us 3643 final ViewParent parent = getParent(); 3644 if (parent != null) { 3645 parent.requestDisallowInterceptTouchEvent(true); 3646 } 3647 scrollIfNeeded(x, y, vtev); 3648 return true; 3649 } 3650 3651 return false; 3652 } 3653 scrollIfNeeded(int x, int y, MotionEvent vtev)3654 private void scrollIfNeeded(int x, int y, MotionEvent vtev) { 3655 int rawDeltaY = y - mMotionY; 3656 int scrollOffsetCorrection = 0; 3657 if (mLastY == Integer.MIN_VALUE) { 3658 rawDeltaY -= mMotionCorrection; 3659 } 3660 3661 int incrementalDeltaY = mLastY != Integer.MIN_VALUE ? y - mLastY : rawDeltaY; 3662 3663 // First allow releasing existing overscroll effect: 3664 incrementalDeltaY = releaseGlow(incrementalDeltaY, x); 3665 3666 if (dispatchNestedPreScroll(0, -incrementalDeltaY, mScrollConsumed, mScrollOffset)) { 3667 rawDeltaY += mScrollConsumed[1]; 3668 scrollOffsetCorrection = -mScrollOffset[1]; 3669 incrementalDeltaY += mScrollConsumed[1]; 3670 if (vtev != null) { 3671 vtev.offsetLocation(0, mScrollOffset[1]); 3672 mNestedYOffset += mScrollOffset[1]; 3673 } 3674 } 3675 final int deltaY = rawDeltaY; 3676 int lastYCorrection = 0; 3677 3678 if (mTouchMode == TOUCH_MODE_SCROLL) { 3679 if (PROFILE_SCROLLING) { 3680 if (!mScrollProfilingStarted) { 3681 Debug.startMethodTracing("AbsListViewScroll"); 3682 mScrollProfilingStarted = true; 3683 } 3684 } 3685 3686 if (mScrollStrictSpan == null) { 3687 // If it's non-null, we're already in a scroll. 3688 mScrollStrictSpan = StrictMode.enterCriticalSpan("AbsListView-scroll"); 3689 } 3690 3691 if (y != mLastY) { 3692 // We may be here after stopping a fling and continuing to scroll. 3693 // If so, we haven't disallowed intercepting touch events yet. 3694 // Make sure that we do so in case we're in a parent that can intercept. 3695 if ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) == 0 && 3696 Math.abs(rawDeltaY) > mTouchSlop) { 3697 final ViewParent parent = getParent(); 3698 if (parent != null) { 3699 parent.requestDisallowInterceptTouchEvent(true); 3700 } 3701 } 3702 3703 final int motionIndex; 3704 if (mMotionPosition >= 0) { 3705 motionIndex = mMotionPosition - mFirstPosition; 3706 } else { 3707 // If we don't have a motion position that we can reliably track, 3708 // pick something in the middle to make a best guess at things below. 3709 motionIndex = getChildCount() / 2; 3710 } 3711 3712 int motionViewPrevTop = 0; 3713 View motionView = this.getChildAt(motionIndex); 3714 if (motionView != null) { 3715 motionViewPrevTop = motionView.getTop(); 3716 } 3717 3718 // No need to do all this work if we're not going to move anyway 3719 boolean atEdge = false; 3720 if (incrementalDeltaY != 0) { 3721 atEdge = trackMotionScroll(deltaY, incrementalDeltaY); 3722 } 3723 3724 // Check to see if we have bumped into the scroll limit 3725 motionView = this.getChildAt(motionIndex); 3726 if (motionView != null) { 3727 // Check if the top of the motion view is where it is 3728 // supposed to be 3729 final int motionViewRealTop = motionView.getTop(); 3730 if (atEdge) { 3731 // Apply overscroll 3732 3733 int overscroll = -incrementalDeltaY - 3734 (motionViewRealTop - motionViewPrevTop); 3735 if (dispatchNestedScroll(0, overscroll - incrementalDeltaY, 0, overscroll, 3736 mScrollOffset)) { 3737 lastYCorrection -= mScrollOffset[1]; 3738 if (vtev != null) { 3739 vtev.offsetLocation(0, mScrollOffset[1]); 3740 mNestedYOffset += mScrollOffset[1]; 3741 } 3742 } else { 3743 final boolean atOverscrollEdge = overScrollBy(0, overscroll, 3744 0, mScrollY, 0, 0, 0, mOverscrollDistance, true); 3745 3746 if (atOverscrollEdge && mVelocityTracker != null) { 3747 // Don't allow overfling if we're at the edge 3748 mVelocityTracker.clear(); 3749 } 3750 3751 final int overscrollMode = getOverScrollMode(); 3752 if (overscrollMode == OVER_SCROLL_ALWAYS || 3753 (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && 3754 !contentFits())) { 3755 if (!atOverscrollEdge) { 3756 mDirection = 0; // Reset when entering overscroll. 3757 mTouchMode = TOUCH_MODE_OVERSCROLL; 3758 } 3759 if (incrementalDeltaY > 0) { 3760 mEdgeGlowTop.onPullDistance((float) -overscroll / getHeight(), 3761 (float) x / getWidth()); 3762 if (!mEdgeGlowBottom.isFinished()) { 3763 mEdgeGlowBottom.onRelease(); 3764 } 3765 invalidateEdgeEffects(); 3766 } else if (incrementalDeltaY < 0) { 3767 mEdgeGlowBottom.onPullDistance((float) overscroll / getHeight(), 3768 1.f - (float) x / getWidth()); 3769 if (!mEdgeGlowTop.isFinished()) { 3770 mEdgeGlowTop.onRelease(); 3771 } 3772 invalidateEdgeEffects(); 3773 } 3774 } 3775 } 3776 } 3777 mMotionY = y + lastYCorrection + scrollOffsetCorrection; 3778 } 3779 mLastY = y + lastYCorrection + scrollOffsetCorrection; 3780 } 3781 } else if (mTouchMode == TOUCH_MODE_OVERSCROLL) { 3782 if (y != mLastY) { 3783 final int oldScroll = mScrollY; 3784 final int newScroll = oldScroll - incrementalDeltaY; 3785 int newDirection = y > mLastY ? 1 : -1; 3786 3787 if (mDirection == 0) { 3788 mDirection = newDirection; 3789 } 3790 3791 int overScrollDistance = -incrementalDeltaY; 3792 if ((newScroll < 0 && oldScroll >= 0) || (newScroll > 0 && oldScroll <= 0)) { 3793 overScrollDistance = -oldScroll; 3794 incrementalDeltaY += overScrollDistance; 3795 } else { 3796 incrementalDeltaY = 0; 3797 } 3798 3799 if (overScrollDistance != 0) { 3800 overScrollBy(0, overScrollDistance, 0, mScrollY, 0, 0, 3801 0, mOverscrollDistance, true); 3802 final int overscrollMode = getOverScrollMode(); 3803 if (overscrollMode == OVER_SCROLL_ALWAYS || 3804 (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && 3805 !contentFits())) { 3806 if (rawDeltaY > 0) { 3807 mEdgeGlowTop.onPullDistance((float) overScrollDistance / getHeight(), 3808 (float) x / getWidth()); 3809 if (!mEdgeGlowBottom.isFinished()) { 3810 mEdgeGlowBottom.onRelease(); 3811 } 3812 invalidateEdgeEffects(); 3813 } else if (rawDeltaY < 0) { 3814 mEdgeGlowBottom.onPullDistance( 3815 (float) -overScrollDistance / getHeight(), 3816 1.f - (float) x / getWidth()); 3817 if (!mEdgeGlowTop.isFinished()) { 3818 mEdgeGlowTop.onRelease(); 3819 } 3820 invalidateEdgeEffects(); 3821 } 3822 } 3823 } 3824 3825 if (incrementalDeltaY != 0) { 3826 // Coming back to 'real' list scrolling 3827 if (mScrollY != 0) { 3828 mScrollY = 0; 3829 invalidateParentIfNeeded(); 3830 } 3831 3832 trackMotionScroll(incrementalDeltaY, incrementalDeltaY); 3833 3834 mTouchMode = TOUCH_MODE_SCROLL; 3835 3836 // We did not scroll the full amount. Treat this essentially like the 3837 // start of a new touch scroll 3838 final int motionPosition = findClosestMotionRow(y); 3839 3840 mMotionCorrection = 0; 3841 View motionView = getChildAt(motionPosition - mFirstPosition); 3842 mMotionViewOriginalTop = motionView != null ? motionView.getTop() : 0; 3843 mMotionY = y + scrollOffsetCorrection; 3844 mMotionPosition = motionPosition; 3845 } 3846 mLastY = y + lastYCorrection + scrollOffsetCorrection; 3847 mDirection = newDirection; 3848 } 3849 } 3850 } 3851 3852 /** 3853 * If the edge glow is currently active, this consumes part or all of deltaY 3854 * on the edge glow. 3855 * 3856 * @param deltaY The pointer motion, in pixels, in the vertical direction, positive 3857 * for moving down and negative for moving up. 3858 * @param x The horizontal position of the pointer. 3859 * @return The remainder of <code>deltaY</code> that has not been consumed by the 3860 * edge glow. 3861 */ releaseGlow(int deltaY, int x)3862 private int releaseGlow(int deltaY, int x) { 3863 // First allow releasing existing overscroll effect: 3864 float consumed = 0; 3865 if (mEdgeGlowTop.getDistance() != 0) { 3866 if (canScrollUp()) { 3867 mEdgeGlowTop.onRelease(); 3868 } else { 3869 consumed = mEdgeGlowTop.onPullDistance((float) deltaY / getHeight(), 3870 (float) x / getWidth()); 3871 } 3872 invalidateEdgeEffects(); 3873 } else if (mEdgeGlowBottom.getDistance() != 0) { 3874 if (canScrollDown()) { 3875 mEdgeGlowBottom.onRelease(); 3876 } else { 3877 consumed = -mEdgeGlowBottom.onPullDistance((float) -deltaY / getHeight(), 3878 1f - (float) x / getWidth()); 3879 } 3880 invalidateEdgeEffects(); 3881 } 3882 int pixelsConsumed = Math.round(consumed * getHeight()); 3883 return deltaY - pixelsConsumed; 3884 } 3885 3886 /** 3887 * @return <code>true</code> if either the top or bottom edge glow is currently active or 3888 * <code>false</code> if it has no value to release. 3889 */ doesTouchStopStretch()3890 private boolean doesTouchStopStretch() { 3891 return (mEdgeGlowBottom.getDistance() != 0 && !canScrollDown()) 3892 || (mEdgeGlowTop.getDistance() != 0 && !canScrollUp()); 3893 } 3894 invalidateEdgeEffects()3895 private void invalidateEdgeEffects() { 3896 if (!shouldDisplayEdgeEffects()) { 3897 return; 3898 } 3899 invalidate(); 3900 } 3901 3902 @Override onTouchModeChanged(boolean isInTouchMode)3903 public void onTouchModeChanged(boolean isInTouchMode) { 3904 if (isInTouchMode) { 3905 // Get rid of the selection when we enter touch mode 3906 hideSelector(); 3907 // Layout, but only if we already have done so previously. 3908 // (Otherwise may clobber a LAYOUT_SYNC layout that was requested to restore 3909 // state.) 3910 if (getHeight() > 0 && getChildCount() > 0) { 3911 // We do not lose focus initiating a touch (since AbsListView is focusable in 3912 // touch mode). Force an initial layout to get rid of the selection. 3913 layoutChildren(); 3914 } 3915 updateSelectorState(); 3916 } else { 3917 int touchMode = mTouchMode; 3918 if (touchMode == TOUCH_MODE_OVERSCROLL || touchMode == TOUCH_MODE_OVERFLING) { 3919 if (mFlingRunnable != null) { 3920 mFlingRunnable.endFling(); 3921 } 3922 if (mPositionScroller != null) { 3923 mPositionScroller.stop(); 3924 } 3925 3926 if (mScrollY != 0) { 3927 mScrollY = 0; 3928 invalidateParentCaches(); 3929 finishGlows(); 3930 invalidate(); 3931 } 3932 } 3933 } 3934 } 3935 3936 /** @hide */ 3937 @Override handleScrollBarDragging(MotionEvent event)3938 protected boolean handleScrollBarDragging(MotionEvent event) { 3939 // Doesn't support normal scroll bar dragging. Use FastScroller. 3940 return false; 3941 } 3942 3943 @Override onTouchEvent(MotionEvent ev)3944 public boolean onTouchEvent(MotionEvent ev) { 3945 if (!isEnabled()) { 3946 // A disabled view that is clickable still consumes the touch 3947 // events, it just doesn't respond to them. 3948 return isClickable() || isLongClickable(); 3949 } 3950 3951 if (mPositionScroller != null) { 3952 mPositionScroller.stop(); 3953 } 3954 3955 if (mIsDetaching || !isAttachedToWindow()) { 3956 // Something isn't right. 3957 // Since we rely on being attached to get data set change notifications, 3958 // don't risk doing anything where we might try to resync and find things 3959 // in a bogus state. 3960 return false; 3961 } 3962 3963 startNestedScroll(SCROLL_AXIS_VERTICAL); 3964 3965 if (mFastScroll != null && mFastScroll.onTouchEvent(ev)) { 3966 return true; 3967 } 3968 3969 initVelocityTrackerIfNotExists(); 3970 final MotionEvent vtev = MotionEvent.obtain(ev); 3971 3972 final int actionMasked = ev.getActionMasked(); 3973 if (actionMasked == MotionEvent.ACTION_DOWN) { 3974 mNestedYOffset = 0; 3975 } 3976 vtev.offsetLocation(0, mNestedYOffset); 3977 switch (actionMasked) { 3978 case MotionEvent.ACTION_DOWN: { 3979 onTouchDown(ev); 3980 break; 3981 } 3982 3983 case MotionEvent.ACTION_MOVE: { 3984 onTouchMove(ev, vtev); 3985 break; 3986 } 3987 3988 case MotionEvent.ACTION_UP: { 3989 onTouchUp(ev); 3990 break; 3991 } 3992 3993 case MotionEvent.ACTION_CANCEL: { 3994 onTouchCancel(); 3995 break; 3996 } 3997 3998 case MotionEvent.ACTION_POINTER_UP: { 3999 onSecondaryPointerUp(ev); 4000 final int x = mMotionX; 4001 final int y = mMotionY; 4002 final int motionPosition = pointToPosition(x, y); 4003 if (motionPosition >= 0) { 4004 // Remember where the motion event started 4005 final View child = getChildAt(motionPosition - mFirstPosition); 4006 mMotionViewOriginalTop = child.getTop(); 4007 mMotionPosition = motionPosition; 4008 } 4009 mLastY = y; 4010 break; 4011 } 4012 4013 case MotionEvent.ACTION_POINTER_DOWN: { 4014 // New pointers take over dragging duties 4015 final int index = ev.getActionIndex(); 4016 final int id = ev.getPointerId(index); 4017 final int x = (int) ev.getX(index); 4018 final int y = (int) ev.getY(index); 4019 mMotionCorrection = 0; 4020 mActivePointerId = id; 4021 mMotionX = x; 4022 mMotionY = y; 4023 final int motionPosition = pointToPosition(x, y); 4024 if (motionPosition >= 0) { 4025 // Remember where the motion event started 4026 final View child = getChildAt(motionPosition - mFirstPosition); 4027 mMotionViewOriginalTop = child.getTop(); 4028 mMotionPosition = motionPosition; 4029 } 4030 mLastY = y; 4031 break; 4032 } 4033 } 4034 4035 if (mVelocityTracker != null) { 4036 mVelocityTracker.addMovement(vtev); 4037 } 4038 vtev.recycle(); 4039 return true; 4040 } 4041 onTouchDown(MotionEvent ev)4042 private void onTouchDown(MotionEvent ev) { 4043 mHasPerformedLongPress = false; 4044 mActivePointerId = ev.getPointerId(0); 4045 hideSelector(); 4046 4047 if (mTouchMode == TOUCH_MODE_OVERFLING) { 4048 // Stopped the fling. It is a scroll. 4049 if (mFlingRunnable != null) { 4050 mFlingRunnable.endFling(); 4051 } 4052 if (mPositionScroller != null) { 4053 mPositionScroller.stop(); 4054 } 4055 mTouchMode = TOUCH_MODE_OVERSCROLL; 4056 mMotionX = (int) ev.getX(); 4057 mMotionY = (int) ev.getY(); 4058 mLastY = mMotionY; 4059 mMotionCorrection = 0; 4060 mDirection = 0; 4061 stopEdgeGlowRecede(ev.getX()); 4062 } else { 4063 final int x = (int) ev.getX(); 4064 final int y = (int) ev.getY(); 4065 int motionPosition = pointToPosition(x, y); 4066 4067 if (!mDataChanged) { 4068 if (mTouchMode == TOUCH_MODE_FLING) { 4069 // Stopped a fling. It is a scroll. 4070 createScrollingCache(); 4071 mTouchMode = TOUCH_MODE_SCROLL; 4072 mMotionCorrection = 0; 4073 motionPosition = findMotionRow(y); 4074 if (mFlingRunnable != null) { 4075 mFlingRunnable.flywheelTouch(); 4076 } 4077 stopEdgeGlowRecede(x); 4078 } else if ((motionPosition >= 0) && getAdapter().isEnabled(motionPosition)) { 4079 // User clicked on an actual view (and was not stopping a 4080 // fling). It might be a click or a scroll. Assume it is a 4081 // click until proven otherwise. 4082 mTouchMode = TOUCH_MODE_DOWN; 4083 4084 // FIXME Debounce 4085 if (mPendingCheckForTap == null) { 4086 mPendingCheckForTap = new CheckForTap(); 4087 } 4088 4089 mPendingCheckForTap.x = ev.getX(); 4090 mPendingCheckForTap.y = ev.getY(); 4091 postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); 4092 } 4093 } 4094 4095 if (motionPosition >= 0) { 4096 // Remember where the motion event started 4097 final View v = getChildAt(motionPosition - mFirstPosition); 4098 mMotionViewOriginalTop = v.getTop(); 4099 } 4100 4101 mMotionX = x; 4102 mMotionY = y; 4103 mMotionPosition = motionPosition; 4104 mLastY = Integer.MIN_VALUE; 4105 } 4106 4107 if (mTouchMode == TOUCH_MODE_DOWN && mMotionPosition != INVALID_POSITION 4108 && performButtonActionOnTouchDown(ev)) { 4109 removeCallbacks(mPendingCheckForTap); 4110 } 4111 } 4112 stopEdgeGlowRecede(float x)4113 private void stopEdgeGlowRecede(float x) { 4114 if (mEdgeGlowTop.getDistance() != 0) { 4115 mEdgeGlowTop.onPullDistance(0, x / getWidth()); 4116 } 4117 if (mEdgeGlowBottom.getDistance() != 0) { 4118 mEdgeGlowBottom.onPullDistance(0, x / getWidth()); 4119 } 4120 } 4121 onTouchMove(MotionEvent ev, MotionEvent vtev)4122 private void onTouchMove(MotionEvent ev, MotionEvent vtev) { 4123 if (mHasPerformedLongPress) { 4124 // Consume all move events following a successful long press. 4125 return; 4126 } 4127 4128 int pointerIndex = ev.findPointerIndex(mActivePointerId); 4129 if (pointerIndex == -1) { 4130 pointerIndex = 0; 4131 mActivePointerId = ev.getPointerId(pointerIndex); 4132 } 4133 4134 if (mDataChanged) { 4135 // Re-sync everything if data has been changed 4136 // since the scroll operation can query the adapter. 4137 layoutChildren(); 4138 } 4139 4140 final int y = (int) ev.getY(pointerIndex); 4141 4142 switch (mTouchMode) { 4143 case TOUCH_MODE_DOWN: 4144 case TOUCH_MODE_TAP: 4145 case TOUCH_MODE_DONE_WAITING: 4146 // Check if we have moved far enough that it looks more like a 4147 // scroll than a tap. If so, we'll enter scrolling mode. 4148 if (startScrollIfNeeded((int) ev.getX(pointerIndex), y, vtev)) { 4149 break; 4150 } 4151 // Otherwise, check containment within list bounds. If we're 4152 // outside bounds, cancel any active presses. 4153 final View motionView = getChildAt(mMotionPosition - mFirstPosition); 4154 final float x = ev.getX(pointerIndex); 4155 if (!pointInView(x, y, mTouchSlop)) { 4156 setPressed(false); 4157 if (motionView != null) { 4158 motionView.setPressed(false); 4159 } 4160 removeCallbacks(mTouchMode == TOUCH_MODE_DOWN ? 4161 mPendingCheckForTap : mPendingCheckForLongPress); 4162 mTouchMode = TOUCH_MODE_DONE_WAITING; 4163 updateSelectorState(); 4164 } else if (motionView != null) { 4165 // Still within bounds, update the hotspot. 4166 final float[] point = mTmpPoint; 4167 point[0] = x; 4168 point[1] = y; 4169 transformPointToViewLocal(point, motionView); 4170 motionView.drawableHotspotChanged(point[0], point[1]); 4171 } 4172 break; 4173 case TOUCH_MODE_SCROLL: 4174 case TOUCH_MODE_OVERSCROLL: 4175 scrollIfNeeded((int) ev.getX(pointerIndex), y, vtev); 4176 break; 4177 } 4178 } 4179 onTouchUp(MotionEvent ev)4180 private void onTouchUp(MotionEvent ev) { 4181 switch (mTouchMode) { 4182 case TOUCH_MODE_DOWN: 4183 case TOUCH_MODE_TAP: 4184 case TOUCH_MODE_DONE_WAITING: 4185 final int motionPosition = mMotionPosition; 4186 final View child = getChildAt(motionPosition - mFirstPosition); 4187 if (child != null) { 4188 if (mTouchMode != TOUCH_MODE_DOWN) { 4189 child.setPressed(false); 4190 } 4191 4192 final float x = ev.getX(); 4193 final boolean inList = 4194 x > mListPadding.left && x < getWidth() - mListPadding.right; 4195 if (inList && !child.hasExplicitFocusable()) { 4196 if (mPerformClick == null) { 4197 mPerformClick = new PerformClick(); 4198 } 4199 4200 final AbsListView.PerformClick performClick = mPerformClick; 4201 performClick.mClickMotionPosition = motionPosition; 4202 performClick.rememberWindowAttachCount(); 4203 4204 mResurrectToPosition = motionPosition; 4205 4206 if (mTouchMode == TOUCH_MODE_DOWN || mTouchMode == TOUCH_MODE_TAP) { 4207 removeCallbacks(mTouchMode == TOUCH_MODE_DOWN 4208 ? mPendingCheckForTap : mPendingCheckForLongPress); 4209 mLayoutMode = LAYOUT_NORMAL; 4210 if (!mDataChanged && mAdapter.isEnabled(motionPosition)) { 4211 mTouchMode = TOUCH_MODE_TAP; 4212 setSelectedPositionInt(mMotionPosition); 4213 layoutChildren(); 4214 child.setPressed(true); 4215 positionSelector(mMotionPosition, child); 4216 setPressed(true); 4217 if (mSelector != null) { 4218 Drawable d = mSelector.getCurrent(); 4219 if (d != null && d instanceof TransitionDrawable) { 4220 ((TransitionDrawable) d).resetTransition(); 4221 } 4222 mSelector.setHotspot(x, ev.getY()); 4223 } 4224 if (mTouchModeReset != null) { 4225 removeCallbacks(mTouchModeReset); 4226 } 4227 mTouchModeReset = new Runnable() { 4228 @Override 4229 public void run() { 4230 mTouchModeReset = null; 4231 mTouchMode = TOUCH_MODE_REST; 4232 child.setPressed(false); 4233 setPressed(false); 4234 if (!mDataChanged && !mIsDetaching 4235 && isAttachedToWindow()) { 4236 performClick.run(); 4237 } 4238 } 4239 }; 4240 postDelayed(mTouchModeReset, 4241 ViewConfiguration.getPressedStateDuration()); 4242 } else { 4243 mTouchMode = TOUCH_MODE_REST; 4244 updateSelectorState(); 4245 } 4246 return; 4247 } else if (!mDataChanged && mAdapter.isEnabled(motionPosition)) { 4248 performClick.run(); 4249 } 4250 } 4251 } 4252 mTouchMode = TOUCH_MODE_REST; 4253 updateSelectorState(); 4254 break; 4255 case TOUCH_MODE_SCROLL: 4256 final int childCount = getChildCount(); 4257 if (childCount > 0) { 4258 final int firstChildTop = getChildAt(0).getTop(); 4259 final int lastChildBottom = getChildAt(childCount - 1).getBottom(); 4260 final int contentTop = mListPadding.top; 4261 final int contentBottom = getHeight() - mListPadding.bottom; 4262 if (mFirstPosition == 0 && firstChildTop >= contentTop 4263 && mFirstPosition + childCount < mItemCount 4264 && lastChildBottom <= getHeight() - contentBottom) { 4265 mTouchMode = TOUCH_MODE_REST; 4266 reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); 4267 } else { 4268 final VelocityTracker velocityTracker = mVelocityTracker; 4269 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 4270 4271 final int initialVelocity = (int) 4272 (velocityTracker.getYVelocity(mActivePointerId) * mVelocityScale); 4273 // Fling if we have enough velocity and we aren't at a boundary. 4274 // Since we can potentially overfling more than we can overscroll, don't 4275 // allow the weird behavior where you can scroll to a boundary then 4276 // fling further. 4277 boolean flingVelocity = Math.abs(initialVelocity) > mMinimumVelocity; 4278 if (flingVelocity && !mEdgeGlowTop.isFinished()) { 4279 mEdgeGlowTop.onAbsorb(initialVelocity); 4280 } else if (flingVelocity && !mEdgeGlowBottom.isFinished()) { 4281 mEdgeGlowBottom.onAbsorb(-initialVelocity); 4282 } else if (flingVelocity 4283 && !((mFirstPosition == 0 4284 && firstChildTop == contentTop - mOverscrollDistance) 4285 || (mFirstPosition + childCount == mItemCount 4286 && lastChildBottom == contentBottom + mOverscrollDistance)) 4287 ) { 4288 if (!dispatchNestedPreFling(0, -initialVelocity)) { 4289 if (mFlingRunnable == null) { 4290 mFlingRunnable = new FlingRunnable(); 4291 } 4292 reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING); 4293 mFlingRunnable.start(-initialVelocity); 4294 dispatchNestedFling(0, -initialVelocity, true); 4295 } else { 4296 mTouchMode = TOUCH_MODE_REST; 4297 reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); 4298 } 4299 } else { 4300 mTouchMode = TOUCH_MODE_REST; 4301 reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); 4302 if (mFlingRunnable != null) { 4303 mFlingRunnable.endFling(); 4304 } 4305 if (mPositionScroller != null) { 4306 mPositionScroller.stop(); 4307 } 4308 if (flingVelocity && !dispatchNestedPreFling(0, -initialVelocity)) { 4309 dispatchNestedFling(0, -initialVelocity, false); 4310 } 4311 } 4312 } 4313 } else { 4314 mTouchMode = TOUCH_MODE_REST; 4315 reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); 4316 } 4317 break; 4318 4319 case TOUCH_MODE_OVERSCROLL: 4320 if (mFlingRunnable == null) { 4321 mFlingRunnable = new FlingRunnable(); 4322 } 4323 final VelocityTracker velocityTracker = mVelocityTracker; 4324 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 4325 final int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId); 4326 4327 reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING); 4328 if (Math.abs(initialVelocity) > mMinimumVelocity) { 4329 mFlingRunnable.startOverfling(-initialVelocity); 4330 } else { 4331 mFlingRunnable.startSpringback(); 4332 } 4333 4334 break; 4335 } 4336 4337 setPressed(false); 4338 4339 if (shouldDisplayEdgeEffects()) { 4340 mEdgeGlowTop.onRelease(); 4341 mEdgeGlowBottom.onRelease(); 4342 } 4343 4344 // Need to redraw since we probably aren't drawing the selector anymore 4345 invalidate(); 4346 removeCallbacks(mPendingCheckForLongPress); 4347 recycleVelocityTracker(); 4348 4349 mActivePointerId = INVALID_POINTER; 4350 4351 if (PROFILE_SCROLLING) { 4352 if (mScrollProfilingStarted) { 4353 Debug.stopMethodTracing(); 4354 mScrollProfilingStarted = false; 4355 } 4356 } 4357 4358 if (mScrollStrictSpan != null) { 4359 mScrollStrictSpan.finish(); 4360 mScrollStrictSpan = null; 4361 } 4362 } 4363 shouldDisplayEdgeEffects()4364 private boolean shouldDisplayEdgeEffects() { 4365 return getOverScrollMode() != OVER_SCROLL_NEVER; 4366 } 4367 onTouchCancel()4368 private void onTouchCancel() { 4369 switch (mTouchMode) { 4370 case TOUCH_MODE_OVERSCROLL: 4371 if (mFlingRunnable == null) { 4372 mFlingRunnable = new FlingRunnable(); 4373 } 4374 mFlingRunnable.startSpringback(); 4375 break; 4376 4377 case TOUCH_MODE_OVERFLING: 4378 // Do nothing - let it play out. 4379 break; 4380 4381 default: 4382 mTouchMode = TOUCH_MODE_REST; 4383 setPressed(false); 4384 final View motionView = this.getChildAt(mMotionPosition - mFirstPosition); 4385 if (motionView != null) { 4386 motionView.setPressed(false); 4387 } 4388 clearScrollingCache(); 4389 removeCallbacks(mPendingCheckForLongPress); 4390 recycleVelocityTracker(); 4391 } 4392 4393 if (shouldDisplayEdgeEffects()) { 4394 mEdgeGlowTop.onRelease(); 4395 mEdgeGlowBottom.onRelease(); 4396 } 4397 mActivePointerId = INVALID_POINTER; 4398 } 4399 4400 @Override onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY)4401 protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) { 4402 if (mScrollY != scrollY) { 4403 onScrollChanged(mScrollX, scrollY, mScrollX, mScrollY); 4404 mScrollY = scrollY; 4405 invalidateParentIfNeeded(); 4406 4407 awakenScrollBars(); 4408 } 4409 } 4410 4411 @Override onGenericMotionEvent(MotionEvent event)4412 public boolean onGenericMotionEvent(MotionEvent event) { 4413 switch (event.getAction()) { 4414 case MotionEvent.ACTION_SCROLL: 4415 final float axisValue; 4416 if (event.isFromSource(InputDevice.SOURCE_CLASS_POINTER)) { 4417 axisValue = event.getAxisValue(MotionEvent.AXIS_VSCROLL); 4418 } else if (event.isFromSource(InputDevice.SOURCE_ROTARY_ENCODER)) { 4419 axisValue = event.getAxisValue(MotionEvent.AXIS_SCROLL); 4420 } else { 4421 axisValue = 0; 4422 } 4423 4424 final int delta = Math.round(axisValue * mVerticalScrollFactor); 4425 if (delta != 0) { 4426 if (!trackMotionScroll(delta, delta)) { 4427 return true; 4428 } 4429 } 4430 break; 4431 case MotionEvent.ACTION_BUTTON_PRESS: 4432 if (event.isFromSource(InputDevice.SOURCE_CLASS_POINTER)) { 4433 int actionButton = event.getActionButton(); 4434 if ((actionButton == MotionEvent.BUTTON_STYLUS_PRIMARY 4435 || actionButton == MotionEvent.BUTTON_SECONDARY) 4436 && (mTouchMode == TOUCH_MODE_DOWN || mTouchMode == TOUCH_MODE_TAP)) { 4437 if (performStylusButtonPressAction(event)) { 4438 removeCallbacks(mPendingCheckForLongPress); 4439 removeCallbacks(mPendingCheckForTap); 4440 } 4441 } 4442 } 4443 break; 4444 } 4445 4446 return super.onGenericMotionEvent(event); 4447 } 4448 4449 /** 4450 * Initiate a fling with the given velocity. 4451 * 4452 * <p>Applications can use this method to manually initiate a fling as if the user 4453 * initiated it via touch interaction.</p> 4454 * 4455 * @param velocityY Vertical velocity in pixels per second. Note that this is velocity of 4456 * content, not velocity of a touch that initiated the fling. 4457 */ fling(int velocityY)4458 public void fling(int velocityY) { 4459 if (mFlingRunnable == null) { 4460 mFlingRunnable = new FlingRunnable(); 4461 } 4462 reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING); 4463 mFlingRunnable.start(velocityY); 4464 } 4465 4466 @Override onStartNestedScroll(View child, View target, int nestedScrollAxes)4467 public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { 4468 return ((nestedScrollAxes & SCROLL_AXIS_VERTICAL) != 0); 4469 } 4470 4471 @Override onNestedScrollAccepted(View child, View target, int axes)4472 public void onNestedScrollAccepted(View child, View target, int axes) { 4473 super.onNestedScrollAccepted(child, target, axes); 4474 startNestedScroll(SCROLL_AXIS_VERTICAL); 4475 } 4476 4477 @Override onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed)4478 public void onNestedScroll(View target, int dxConsumed, int dyConsumed, 4479 int dxUnconsumed, int dyUnconsumed) { 4480 final int motionIndex = getChildCount() / 2; 4481 final View motionView = getChildAt(motionIndex); 4482 final int oldTop = motionView != null ? motionView.getTop() : 0; 4483 if (motionView == null || trackMotionScroll(-dyUnconsumed, -dyUnconsumed)) { 4484 int myUnconsumed = dyUnconsumed; 4485 int myConsumed = 0; 4486 if (motionView != null) { 4487 myConsumed = motionView.getTop() - oldTop; 4488 myUnconsumed -= myConsumed; 4489 } 4490 dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null); 4491 } 4492 } 4493 4494 @Override onNestedFling(View target, float velocityX, float velocityY, boolean consumed)4495 public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { 4496 final int childCount = getChildCount(); 4497 if (!consumed && childCount > 0 && canScrollList((int) velocityY) && 4498 Math.abs(velocityY) > mMinimumVelocity) { 4499 reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING); 4500 if (mFlingRunnable == null) { 4501 mFlingRunnable = new FlingRunnable(); 4502 } 4503 if (!dispatchNestedPreFling(0, velocityY)) { 4504 mFlingRunnable.start((int) velocityY); 4505 } 4506 return true; 4507 } 4508 return dispatchNestedFling(velocityX, velocityY, consumed); 4509 } 4510 4511 @Override draw(Canvas canvas)4512 public void draw(Canvas canvas) { 4513 super.draw(canvas); 4514 if (shouldDisplayEdgeEffects()) { 4515 final int scrollY = mScrollY; 4516 final boolean clipToPadding = getClipToPadding(); 4517 final int width; 4518 final int height; 4519 final int translateX; 4520 final int translateY; 4521 4522 if (clipToPadding) { 4523 width = getWidth() - mPaddingLeft - mPaddingRight; 4524 height = getHeight() - mPaddingTop - mPaddingBottom; 4525 translateX = mPaddingLeft; 4526 translateY = mPaddingTop; 4527 } else { 4528 width = getWidth(); 4529 height = getHeight(); 4530 translateX = 0; 4531 translateY = 0; 4532 } 4533 mEdgeGlowTop.setSize(width, height); 4534 mEdgeGlowBottom.setSize(width, height); 4535 if (!mEdgeGlowTop.isFinished()) { 4536 final int restoreCount = canvas.save(); 4537 canvas.clipRect(translateX, translateY, 4538 translateX + width ,translateY + mEdgeGlowTop.getMaxHeight()); 4539 final int edgeY = Math.min(0, scrollY + mFirstPositionDistanceGuess) + translateY; 4540 canvas.translate(translateX, edgeY); 4541 if (mEdgeGlowTop.draw(canvas)) { 4542 invalidateEdgeEffects(); 4543 } 4544 canvas.restoreToCount(restoreCount); 4545 } 4546 if (!mEdgeGlowBottom.isFinished()) { 4547 final int restoreCount = canvas.save(); 4548 canvas.clipRect(translateX, translateY + height - mEdgeGlowBottom.getMaxHeight(), 4549 translateX + width, translateY + height); 4550 final int edgeX = -width + translateX; 4551 final int edgeY = Math.max(getHeight(), scrollY + mLastPositionDistanceGuess) 4552 - (clipToPadding ? mPaddingBottom : 0); 4553 canvas.translate(edgeX, edgeY); 4554 canvas.rotate(180, width, 0); 4555 if (mEdgeGlowBottom.draw(canvas)) { 4556 invalidateEdgeEffects(); 4557 } 4558 canvas.restoreToCount(restoreCount); 4559 } 4560 } 4561 } 4562 initOrResetVelocityTracker()4563 private void initOrResetVelocityTracker() { 4564 if (mVelocityTracker == null) { 4565 mVelocityTracker = VelocityTracker.obtain(); 4566 } else { 4567 mVelocityTracker.clear(); 4568 } 4569 } 4570 initVelocityTrackerIfNotExists()4571 private void initVelocityTrackerIfNotExists() { 4572 if (mVelocityTracker == null) { 4573 mVelocityTracker = VelocityTracker.obtain(); 4574 } 4575 } 4576 recycleVelocityTracker()4577 private void recycleVelocityTracker() { 4578 if (mVelocityTracker != null) { 4579 mVelocityTracker.recycle(); 4580 mVelocityTracker = null; 4581 } 4582 } 4583 4584 @Override requestDisallowInterceptTouchEvent(boolean disallowIntercept)4585 public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { 4586 if (disallowIntercept) { 4587 recycleVelocityTracker(); 4588 } 4589 super.requestDisallowInterceptTouchEvent(disallowIntercept); 4590 } 4591 4592 @Override onInterceptHoverEvent(MotionEvent event)4593 public boolean onInterceptHoverEvent(MotionEvent event) { 4594 if (mFastScroll != null && mFastScroll.onInterceptHoverEvent(event)) { 4595 return true; 4596 } 4597 4598 return super.onInterceptHoverEvent(event); 4599 } 4600 4601 @Override onResolvePointerIcon(MotionEvent event, int pointerIndex)4602 public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) { 4603 if (mFastScroll != null) { 4604 PointerIcon pointerIcon = mFastScroll.onResolvePointerIcon(event, pointerIndex); 4605 if (pointerIcon != null) { 4606 return pointerIcon; 4607 } 4608 } 4609 return super.onResolvePointerIcon(event, pointerIndex); 4610 } 4611 4612 @Override onInterceptTouchEvent(MotionEvent ev)4613 public boolean onInterceptTouchEvent(MotionEvent ev) { 4614 final int actionMasked = ev.getActionMasked(); 4615 View v; 4616 4617 if (mPositionScroller != null) { 4618 mPositionScroller.stop(); 4619 } 4620 4621 if (mIsDetaching || !isAttachedToWindow()) { 4622 // Something isn't right. 4623 // Since we rely on being attached to get data set change notifications, 4624 // don't risk doing anything where we might try to resync and find things 4625 // in a bogus state. 4626 return false; 4627 } 4628 4629 if (mFastScroll != null && mFastScroll.onInterceptTouchEvent(ev)) { 4630 return true; 4631 } 4632 4633 switch (actionMasked) { 4634 case MotionEvent.ACTION_DOWN: { 4635 int touchMode = mTouchMode; 4636 if (touchMode == TOUCH_MODE_OVERFLING || touchMode == TOUCH_MODE_OVERSCROLL) { 4637 mMotionCorrection = 0; 4638 return true; 4639 } 4640 4641 final int x = (int) ev.getX(); 4642 final int y = (int) ev.getY(); 4643 mActivePointerId = ev.getPointerId(0); 4644 4645 int motionPosition = findMotionRow(y); 4646 if (doesTouchStopStretch()) { 4647 // Pressed during edge effect, so this is considered the same as a fling catch. 4648 touchMode = mTouchMode = TOUCH_MODE_FLING; 4649 } else if (touchMode != TOUCH_MODE_FLING && motionPosition >= 0) { 4650 // User clicked on an actual view (and was not stopping a fling). 4651 // Remember where the motion event started 4652 v = getChildAt(motionPosition - mFirstPosition); 4653 mMotionViewOriginalTop = v.getTop(); 4654 mMotionX = x; 4655 mMotionY = y; 4656 mMotionPosition = motionPosition; 4657 mTouchMode = TOUCH_MODE_DOWN; 4658 clearScrollingCache(); 4659 } 4660 mLastY = Integer.MIN_VALUE; 4661 initOrResetVelocityTracker(); 4662 mVelocityTracker.addMovement(ev); 4663 mNestedYOffset = 0; 4664 startNestedScroll(SCROLL_AXIS_VERTICAL); 4665 if (touchMode == TOUCH_MODE_FLING) { 4666 return true; 4667 } 4668 break; 4669 } 4670 4671 case MotionEvent.ACTION_MOVE: { 4672 switch (mTouchMode) { 4673 case TOUCH_MODE_DOWN: 4674 int pointerIndex = ev.findPointerIndex(mActivePointerId); 4675 if (pointerIndex == -1) { 4676 pointerIndex = 0; 4677 mActivePointerId = ev.getPointerId(pointerIndex); 4678 } 4679 final int y = (int) ev.getY(pointerIndex); 4680 initVelocityTrackerIfNotExists(); 4681 mVelocityTracker.addMovement(ev); 4682 if (startScrollIfNeeded((int) ev.getX(pointerIndex), y, null)) { 4683 return true; 4684 } 4685 break; 4686 } 4687 break; 4688 } 4689 4690 case MotionEvent.ACTION_CANCEL: 4691 case MotionEvent.ACTION_UP: { 4692 mTouchMode = TOUCH_MODE_REST; 4693 mActivePointerId = INVALID_POINTER; 4694 recycleVelocityTracker(); 4695 reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); 4696 stopNestedScroll(); 4697 break; 4698 } 4699 4700 case MotionEvent.ACTION_POINTER_UP: { 4701 onSecondaryPointerUp(ev); 4702 break; 4703 } 4704 } 4705 4706 return false; 4707 } 4708 onSecondaryPointerUp(MotionEvent ev)4709 private void onSecondaryPointerUp(MotionEvent ev) { 4710 final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> 4711 MotionEvent.ACTION_POINTER_INDEX_SHIFT; 4712 final int pointerId = ev.getPointerId(pointerIndex); 4713 if (pointerId == mActivePointerId) { 4714 // This was our active pointer going up. Choose a new 4715 // active pointer and adjust accordingly. 4716 // TODO: Make this decision more intelligent. 4717 final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 4718 mMotionX = (int) ev.getX(newPointerIndex); 4719 mMotionY = (int) ev.getY(newPointerIndex); 4720 mMotionCorrection = 0; 4721 mActivePointerId = ev.getPointerId(newPointerIndex); 4722 } 4723 } 4724 4725 /** 4726 * {@inheritDoc} 4727 */ 4728 @Override addTouchables(ArrayList<View> views)4729 public void addTouchables(ArrayList<View> views) { 4730 final int count = getChildCount(); 4731 final int firstPosition = mFirstPosition; 4732 final ListAdapter adapter = mAdapter; 4733 4734 if (adapter == null) { 4735 return; 4736 } 4737 4738 for (int i = 0; i < count; i++) { 4739 final View child = getChildAt(i); 4740 if (adapter.isEnabled(firstPosition + i)) { 4741 views.add(child); 4742 } 4743 child.addTouchables(views); 4744 } 4745 } 4746 4747 /** 4748 * Fires an "on scroll state changed" event to the registered 4749 * {@link android.widget.AbsListView.OnScrollListener}, if any. The state change 4750 * is fired only if the specified state is different from the previously known state. 4751 * 4752 * @param newState The new scroll state. 4753 */ 4754 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123769710) reportScrollStateChange(int newState)4755 void reportScrollStateChange(int newState) { 4756 if (newState != mLastScrollState) { 4757 if (mOnScrollListener != null) { 4758 mLastScrollState = newState; 4759 mOnScrollListener.onScrollStateChanged(this, newState); 4760 } 4761 } 4762 4763 // When scrolling, we want to report changes in the active children to Content Capture, 4764 // so set the flag to report on the next update only when scrolling has stopped or a fling 4765 // scroll is performed. 4766 if (newState == OnScrollListener.SCROLL_STATE_IDLE 4767 || newState == OnScrollListener.SCROLL_STATE_FLING) { 4768 mReportChildrenToContentCaptureOnNextUpdate = true; 4769 } 4770 } 4771 4772 /** 4773 * Responsible for fling behavior. Use {@link #start(int)} to 4774 * initiate a fling. Each frame of the fling is handled in {@link #run()}. 4775 * A FlingRunnable will keep re-posting itself until the fling is done. 4776 * 4777 */ 4778 private class FlingRunnable implements Runnable { 4779 /** 4780 * Tracks the decay of a fling scroll 4781 */ 4782 @UnsupportedAppUsage 4783 private final OverScroller mScroller; 4784 4785 /** 4786 * Y value reported by mScroller on the previous fling 4787 */ 4788 private int mLastFlingY; 4789 4790 /** 4791 * If true, {@link #endFling()} will not report scroll state change to 4792 * {@link OnScrollListener#SCROLL_STATE_IDLE}. 4793 */ 4794 private boolean mSuppressIdleStateChangeCall; 4795 4796 private final Runnable mCheckFlywheel = new Runnable() { 4797 @Override 4798 public void run() { 4799 final int activeId = mActivePointerId; 4800 final VelocityTracker vt = mVelocityTracker; 4801 final OverScroller scroller = mScroller; 4802 if (vt == null || activeId == INVALID_POINTER) { 4803 return; 4804 } 4805 4806 vt.computeCurrentVelocity(1000, mMaximumVelocity); 4807 final float yvel = -vt.getYVelocity(activeId); 4808 4809 if (Math.abs(yvel) >= mMinimumVelocity 4810 && scroller.isScrollingInDirection(0, yvel)) { 4811 // Keep the fling alive a little longer 4812 postDelayed(this, FLYWHEEL_TIMEOUT); 4813 } else { 4814 endFling(); 4815 mTouchMode = TOUCH_MODE_SCROLL; 4816 reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); 4817 } 4818 } 4819 }; 4820 4821 private static final int FLYWHEEL_TIMEOUT = 40; // milliseconds 4822 FlingRunnable()4823 FlingRunnable() { 4824 mScroller = new OverScroller(getContext()); 4825 } 4826 4827 // Use AbsListView#fling(int) instead 4828 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) start(int initialVelocity)4829 void start(int initialVelocity) { 4830 int initialY = initialVelocity < 0 ? Integer.MAX_VALUE : 0; 4831 mLastFlingY = initialY; 4832 mScroller.setInterpolator(null); 4833 mScroller.fling(0, initialY, 0, initialVelocity, 4834 0, Integer.MAX_VALUE, 0, Integer.MAX_VALUE); 4835 mTouchMode = TOUCH_MODE_FLING; 4836 mSuppressIdleStateChangeCall = false; 4837 removeCallbacks(this); 4838 postOnAnimation(this); 4839 4840 if (PROFILE_FLINGING) { 4841 if (!mFlingProfilingStarted) { 4842 Debug.startMethodTracing("AbsListViewFling"); 4843 mFlingProfilingStarted = true; 4844 } 4845 } 4846 4847 if (mFlingStrictSpan == null) { 4848 mFlingStrictSpan = StrictMode.enterCriticalSpan("AbsListView-fling"); 4849 } 4850 } 4851 4852 void startSpringback() { 4853 mSuppressIdleStateChangeCall = false; 4854 if (mScroller.springBack(0, mScrollY, 0, 0, 0, 0)) { 4855 mTouchMode = TOUCH_MODE_OVERFLING; 4856 invalidate(); 4857 postOnAnimation(this); 4858 } else { 4859 mTouchMode = TOUCH_MODE_REST; 4860 reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); 4861 } 4862 } 4863 4864 void startOverfling(int initialVelocity) { 4865 mScroller.setInterpolator(null); 4866 mScroller.fling(0, mScrollY, 0, initialVelocity, 0, 0, 4867 Integer.MIN_VALUE, Integer.MAX_VALUE, 0, getHeight()); 4868 mTouchMode = TOUCH_MODE_OVERFLING; 4869 mSuppressIdleStateChangeCall = false; 4870 invalidate(); 4871 postOnAnimation(this); 4872 } 4873 4874 void edgeReached(int delta) { 4875 mScroller.notifyVerticalEdgeReached(mScrollY, 0, mOverflingDistance); 4876 final int overscrollMode = getOverScrollMode(); 4877 if (overscrollMode == OVER_SCROLL_ALWAYS || 4878 (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && !contentFits())) { 4879 mTouchMode = TOUCH_MODE_OVERFLING; 4880 final int vel = (int) mScroller.getCurrVelocity(); 4881 if (delta > 0) { 4882 mEdgeGlowTop.onAbsorb(vel); 4883 } else { 4884 mEdgeGlowBottom.onAbsorb(vel); 4885 } 4886 } else { 4887 mTouchMode = TOUCH_MODE_REST; 4888 if (mPositionScroller != null) { 4889 mPositionScroller.stop(); 4890 } 4891 } 4892 invalidate(); 4893 postOnAnimation(this); 4894 } 4895 startScroll(int distance, int duration, boolean linear, boolean suppressEndFlingStateChangeCall)4896 void startScroll(int distance, int duration, boolean linear, 4897 boolean suppressEndFlingStateChangeCall) { 4898 int initialY = distance < 0 ? Integer.MAX_VALUE : 0; 4899 mLastFlingY = initialY; 4900 mScroller.setInterpolator(linear ? sLinearInterpolator : null); 4901 mScroller.startScroll(0, initialY, 0, distance, duration); 4902 mTouchMode = TOUCH_MODE_FLING; 4903 mSuppressIdleStateChangeCall = suppressEndFlingStateChangeCall; 4904 postOnAnimation(this); 4905 } 4906 4907 // To interrupt a fling early you should use smoothScrollBy(0,0) instead 4908 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) 4909 void endFling() { 4910 mTouchMode = TOUCH_MODE_REST; 4911 4912 removeCallbacks(this); 4913 removeCallbacks(mCheckFlywheel); 4914 4915 if (!mSuppressIdleStateChangeCall) { 4916 reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); 4917 } 4918 clearScrollingCache(); 4919 mScroller.abortAnimation(); 4920 4921 if (mFlingStrictSpan != null) { 4922 mFlingStrictSpan.finish(); 4923 mFlingStrictSpan = null; 4924 } 4925 } 4926 4927 void flywheelTouch() { 4928 postDelayed(mCheckFlywheel, FLYWHEEL_TIMEOUT); 4929 } 4930 4931 @Override 4932 public void run() { 4933 switch (mTouchMode) { 4934 default: 4935 endFling(); 4936 return; 4937 4938 case TOUCH_MODE_SCROLL: 4939 if (mScroller.isFinished()) { 4940 return; 4941 } 4942 // Fall through 4943 case TOUCH_MODE_FLING: { 4944 if (mDataChanged) { 4945 layoutChildren(); 4946 } 4947 4948 if (mItemCount == 0 || getChildCount() == 0) { 4949 endFling(); 4950 return; 4951 } 4952 4953 final OverScroller scroller = mScroller; 4954 boolean more = scroller.computeScrollOffset(); 4955 final int y = scroller.getCurrY(); 4956 4957 // Flip sign to convert finger direction to list items direction 4958 // (e.g. finger moving down means list is moving towards the top) 4959 int delta = mLastFlingY - y; 4960 4961 // Pretend that each frame of a fling scroll is a touch scroll 4962 if (delta > 0) { 4963 // List is moving towards the top. Use first view as mMotionPosition 4964 mMotionPosition = mFirstPosition; 4965 final View firstView = getChildAt(0); 4966 mMotionViewOriginalTop = firstView.getTop(); 4967 4968 // Don't fling more than 1 screen 4969 delta = Math.min(getHeight() - mPaddingBottom - mPaddingTop - 1, delta); 4970 } else { 4971 // List is moving towards the bottom. Use last view as mMotionPosition 4972 int offsetToLast = getChildCount() - 1; 4973 mMotionPosition = mFirstPosition + offsetToLast; 4974 4975 final View lastView = getChildAt(offsetToLast); 4976 mMotionViewOriginalTop = lastView.getTop(); 4977 4978 // Don't fling more than 1 screen 4979 delta = Math.max(-(getHeight() - mPaddingBottom - mPaddingTop - 1), delta); 4980 } 4981 4982 // Check to see if we have bumped into the scroll limit 4983 View motionView = getChildAt(mMotionPosition - mFirstPosition); 4984 int oldTop = 0; 4985 if (motionView != null) { 4986 oldTop = motionView.getTop(); 4987 } 4988 4989 // Don't stop just because delta is zero (it could have been rounded) 4990 final boolean atEdge = trackMotionScroll(delta, delta); 4991 final boolean atEnd = atEdge && (delta != 0); 4992 if (atEnd) { 4993 if (motionView != null) { 4994 // Tweak the scroll for how far we overshot 4995 int overshoot = -(delta - (motionView.getTop() - oldTop)); 4996 overScrollBy(0, overshoot, 0, mScrollY, 0, 0, 4997 0, mOverflingDistance, false); 4998 } 4999 if (more) { 5000 edgeReached(delta); 5001 } 5002 break; 5003 } 5004 5005 if (more && !atEnd) { 5006 if (atEdge) invalidate(); 5007 mLastFlingY = y; 5008 postOnAnimation(this); 5009 } else { 5010 endFling(); 5011 5012 if (PROFILE_FLINGING) { 5013 if (mFlingProfilingStarted) { 5014 Debug.stopMethodTracing(); 5015 mFlingProfilingStarted = false; 5016 } 5017 5018 if (mFlingStrictSpan != null) { 5019 mFlingStrictSpan.finish(); 5020 mFlingStrictSpan = null; 5021 } 5022 } 5023 } 5024 break; 5025 } 5026 5027 case TOUCH_MODE_OVERFLING: { 5028 final OverScroller scroller = mScroller; 5029 if (scroller.computeScrollOffset()) { 5030 final int scrollY = mScrollY; 5031 final int currY = scroller.getCurrY(); 5032 final int deltaY = currY - scrollY; 5033 if (overScrollBy(0, deltaY, 0, scrollY, 0, 0, 5034 0, mOverflingDistance, false)) { 5035 final boolean crossDown = scrollY <= 0 && currY > 0; 5036 final boolean crossUp = scrollY >= 0 && currY < 0; 5037 if (crossDown || crossUp) { 5038 int velocity = (int) scroller.getCurrVelocity(); 5039 if (crossUp) velocity = -velocity; 5040 5041 // Don't flywheel from this; we're just continuing things. 5042 scroller.abortAnimation(); 5043 start(velocity); 5044 } else { 5045 startSpringback(); 5046 } 5047 } else { 5048 invalidate(); 5049 postOnAnimation(this); 5050 } 5051 } else { 5052 endFling(); 5053 } 5054 break; 5055 } 5056 } 5057 } 5058 } 5059 5060 /** 5061 * The amount of friction applied to flings. The default value 5062 * is {@link ViewConfiguration#getScrollFriction}. 5063 */ 5064 public void setFriction(float friction) { 5065 if (mFlingRunnable == null) { 5066 mFlingRunnable = new FlingRunnable(); 5067 } 5068 mFlingRunnable.mScroller.setFriction(friction); 5069 } 5070 5071 /** 5072 * Sets a scale factor for the fling velocity. The initial scale 5073 * factor is 1.0. 5074 * 5075 * @param scale The scale factor to multiply the velocity by. 5076 */ 5077 public void setVelocityScale(float scale) { 5078 mVelocityScale = scale; 5079 } 5080 5081 /** 5082 * Override this for better control over position scrolling. 5083 */ 5084 AbsPositionScroller createPositionScroller() { 5085 return new PositionScroller(); 5086 } 5087 5088 /** 5089 * Smoothly scroll to the specified adapter position. The view will 5090 * scroll such that the indicated position is displayed. 5091 * @param position Scroll to this adapter position. 5092 */ 5093 public void smoothScrollToPosition(int position) { 5094 if (mPositionScroller == null) { 5095 mPositionScroller = createPositionScroller(); 5096 } 5097 mPositionScroller.start(position); 5098 } 5099 5100 /** 5101 * Smoothly scroll to the specified adapter position. The view will scroll 5102 * such that the indicated position is displayed <code>offset</code> pixels below 5103 * the top edge of the view. If this is impossible, (e.g. the offset would scroll 5104 * the first or last item beyond the boundaries of the list) it will get as close 5105 * as possible. The scroll will take <code>duration</code> milliseconds to complete. 5106 * 5107 * @param position Position to scroll to 5108 * @param offset Desired distance in pixels of <code>position</code> from the top 5109 * of the view when scrolling is finished 5110 * @param duration Number of milliseconds to use for the scroll 5111 */ 5112 public void smoothScrollToPositionFromTop(int position, int offset, int duration) { 5113 if (mPositionScroller == null) { 5114 mPositionScroller = createPositionScroller(); 5115 } 5116 mPositionScroller.startWithOffset(position, offset, duration); 5117 } 5118 5119 /** 5120 * Smoothly scroll to the specified adapter position. The view will scroll 5121 * such that the indicated position is displayed <code>offset</code> pixels below 5122 * the top edge of the view. If this is impossible, (e.g. the offset would scroll 5123 * the first or last item beyond the boundaries of the list) it will get as close 5124 * as possible. 5125 * 5126 * @param position Position to scroll to 5127 * @param offset Desired distance in pixels of <code>position</code> from the top 5128 * of the view when scrolling is finished 5129 */ 5130 public void smoothScrollToPositionFromTop(int position, int offset) { 5131 if (mPositionScroller == null) { 5132 mPositionScroller = createPositionScroller(); 5133 } 5134 mPositionScroller.startWithOffset(position, offset); 5135 } 5136 5137 /** 5138 * Smoothly scroll to the specified adapter position. The view will 5139 * scroll such that the indicated position is displayed, but it will 5140 * stop early if scrolling further would scroll boundPosition out of 5141 * view. 5142 * 5143 * @param position Scroll to this adapter position. 5144 * @param boundPosition Do not scroll if it would move this adapter 5145 * position out of view. 5146 */ 5147 public void smoothScrollToPosition(int position, int boundPosition) { 5148 if (mPositionScroller == null) { 5149 mPositionScroller = createPositionScroller(); 5150 } 5151 mPositionScroller.start(position, boundPosition); 5152 } 5153 5154 /** 5155 * Smoothly scroll by distance pixels over duration milliseconds. 5156 * @param distance Distance to scroll in pixels. 5157 * @param duration Duration of the scroll animation in milliseconds. 5158 */ 5159 public void smoothScrollBy(int distance, int duration) { 5160 smoothScrollBy(distance, duration, false, false); 5161 } 5162 5163 @UnsupportedAppUsage 5164 void smoothScrollBy(int distance, int duration, boolean linear, 5165 boolean suppressEndFlingStateChangeCall) { 5166 if (mFlingRunnable == null) { 5167 mFlingRunnable = new FlingRunnable(); 5168 } 5169 5170 // No sense starting to scroll if we're not going anywhere 5171 final int firstPos = mFirstPosition; 5172 final int childCount = getChildCount(); 5173 final int lastPos = firstPos + childCount; 5174 final int topLimit = getPaddingTop(); 5175 final int bottomLimit = getHeight() - getPaddingBottom(); 5176 5177 if (distance == 0 || mItemCount == 0 || childCount == 0 || 5178 (firstPos == 0 && getChildAt(0).getTop() == topLimit && distance < 0) || 5179 (lastPos == mItemCount && 5180 getChildAt(childCount - 1).getBottom() == bottomLimit && distance > 0)) { 5181 mFlingRunnable.endFling(); 5182 if (mPositionScroller != null) { 5183 mPositionScroller.stop(); 5184 } 5185 } else { 5186 reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING); 5187 mFlingRunnable.startScroll(distance, duration, linear, suppressEndFlingStateChangeCall); 5188 } 5189 } 5190 5191 /** 5192 * Allows RemoteViews to scroll relatively to a position. 5193 */ 5194 void smoothScrollByOffset(int position) { 5195 int index = -1; 5196 if (position < 0) { 5197 index = getFirstVisiblePosition(); 5198 } else if (position > 0) { 5199 index = getLastVisiblePosition(); 5200 } 5201 5202 if (index > -1) { 5203 View child = getChildAt(index - getFirstVisiblePosition()); 5204 if (child != null) { 5205 Rect visibleRect = new Rect(); 5206 if (child.getGlobalVisibleRect(visibleRect)) { 5207 // the child is partially visible 5208 int childRectArea = child.getWidth() * child.getHeight(); 5209 int visibleRectArea = visibleRect.width() * visibleRect.height(); 5210 float visibleArea = (visibleRectArea / (float) childRectArea); 5211 final float visibleThreshold = 0.75f; 5212 if ((position < 0) && (visibleArea < visibleThreshold)) { 5213 // the top index is not perceivably visible so offset 5214 // to account for showing that top index as well 5215 ++index; 5216 } else if ((position > 0) && (visibleArea < visibleThreshold)) { 5217 // the bottom index is not perceivably visible so offset 5218 // to account for showing that bottom index as well 5219 --index; 5220 } 5221 } 5222 smoothScrollToPosition(Math.max(0, Math.min(getCount(), index + position))); 5223 } 5224 } 5225 } 5226 5227 private void createScrollingCache() { 5228 if (mScrollingCacheEnabled && !mCachingStarted && !isHardwareAccelerated()) { 5229 setChildrenDrawnWithCacheEnabled(true); 5230 setChildrenDrawingCacheEnabled(true); 5231 mCachingStarted = mCachingActive = true; 5232 } 5233 } 5234 5235 private void clearScrollingCache() { 5236 if (!isHardwareAccelerated()) { 5237 if (mClearScrollingCache == null) { 5238 mClearScrollingCache = new Runnable() { 5239 @Override 5240 public void run() { 5241 if (mCachingStarted) { 5242 mCachingStarted = mCachingActive = false; 5243 setChildrenDrawnWithCacheEnabled(false); 5244 if ((mPersistentDrawingCache & PERSISTENT_SCROLLING_CACHE) == 0) { 5245 setChildrenDrawingCacheEnabled(false); 5246 } 5247 if (!isAlwaysDrawnWithCacheEnabled()) { 5248 invalidate(); 5249 } 5250 } 5251 } 5252 }; 5253 } 5254 post(mClearScrollingCache); 5255 } 5256 } 5257 5258 /** 5259 * Scrolls the list items within the view by a specified number of pixels. 5260 * 5261 * <p>The actual amount of scroll is capped by the list content viewport height 5262 * which is the list height minus top and bottom paddings minus one pixel.</p> 5263 * 5264 * @param y the amount of pixels to scroll by vertically 5265 * @see #canScrollList(int) 5266 */ 5267 public void scrollListBy(int y) { 5268 trackMotionScroll(-y, -y); 5269 } 5270 5271 /** 5272 * Check if the items in the list can be scrolled in a certain direction. 5273 * 5274 * @param direction Negative to check scrolling up, positive to check 5275 * scrolling down. 5276 * @return true if the list can be scrolled in the specified direction, 5277 * false otherwise. 5278 * @see #scrollListBy(int) 5279 */ 5280 public boolean canScrollList(int direction) { 5281 final int childCount = getChildCount(); 5282 if (childCount == 0) { 5283 return false; 5284 } 5285 5286 final int firstPosition = mFirstPosition; 5287 final Rect listPadding = mListPadding; 5288 if (direction > 0) { 5289 final int lastBottom = getChildAt(childCount - 1).getBottom(); 5290 final int lastPosition = firstPosition + childCount; 5291 return lastPosition < mItemCount || lastBottom > getHeight() - listPadding.bottom; 5292 } else { 5293 final int firstTop = getChildAt(0).getTop(); 5294 return firstPosition > 0 || firstTop < listPadding.top; 5295 } 5296 } 5297 5298 /** 5299 * Track a motion scroll 5300 * 5301 * @param deltaY Amount to offset mMotionView. This is the accumulated delta since the motion 5302 * began. Positive numbers mean the user's finger is moving down the screen. 5303 * @param incrementalDeltaY Change in deltaY from the previous event. 5304 * @return true if we're already at the beginning/end of the list and have nothing to do. 5305 */ 5306 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 124051739) 5307 boolean trackMotionScroll(int deltaY, int incrementalDeltaY) { 5308 final int childCount = getChildCount(); 5309 if (childCount == 0) { 5310 return true; 5311 } 5312 5313 final int firstTop = getChildAt(0).getTop(); 5314 final int lastBottom = getChildAt(childCount - 1).getBottom(); 5315 5316 final Rect listPadding = mListPadding; 5317 5318 // "effective padding" In this case is the amount of padding that affects 5319 // how much space should not be filled by items. If we don't clip to padding 5320 // there is no effective padding. 5321 int effectivePaddingTop = 0; 5322 int effectivePaddingBottom = 0; 5323 if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { 5324 effectivePaddingTop = listPadding.top; 5325 effectivePaddingBottom = listPadding.bottom; 5326 } 5327 5328 // FIXME account for grid vertical spacing too? 5329 final int spaceAbove = effectivePaddingTop - firstTop; 5330 final int end = getHeight() - effectivePaddingBottom; 5331 final int spaceBelow = lastBottom - end; 5332 5333 final int height = getHeight() - mPaddingBottom - mPaddingTop; 5334 if (deltaY < 0) { 5335 deltaY = Math.max(-(height - 1), deltaY); 5336 } else { 5337 deltaY = Math.min(height - 1, deltaY); 5338 } 5339 5340 if (incrementalDeltaY < 0) { 5341 incrementalDeltaY = Math.max(-(height - 1), incrementalDeltaY); 5342 } else { 5343 incrementalDeltaY = Math.min(height - 1, incrementalDeltaY); 5344 } 5345 5346 final int firstPosition = mFirstPosition; 5347 5348 // Update our guesses for where the first and last views are 5349 if (firstPosition == 0) { 5350 mFirstPositionDistanceGuess = firstTop - listPadding.top; 5351 } else { 5352 mFirstPositionDistanceGuess += incrementalDeltaY; 5353 } 5354 if (firstPosition + childCount == mItemCount) { 5355 mLastPositionDistanceGuess = lastBottom + listPadding.bottom; 5356 } else { 5357 mLastPositionDistanceGuess += incrementalDeltaY; 5358 } 5359 5360 final boolean cannotScrollDown = (firstPosition == 0 && 5361 firstTop >= listPadding.top && incrementalDeltaY >= 0); 5362 final boolean cannotScrollUp = (firstPosition + childCount == mItemCount && 5363 lastBottom <= getHeight() - listPadding.bottom && incrementalDeltaY <= 0); 5364 5365 if (cannotScrollDown || cannotScrollUp) { 5366 return incrementalDeltaY != 0; 5367 } 5368 5369 final boolean down = incrementalDeltaY < 0; 5370 5371 final boolean inTouchMode = isInTouchMode(); 5372 if (inTouchMode) { 5373 hideSelector(); 5374 } 5375 5376 final int headerViewsCount = getHeaderViewsCount(); 5377 final int footerViewsStart = mItemCount - getFooterViewsCount(); 5378 5379 int start = 0; 5380 int count = 0; 5381 5382 if (down) { 5383 int top = -incrementalDeltaY; 5384 if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { 5385 top += listPadding.top; 5386 } 5387 for (int i = 0; i < childCount; i++) { 5388 final View child = getChildAt(i); 5389 if (child.getBottom() >= top) { 5390 break; 5391 } else { 5392 count++; 5393 int position = firstPosition + i; 5394 if (position >= headerViewsCount && position < footerViewsStart) { 5395 // The view will be rebound to new data, clear any 5396 // system-managed transient state. 5397 child.clearAccessibilityFocus(); 5398 mRecycler.addScrapView(child, position); 5399 } 5400 } 5401 } 5402 } else { 5403 int bottom = getHeight() - incrementalDeltaY; 5404 if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) { 5405 bottom -= listPadding.bottom; 5406 } 5407 for (int i = childCount - 1; i >= 0; i--) { 5408 final View child = getChildAt(i); 5409 if (child.getTop() <= bottom) { 5410 break; 5411 } else { 5412 start = i; 5413 count++; 5414 int position = firstPosition + i; 5415 if (position >= headerViewsCount && position < footerViewsStart) { 5416 // The view will be rebound to new data, clear any 5417 // system-managed transient state. 5418 child.clearAccessibilityFocus(); 5419 mRecycler.addScrapView(child, position); 5420 } 5421 } 5422 } 5423 } 5424 5425 mMotionViewNewTop = mMotionViewOriginalTop + deltaY; 5426 5427 mBlockLayoutRequests = true; 5428 5429 if (count > 0) { 5430 detachViewsFromParent(start, count); 5431 mRecycler.removeSkippedScrap(); 5432 } 5433 5434 // invalidate before moving the children to avoid unnecessary invalidate 5435 // calls to bubble up from the children all the way to the top 5436 if (!awakenScrollBars()) { 5437 invalidate(); 5438 } 5439 5440 offsetChildrenTopAndBottom(incrementalDeltaY); 5441 5442 if (down) { 5443 mFirstPosition += count; 5444 } 5445 5446 final int absIncrementalDeltaY = Math.abs(incrementalDeltaY); 5447 if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) { 5448 fillGap(down); 5449 } 5450 5451 mRecycler.fullyDetachScrapViews(); 5452 boolean selectorOnScreen = false; 5453 if (!inTouchMode && mSelectedPosition != INVALID_POSITION) { 5454 final int childIndex = mSelectedPosition - mFirstPosition; 5455 if (childIndex >= 0 && childIndex < getChildCount()) { 5456 positionSelector(mSelectedPosition, getChildAt(childIndex)); 5457 selectorOnScreen = true; 5458 } 5459 } else if (mSelectorPosition != INVALID_POSITION) { 5460 final int childIndex = mSelectorPosition - mFirstPosition; 5461 if (childIndex >= 0 && childIndex < getChildCount()) { 5462 positionSelector(mSelectorPosition, getChildAt(childIndex)); 5463 selectorOnScreen = true; 5464 } 5465 } 5466 if (!selectorOnScreen) { 5467 mSelectorRect.setEmpty(); 5468 } 5469 5470 mBlockLayoutRequests = false; 5471 5472 invokeOnItemScrollListener(); 5473 5474 return false; 5475 } 5476 5477 /** 5478 * Returns the number of header views in the list. Header views are special views 5479 * at the top of the list that should not be recycled during a layout. 5480 * 5481 * @return The number of header views, 0 in the default implementation. 5482 */ 5483 int getHeaderViewsCount() { 5484 return 0; 5485 } 5486 5487 /** 5488 * Returns the number of footer views in the list. Footer views are special views 5489 * at the bottom of the list that should not be recycled during a layout. 5490 * 5491 * @return The number of footer views, 0 in the default implementation. 5492 */ 5493 int getFooterViewsCount() { 5494 return 0; 5495 } 5496 5497 /** 5498 * Fills the gap left open by a touch-scroll. During a touch scroll, children that 5499 * remain on screen are shifted and the other ones are discarded. The role of this 5500 * method is to fill the gap thus created by performing a partial layout in the 5501 * empty space. 5502 * 5503 * @param down true if the scroll is going down, false if it is going up 5504 */ 5505 @SuppressWarnings("HiddenAbstractMethod") 5506 abstract void fillGap(boolean down); 5507 hideSelector()5508 void hideSelector() { 5509 if (mSelectedPosition != INVALID_POSITION) { 5510 if (mLayoutMode != LAYOUT_SPECIFIC) { 5511 mResurrectToPosition = mSelectedPosition; 5512 } 5513 if (mNextSelectedPosition >= 0 && mNextSelectedPosition != mSelectedPosition) { 5514 mResurrectToPosition = mNextSelectedPosition; 5515 } 5516 setSelectedPositionInt(INVALID_POSITION); 5517 setNextSelectedPositionInt(INVALID_POSITION); 5518 mSelectedTop = 0; 5519 } 5520 } 5521 5522 /** 5523 * @return A position to select. First we try mSelectedPosition. If that has been clobbered by 5524 * entering touch mode, we then try mResurrectToPosition. Values are pinned to the range 5525 * of items available in the adapter 5526 */ reconcileSelectedPosition()5527 int reconcileSelectedPosition() { 5528 int position = mSelectedPosition; 5529 if (position < 0) { 5530 position = mResurrectToPosition; 5531 } 5532 position = Math.max(0, position); 5533 position = Math.min(position, mItemCount - 1); 5534 return position; 5535 } 5536 5537 /** 5538 * Find the row closest to y. This row will be used as the motion row when scrolling 5539 * 5540 * @param y Where the user touched 5541 * @return The position of the first (or only) item in the row containing y 5542 */ 5543 @SuppressWarnings("HiddenAbstractMethod") 5544 @UnsupportedAppUsage 5545 abstract int findMotionRow(int y); 5546 5547 /** 5548 * Find the row closest to y. This row will be used as the motion row when scrolling. 5549 * 5550 * @param y Where the user touched 5551 * @return The position of the first (or only) item in the row closest to y 5552 */ findClosestMotionRow(int y)5553 int findClosestMotionRow(int y) { 5554 final int childCount = getChildCount(); 5555 if (childCount == 0) { 5556 return INVALID_POSITION; 5557 } 5558 5559 final int motionRow = findMotionRow(y); 5560 return motionRow != INVALID_POSITION ? motionRow : mFirstPosition + childCount - 1; 5561 } 5562 5563 /** 5564 * Causes all the views to be rebuilt and redrawn. 5565 */ invalidateViews()5566 public void invalidateViews() { 5567 mDataChanged = true; 5568 rememberSyncState(); 5569 requestLayout(); 5570 invalidate(); 5571 } 5572 5573 /** 5574 * If there is a selection returns false. 5575 * Otherwise resurrects the selection and returns true if resurrected. 5576 */ 5577 @UnsupportedAppUsage resurrectSelectionIfNeeded()5578 boolean resurrectSelectionIfNeeded() { 5579 if (mSelectedPosition < 0 && resurrectSelection()) { 5580 updateSelectorState(); 5581 return true; 5582 } 5583 return false; 5584 } 5585 5586 /** 5587 * Makes the item at the supplied position selected. 5588 * 5589 * @param position the position of the new selection 5590 */ 5591 @SuppressWarnings("HiddenAbstractMethod") 5592 abstract void setSelectionInt(int position); 5593 5594 /** 5595 * Attempt to bring the selection back if the user is switching from touch 5596 * to trackball mode 5597 * @return Whether selection was set to something. 5598 */ resurrectSelection()5599 boolean resurrectSelection() { 5600 final int childCount = getChildCount(); 5601 5602 if (childCount <= 0) { 5603 return false; 5604 } 5605 5606 int selectedTop = 0; 5607 int selectedPos; 5608 int childrenTop = mListPadding.top; 5609 int childrenBottom = mBottom - mTop - mListPadding.bottom; 5610 final int firstPosition = mFirstPosition; 5611 final int toPosition = mResurrectToPosition; 5612 boolean down = true; 5613 5614 if (toPosition >= firstPosition && toPosition < firstPosition + childCount) { 5615 selectedPos = toPosition; 5616 5617 final View selected = getChildAt(selectedPos - mFirstPosition); 5618 selectedTop = selected.getTop(); 5619 int selectedBottom = selected.getBottom(); 5620 5621 // We are scrolled, don't get in the fade 5622 if (selectedTop < childrenTop) { 5623 selectedTop = childrenTop + getVerticalFadingEdgeLength(); 5624 } else if (selectedBottom > childrenBottom) { 5625 selectedTop = childrenBottom - selected.getMeasuredHeight() 5626 - getVerticalFadingEdgeLength(); 5627 } 5628 } else { 5629 if (toPosition < firstPosition) { 5630 // Default to selecting whatever is first 5631 selectedPos = firstPosition; 5632 for (int i = 0; i < childCount; i++) { 5633 final View v = getChildAt(i); 5634 final int top = v.getTop(); 5635 5636 if (i == 0) { 5637 // Remember the position of the first item 5638 selectedTop = top; 5639 // See if we are scrolled at all 5640 if (firstPosition > 0 || top < childrenTop) { 5641 // If we are scrolled, don't select anything that is 5642 // in the fade region 5643 childrenTop += getVerticalFadingEdgeLength(); 5644 } 5645 } 5646 if (top >= childrenTop) { 5647 // Found a view whose top is fully visisble 5648 selectedPos = firstPosition + i; 5649 selectedTop = top; 5650 break; 5651 } 5652 } 5653 } else { 5654 final int itemCount = mItemCount; 5655 down = false; 5656 selectedPos = firstPosition + childCount - 1; 5657 5658 for (int i = childCount - 1; i >= 0; i--) { 5659 final View v = getChildAt(i); 5660 final int top = v.getTop(); 5661 final int bottom = v.getBottom(); 5662 5663 if (i == childCount - 1) { 5664 selectedTop = top; 5665 if (firstPosition + childCount < itemCount || bottom > childrenBottom) { 5666 childrenBottom -= getVerticalFadingEdgeLength(); 5667 } 5668 } 5669 5670 if (bottom <= childrenBottom) { 5671 selectedPos = firstPosition + i; 5672 selectedTop = top; 5673 break; 5674 } 5675 } 5676 } 5677 } 5678 5679 mResurrectToPosition = INVALID_POSITION; 5680 removeCallbacks(mFlingRunnable); 5681 if (mPositionScroller != null) { 5682 mPositionScroller.stop(); 5683 } 5684 mTouchMode = TOUCH_MODE_REST; 5685 clearScrollingCache(); 5686 mSpecificTop = selectedTop; 5687 selectedPos = lookForSelectablePosition(selectedPos, down); 5688 if (selectedPos >= firstPosition && selectedPos <= getLastVisiblePosition()) { 5689 mLayoutMode = LAYOUT_SPECIFIC; 5690 updateSelectorState(); 5691 setSelectionInt(selectedPos); 5692 invokeOnItemScrollListener(); 5693 } else { 5694 selectedPos = INVALID_POSITION; 5695 } 5696 reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); 5697 5698 return selectedPos >= 0; 5699 } 5700 confirmCheckedPositionsById()5701 void confirmCheckedPositionsById() { 5702 // Clear out the positional check states, we'll rebuild it below from IDs. 5703 mCheckStates.clear(); 5704 5705 boolean checkedCountChanged = false; 5706 for (int checkedIndex = 0; checkedIndex < mCheckedIdStates.size(); checkedIndex++) { 5707 final long id = mCheckedIdStates.keyAt(checkedIndex); 5708 final int lastPos = mCheckedIdStates.valueAt(checkedIndex); 5709 5710 final long lastPosId = mAdapter.getItemId(lastPos); 5711 if (id != lastPosId) { 5712 // Look around to see if the ID is nearby. If not, uncheck it. 5713 final int start = Math.max(0, lastPos - CHECK_POSITION_SEARCH_DISTANCE); 5714 final int end = Math.min(lastPos + CHECK_POSITION_SEARCH_DISTANCE, mItemCount); 5715 boolean found = false; 5716 for (int searchPos = start; searchPos < end; searchPos++) { 5717 final long searchId = mAdapter.getItemId(searchPos); 5718 if (id == searchId) { 5719 found = true; 5720 mCheckStates.put(searchPos, true); 5721 mCheckedIdStates.setValueAt(checkedIndex, searchPos); 5722 break; 5723 } 5724 } 5725 5726 if (!found) { 5727 mCheckedIdStates.delete(id); 5728 checkedIndex--; 5729 mCheckedItemCount--; 5730 checkedCountChanged = true; 5731 if (mChoiceActionMode != null && mMultiChoiceModeCallback != null) { 5732 mMultiChoiceModeCallback.onItemCheckedStateChanged(mChoiceActionMode, 5733 lastPos, id, false); 5734 } 5735 } 5736 } else { 5737 mCheckStates.put(lastPos, true); 5738 } 5739 } 5740 5741 if (checkedCountChanged && mChoiceActionMode != null) { 5742 mChoiceActionMode.invalidate(); 5743 } 5744 } 5745 5746 @Override handleDataChanged()5747 protected void handleDataChanged() { 5748 int count = mItemCount; 5749 int lastHandledItemCount = mLastHandledItemCount; 5750 mLastHandledItemCount = mItemCount; 5751 5752 if (mChoiceMode != CHOICE_MODE_NONE && mAdapter != null && mAdapter.hasStableIds()) { 5753 confirmCheckedPositionsById(); 5754 } 5755 5756 // TODO: In the future we can recycle these views based on stable ID instead. 5757 mRecycler.clearTransientStateViews(); 5758 5759 if (count > 0) { 5760 int newPos; 5761 int selectablePos; 5762 5763 // Find the row we are supposed to sync to 5764 if (mNeedSync) { 5765 // Update this first, since setNextSelectedPositionInt inspects it 5766 mNeedSync = false; 5767 mPendingSync = null; 5768 5769 if (mTranscriptMode == TRANSCRIPT_MODE_ALWAYS_SCROLL) { 5770 mLayoutMode = LAYOUT_FORCE_BOTTOM; 5771 return; 5772 } else if (mTranscriptMode == TRANSCRIPT_MODE_NORMAL) { 5773 if (mForceTranscriptScroll) { 5774 mForceTranscriptScroll = false; 5775 mLayoutMode = LAYOUT_FORCE_BOTTOM; 5776 return; 5777 } 5778 final int childCount = getChildCount(); 5779 final int listBottom = getHeight() - getPaddingBottom(); 5780 final View lastChild = getChildAt(childCount - 1); 5781 final int lastBottom = lastChild != null ? lastChild.getBottom() : listBottom; 5782 if (mFirstPosition + childCount >= lastHandledItemCount && 5783 lastBottom <= listBottom) { 5784 mLayoutMode = LAYOUT_FORCE_BOTTOM; 5785 return; 5786 } 5787 // Something new came in and we didn't scroll; give the user a clue that 5788 // there's something new. 5789 awakenScrollBars(); 5790 } 5791 5792 switch (mSyncMode) { 5793 case SYNC_SELECTED_POSITION: 5794 if (isInTouchMode()) { 5795 // We saved our state when not in touch mode. (We know this because 5796 // mSyncMode is SYNC_SELECTED_POSITION.) Now we are trying to 5797 // restore in touch mode. Just leave mSyncPosition as it is (possibly 5798 // adjusting if the available range changed) and return. 5799 mLayoutMode = LAYOUT_SYNC; 5800 mSyncPosition = Math.min(Math.max(0, mSyncPosition), count - 1); 5801 5802 return; 5803 } else { 5804 // See if we can find a position in the new data with the same 5805 // id as the old selection. This will change mSyncPosition. 5806 newPos = findSyncPosition(); 5807 if (newPos >= 0) { 5808 // Found it. Now verify that new selection is still selectable 5809 selectablePos = lookForSelectablePosition(newPos, true); 5810 if (selectablePos == newPos) { 5811 // Same row id is selected 5812 mSyncPosition = newPos; 5813 5814 if (mSyncHeight == getHeight()) { 5815 // If we are at the same height as when we saved state, try 5816 // to restore the scroll position too. 5817 mLayoutMode = LAYOUT_SYNC; 5818 } else { 5819 // We are not the same height as when the selection was saved, so 5820 // don't try to restore the exact position 5821 mLayoutMode = LAYOUT_SET_SELECTION; 5822 } 5823 5824 // Restore selection 5825 setNextSelectedPositionInt(newPos); 5826 return; 5827 } 5828 } 5829 } 5830 break; 5831 case SYNC_FIRST_POSITION: 5832 // Leave mSyncPosition as it is -- just pin to available range 5833 mLayoutMode = LAYOUT_SYNC; 5834 mSyncPosition = Math.min(Math.max(0, mSyncPosition), count - 1); 5835 5836 return; 5837 } 5838 } 5839 5840 if (!isInTouchMode()) { 5841 // We couldn't find matching data -- try to use the same position 5842 newPos = getSelectedItemPosition(); 5843 5844 // Pin position to the available range 5845 if (newPos >= count) { 5846 newPos = count - 1; 5847 } 5848 if (newPos < 0) { 5849 newPos = 0; 5850 } 5851 5852 // Make sure we select something selectable -- first look down 5853 selectablePos = lookForSelectablePosition(newPos, true); 5854 5855 if (selectablePos >= 0) { 5856 setNextSelectedPositionInt(selectablePos); 5857 return; 5858 } else { 5859 // Looking down didn't work -- try looking up 5860 selectablePos = lookForSelectablePosition(newPos, false); 5861 if (selectablePos >= 0) { 5862 setNextSelectedPositionInt(selectablePos); 5863 return; 5864 } 5865 } 5866 } else { 5867 5868 // We already know where we want to resurrect the selection 5869 if (mResurrectToPosition >= 0) { 5870 return; 5871 } 5872 } 5873 5874 } 5875 5876 // Nothing is selected. Give up and reset everything. 5877 mLayoutMode = mStackFromBottom ? LAYOUT_FORCE_BOTTOM : LAYOUT_FORCE_TOP; 5878 mSelectedPosition = INVALID_POSITION; 5879 mSelectedRowId = INVALID_ROW_ID; 5880 mNextSelectedPosition = INVALID_POSITION; 5881 mNextSelectedRowId = INVALID_ROW_ID; 5882 mNeedSync = false; 5883 mPendingSync = null; 5884 mSelectorPosition = INVALID_POSITION; 5885 checkSelectionChanged(); 5886 } 5887 5888 @Override onDisplayHint(int hint)5889 protected void onDisplayHint(int hint) { 5890 super.onDisplayHint(hint); 5891 switch (hint) { 5892 case INVISIBLE: 5893 if (mPopup != null && mPopup.isShowing()) { 5894 dismissPopup(); 5895 } 5896 break; 5897 case VISIBLE: 5898 if (mFiltered && mPopup != null && !mPopup.isShowing()) { 5899 showPopup(); 5900 } 5901 break; 5902 } 5903 mPopupHidden = hint == INVISIBLE; 5904 } 5905 5906 /** 5907 * Removes the filter window 5908 */ dismissPopup()5909 private void dismissPopup() { 5910 if (mPopup != null) { 5911 mPopup.dismiss(); 5912 } 5913 } 5914 5915 /** 5916 * Shows the filter window 5917 */ showPopup()5918 private void showPopup() { 5919 // Make sure we have a window before showing the popup 5920 if (getWindowVisibility() == View.VISIBLE) { 5921 createTextFilter(true); 5922 positionPopup(); 5923 // Make sure we get focus if we are showing the popup 5924 checkFocus(); 5925 } 5926 } 5927 positionPopup()5928 private void positionPopup() { 5929 int screenHeight = getResources().getDisplayMetrics().heightPixels; 5930 final int[] xy = new int[2]; 5931 getLocationOnScreen(xy); 5932 // TODO: The 20 below should come from the theme 5933 // TODO: And the gravity should be defined in the theme as well 5934 final int bottomGap = screenHeight - xy[1] - getHeight() + (int) (mDensityScale * 20); 5935 if (!mPopup.isShowing()) { 5936 mPopup.showAtLocation(this, Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 5937 xy[0], bottomGap); 5938 } else { 5939 mPopup.update(xy[0], bottomGap, -1, -1); 5940 } 5941 } 5942 5943 /** 5944 * What is the distance between the source and destination rectangles given the direction of 5945 * focus navigation between them? The direction basically helps figure out more quickly what is 5946 * self evident by the relationship between the rects... 5947 * 5948 * @param source the source rectangle 5949 * @param dest the destination rectangle 5950 * @param direction the direction 5951 * @return the distance between the rectangles 5952 */ getDistance(Rect source, Rect dest, int direction)5953 static int getDistance(Rect source, Rect dest, int direction) { 5954 int sX, sY; // source x, y 5955 int dX, dY; // dest x, y 5956 switch (direction) { 5957 case View.FOCUS_RIGHT: 5958 sX = source.right; 5959 sY = source.top + source.height() / 2; 5960 dX = dest.left; 5961 dY = dest.top + dest.height() / 2; 5962 break; 5963 case View.FOCUS_DOWN: 5964 sX = source.left + source.width() / 2; 5965 sY = source.bottom; 5966 dX = dest.left + dest.width() / 2; 5967 dY = dest.top; 5968 break; 5969 case View.FOCUS_LEFT: 5970 sX = source.left; 5971 sY = source.top + source.height() / 2; 5972 dX = dest.right; 5973 dY = dest.top + dest.height() / 2; 5974 break; 5975 case View.FOCUS_UP: 5976 sX = source.left + source.width() / 2; 5977 sY = source.top; 5978 dX = dest.left + dest.width() / 2; 5979 dY = dest.bottom; 5980 break; 5981 case View.FOCUS_FORWARD: 5982 case View.FOCUS_BACKWARD: 5983 sX = source.right + source.width() / 2; 5984 sY = source.top + source.height() / 2; 5985 dX = dest.left + dest.width() / 2; 5986 dY = dest.top + dest.height() / 2; 5987 break; 5988 default: 5989 throw new IllegalArgumentException("direction must be one of " 5990 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT, " 5991 + "FOCUS_FORWARD, FOCUS_BACKWARD}."); 5992 } 5993 int deltaX = dX - sX; 5994 int deltaY = dY - sY; 5995 return deltaY * deltaY + deltaX * deltaX; 5996 } 5997 5998 @Override isInFilterMode()5999 protected boolean isInFilterMode() { 6000 return mFiltered; 6001 } 6002 6003 /** 6004 * Sends a key to the text filter window 6005 * 6006 * @param keyCode The keycode for the event 6007 * @param event The actual key event 6008 * 6009 * @return True if the text filter handled the event, false otherwise. 6010 */ sendToTextFilter(int keyCode, int count, KeyEvent event)6011 boolean sendToTextFilter(int keyCode, int count, KeyEvent event) { 6012 if (!acceptFilter()) { 6013 return false; 6014 } 6015 6016 boolean handled = false; 6017 boolean okToSend = true; 6018 switch (keyCode) { 6019 case KeyEvent.KEYCODE_DPAD_UP: 6020 case KeyEvent.KEYCODE_DPAD_DOWN: 6021 case KeyEvent.KEYCODE_DPAD_LEFT: 6022 case KeyEvent.KEYCODE_DPAD_RIGHT: 6023 case KeyEvent.KEYCODE_DPAD_CENTER: 6024 case KeyEvent.KEYCODE_ENTER: 6025 case KeyEvent.KEYCODE_NUMPAD_ENTER: 6026 okToSend = false; 6027 break; 6028 case KeyEvent.KEYCODE_BACK: 6029 case KeyEvent.KEYCODE_ESCAPE: 6030 if (mFiltered && mPopup != null && mPopup.isShowing()) { 6031 if (event.getAction() == KeyEvent.ACTION_DOWN 6032 && event.getRepeatCount() == 0) { 6033 KeyEvent.DispatcherState state = getKeyDispatcherState(); 6034 if (state != null) { 6035 state.startTracking(event, this); 6036 } 6037 handled = true; 6038 } else if (event.getAction() == KeyEvent.ACTION_UP 6039 && event.isTracking() && !event.isCanceled()) { 6040 handled = true; 6041 mTextFilter.setText(""); 6042 } 6043 } 6044 okToSend = false; 6045 break; 6046 case KeyEvent.KEYCODE_SPACE: 6047 // Only send spaces once we are filtered 6048 okToSend = mFiltered; 6049 break; 6050 } 6051 6052 if (okToSend) { 6053 createTextFilter(true); 6054 6055 KeyEvent forwardEvent = event; 6056 if (forwardEvent.getRepeatCount() > 0) { 6057 forwardEvent = KeyEvent.changeTimeRepeat(event, event.getEventTime(), 0); 6058 } 6059 6060 int action = event.getAction(); 6061 switch (action) { 6062 case KeyEvent.ACTION_DOWN: 6063 handled = mTextFilter.onKeyDown(keyCode, forwardEvent); 6064 break; 6065 6066 case KeyEvent.ACTION_UP: 6067 handled = mTextFilter.onKeyUp(keyCode, forwardEvent); 6068 break; 6069 6070 case KeyEvent.ACTION_MULTIPLE: 6071 handled = mTextFilter.onKeyMultiple(keyCode, count, event); 6072 break; 6073 } 6074 } 6075 return handled; 6076 } 6077 6078 /** 6079 * Return an InputConnection for editing of the filter text. 6080 */ 6081 @Override onCreateInputConnection(EditorInfo outAttrs)6082 public InputConnection onCreateInputConnection(EditorInfo outAttrs) { 6083 if (isTextFilterEnabled()) { 6084 if (mPublicInputConnection == null) { 6085 mDefInputConnection = new BaseInputConnection(this, false); 6086 mPublicInputConnection = new InputConnectionWrapper(outAttrs); 6087 } 6088 outAttrs.inputType = EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_FILTER; 6089 outAttrs.imeOptions = EditorInfo.IME_ACTION_DONE; 6090 return mPublicInputConnection; 6091 } 6092 return null; 6093 } 6094 6095 private class InputConnectionWrapper implements InputConnection { 6096 private final EditorInfo mOutAttrs; 6097 private InputConnection mTarget; 6098 InputConnectionWrapper(EditorInfo outAttrs)6099 public InputConnectionWrapper(EditorInfo outAttrs) { 6100 mOutAttrs = outAttrs; 6101 } 6102 getTarget()6103 private InputConnection getTarget() { 6104 if (mTarget == null) { 6105 mTarget = getTextFilterInput().onCreateInputConnection(mOutAttrs); 6106 } 6107 return mTarget; 6108 } 6109 6110 @Override reportFullscreenMode(boolean enabled)6111 public boolean reportFullscreenMode(boolean enabled) { 6112 // Use our own input connection, since it is 6113 // the "real" one the IME is talking with. 6114 return mDefInputConnection.reportFullscreenMode(enabled); 6115 } 6116 6117 @Override performEditorAction(int editorAction)6118 public boolean performEditorAction(int editorAction) { 6119 // The editor is off in its own window; we need to be 6120 // the one that does this. 6121 if (editorAction == EditorInfo.IME_ACTION_DONE) { 6122 InputMethodManager imm = 6123 getContext().getSystemService(InputMethodManager.class); 6124 if (imm != null) { 6125 imm.hideSoftInputFromWindow(getWindowToken(), 0); 6126 } 6127 return true; 6128 } 6129 return false; 6130 } 6131 6132 @Override sendKeyEvent(KeyEvent event)6133 public boolean sendKeyEvent(KeyEvent event) { 6134 // Use our own input connection, since the filter 6135 // text view may not be shown in a window so has 6136 // no ViewAncestor to dispatch events with. 6137 return mDefInputConnection.sendKeyEvent(event); 6138 } 6139 6140 @Override getTextBeforeCursor(int n, int flags)6141 public CharSequence getTextBeforeCursor(int n, int flags) { 6142 if (mTarget == null) return ""; 6143 return mTarget.getTextBeforeCursor(n, flags); 6144 } 6145 6146 @Override getTextAfterCursor(int n, int flags)6147 public CharSequence getTextAfterCursor(int n, int flags) { 6148 if (mTarget == null) return ""; 6149 return mTarget.getTextAfterCursor(n, flags); 6150 } 6151 6152 @Override getSelectedText(int flags)6153 public CharSequence getSelectedText(int flags) { 6154 if (mTarget == null) return ""; 6155 return mTarget.getSelectedText(flags); 6156 } 6157 6158 @Override getSurroundingText(int beforeLength, int afterLength, int flags)6159 public SurroundingText getSurroundingText(int beforeLength, int afterLength, int flags) { 6160 if (mTarget == null) return null; 6161 return mTarget.getSurroundingText(beforeLength, afterLength, flags); 6162 } 6163 6164 @Override getCursorCapsMode(int reqModes)6165 public int getCursorCapsMode(int reqModes) { 6166 if (mTarget == null) return InputType.TYPE_TEXT_FLAG_CAP_SENTENCES; 6167 return mTarget.getCursorCapsMode(reqModes); 6168 } 6169 6170 @Override getExtractedText(ExtractedTextRequest request, int flags)6171 public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) { 6172 return getTarget().getExtractedText(request, flags); 6173 } 6174 6175 @Override deleteSurroundingText(int beforeLength, int afterLength)6176 public boolean deleteSurroundingText(int beforeLength, int afterLength) { 6177 return getTarget().deleteSurroundingText(beforeLength, afterLength); 6178 } 6179 6180 @Override deleteSurroundingTextInCodePoints(int beforeLength, int afterLength)6181 public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength) { 6182 return getTarget().deleteSurroundingTextInCodePoints(beforeLength, afterLength); 6183 } 6184 6185 @Override setComposingText(CharSequence text, int newCursorPosition)6186 public boolean setComposingText(CharSequence text, int newCursorPosition) { 6187 return getTarget().setComposingText(text, newCursorPosition); 6188 } 6189 6190 @Override setComposingRegion(int start, int end)6191 public boolean setComposingRegion(int start, int end) { 6192 return getTarget().setComposingRegion(start, end); 6193 } 6194 6195 @Override finishComposingText()6196 public boolean finishComposingText() { 6197 return mTarget == null || mTarget.finishComposingText(); 6198 } 6199 6200 @Override commitText(CharSequence text, int newCursorPosition)6201 public boolean commitText(CharSequence text, int newCursorPosition) { 6202 return getTarget().commitText(text, newCursorPosition); 6203 } 6204 6205 @Override commitCompletion(CompletionInfo text)6206 public boolean commitCompletion(CompletionInfo text) { 6207 return getTarget().commitCompletion(text); 6208 } 6209 6210 @Override commitCorrection(CorrectionInfo correctionInfo)6211 public boolean commitCorrection(CorrectionInfo correctionInfo) { 6212 return getTarget().commitCorrection(correctionInfo); 6213 } 6214 6215 @Override setSelection(int start, int end)6216 public boolean setSelection(int start, int end) { 6217 return getTarget().setSelection(start, end); 6218 } 6219 6220 @Override performContextMenuAction(int id)6221 public boolean performContextMenuAction(int id) { 6222 return getTarget().performContextMenuAction(id); 6223 } 6224 6225 @Override beginBatchEdit()6226 public boolean beginBatchEdit() { 6227 return getTarget().beginBatchEdit(); 6228 } 6229 6230 @Override endBatchEdit()6231 public boolean endBatchEdit() { 6232 return getTarget().endBatchEdit(); 6233 } 6234 6235 @Override clearMetaKeyStates(int states)6236 public boolean clearMetaKeyStates(int states) { 6237 return getTarget().clearMetaKeyStates(states); 6238 } 6239 6240 @Override performPrivateCommand(String action, Bundle data)6241 public boolean performPrivateCommand(String action, Bundle data) { 6242 return getTarget().performPrivateCommand(action, data); 6243 } 6244 6245 @Override requestCursorUpdates(int cursorUpdateMode)6246 public boolean requestCursorUpdates(int cursorUpdateMode) { 6247 return getTarget().requestCursorUpdates(cursorUpdateMode); 6248 } 6249 6250 @Override requestCursorUpdates(int cursorUpdateMode, int cursorUpdateFilter)6251 public boolean requestCursorUpdates(int cursorUpdateMode, int cursorUpdateFilter) { 6252 return getTarget().requestCursorUpdates(cursorUpdateMode, cursorUpdateFilter); 6253 } 6254 6255 @Override getHandler()6256 public Handler getHandler() { 6257 return getTarget().getHandler(); 6258 } 6259 6260 @Override closeConnection()6261 public void closeConnection() { 6262 getTarget().closeConnection(); 6263 } 6264 6265 @Override commitContent(InputContentInfo inputContentInfo, int flags, Bundle opts)6266 public boolean commitContent(InputContentInfo inputContentInfo, int flags, Bundle opts) { 6267 return getTarget().commitContent(inputContentInfo, flags, opts); 6268 } 6269 } 6270 6271 /** 6272 * For filtering we proxy an input connection to an internal text editor, 6273 * and this allows the proxying to happen. 6274 */ 6275 @Override checkInputConnectionProxy(View view)6276 public boolean checkInputConnectionProxy(View view) { 6277 return view == mTextFilter; 6278 } 6279 6280 /** 6281 * Creates the window for the text filter and populates it with an EditText field; 6282 * 6283 * @param animateEntrance true if the window should appear with an animation 6284 */ createTextFilter(boolean animateEntrance)6285 private void createTextFilter(boolean animateEntrance) { 6286 if (mPopup == null) { 6287 PopupWindow p = new PopupWindow(getContext()); 6288 p.setFocusable(false); 6289 p.setTouchable(false); 6290 p.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); 6291 p.setContentView(getTextFilterInput()); 6292 p.setWidth(LayoutParams.WRAP_CONTENT); 6293 p.setHeight(LayoutParams.WRAP_CONTENT); 6294 p.setBackgroundDrawable(null); 6295 mPopup = p; 6296 getViewTreeObserver().addOnGlobalLayoutListener(this); 6297 mGlobalLayoutListenerAddedFilter = true; 6298 } 6299 if (animateEntrance) { 6300 mPopup.setAnimationStyle(com.android.internal.R.style.Animation_TypingFilter); 6301 } else { 6302 mPopup.setAnimationStyle(com.android.internal.R.style.Animation_TypingFilterRestore); 6303 } 6304 } 6305 getTextFilterInput()6306 private EditText getTextFilterInput() { 6307 if (mTextFilter == null) { 6308 final LayoutInflater layoutInflater = LayoutInflater.from(getContext()); 6309 mTextFilter = (EditText) layoutInflater.inflate( 6310 com.android.internal.R.layout.typing_filter, null); 6311 // For some reason setting this as the "real" input type changes 6312 // the text view in some way that it doesn't work, and I don't 6313 // want to figure out why this is. 6314 mTextFilter.setRawInputType(EditorInfo.TYPE_CLASS_TEXT 6315 | EditorInfo.TYPE_TEXT_VARIATION_FILTER); 6316 mTextFilter.setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI); 6317 mTextFilter.addTextChangedListener(this); 6318 } 6319 return mTextFilter; 6320 } 6321 6322 /** 6323 * Clear the text filter. 6324 */ clearTextFilter()6325 public void clearTextFilter() { 6326 if (mFiltered) { 6327 getTextFilterInput().setText(""); 6328 mFiltered = false; 6329 if (mPopup != null && mPopup.isShowing()) { 6330 dismissPopup(); 6331 } 6332 } 6333 } 6334 6335 /** 6336 * Returns if the ListView currently has a text filter. 6337 */ hasTextFilter()6338 public boolean hasTextFilter() { 6339 return mFiltered; 6340 } 6341 6342 @Override onGlobalLayout()6343 public void onGlobalLayout() { 6344 if (isShown()) { 6345 // Show the popup if we are filtered 6346 if (mFiltered && mPopup != null && !mPopup.isShowing() && !mPopupHidden) { 6347 showPopup(); 6348 } 6349 } else { 6350 // Hide the popup when we are no longer visible 6351 if (mPopup != null && mPopup.isShowing()) { 6352 dismissPopup(); 6353 } 6354 } 6355 6356 } 6357 6358 /** 6359 * For our text watcher that is associated with the text filter. Does 6360 * nothing. 6361 */ 6362 @Override beforeTextChanged(CharSequence s, int start, int count, int after)6363 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 6364 } 6365 6366 /** 6367 * For our text watcher that is associated with the text filter. Performs 6368 * the actual filtering as the text changes, and takes care of hiding and 6369 * showing the popup displaying the currently entered filter text. 6370 */ 6371 @Override onTextChanged(CharSequence s, int start, int before, int count)6372 public void onTextChanged(CharSequence s, int start, int before, int count) { 6373 if (isTextFilterEnabled()) { 6374 createTextFilter(true); 6375 int length = s.length(); 6376 boolean showing = mPopup.isShowing(); 6377 if (!showing && length > 0) { 6378 // Show the filter popup if necessary 6379 showPopup(); 6380 mFiltered = true; 6381 } else if (showing && length == 0) { 6382 // Remove the filter popup if the user has cleared all text 6383 dismissPopup(); 6384 mFiltered = false; 6385 } 6386 if (mAdapter instanceof Filterable) { 6387 Filter f = ((Filterable) mAdapter).getFilter(); 6388 // Filter should not be null when we reach this part 6389 if (f != null) { 6390 f.filter(s, this); 6391 } else { 6392 throw new IllegalStateException("You cannot call onTextChanged with a non " 6393 + "filterable adapter"); 6394 } 6395 } 6396 } 6397 } 6398 6399 /** 6400 * For our text watcher that is associated with the text filter. Does 6401 * nothing. 6402 */ 6403 @Override afterTextChanged(Editable s)6404 public void afterTextChanged(Editable s) { 6405 } 6406 6407 @Override onFilterComplete(int count)6408 public void onFilterComplete(int count) { 6409 if (mSelectedPosition < 0 && count > 0) { 6410 mResurrectToPosition = INVALID_POSITION; 6411 resurrectSelection(); 6412 } 6413 } 6414 6415 @Override generateDefaultLayoutParams()6416 protected ViewGroup.LayoutParams generateDefaultLayoutParams() { 6417 return new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 6418 ViewGroup.LayoutParams.WRAP_CONTENT, 0); 6419 } 6420 6421 @Override generateLayoutParams(ViewGroup.LayoutParams p)6422 protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { 6423 return new LayoutParams(p); 6424 } 6425 6426 @Override generateLayoutParams(AttributeSet attrs)6427 public LayoutParams generateLayoutParams(AttributeSet attrs) { 6428 return new AbsListView.LayoutParams(getContext(), attrs); 6429 } 6430 6431 @Override checkLayoutParams(ViewGroup.LayoutParams p)6432 protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 6433 return p instanceof AbsListView.LayoutParams; 6434 } 6435 6436 /** 6437 * Puts the list or grid into transcript mode. In this mode the list or grid will always scroll 6438 * to the bottom to show new items. 6439 * 6440 * @param mode the transcript mode to set 6441 * 6442 * @see #TRANSCRIPT_MODE_DISABLED 6443 * @see #TRANSCRIPT_MODE_NORMAL 6444 * @see #TRANSCRIPT_MODE_ALWAYS_SCROLL 6445 */ setTranscriptMode(int mode)6446 public void setTranscriptMode(int mode) { 6447 mTranscriptMode = mode; 6448 } 6449 6450 /** 6451 * Returns the current transcript mode. 6452 * 6453 * @return {@link #TRANSCRIPT_MODE_DISABLED}, {@link #TRANSCRIPT_MODE_NORMAL} or 6454 * {@link #TRANSCRIPT_MODE_ALWAYS_SCROLL} 6455 */ 6456 @InspectableProperty(enumMapping = { 6457 @EnumEntry(value = TRANSCRIPT_MODE_DISABLED, name = "disabled"), 6458 @EnumEntry(value = TRANSCRIPT_MODE_NORMAL, name = "normal"), 6459 @EnumEntry(value = TRANSCRIPT_MODE_ALWAYS_SCROLL, name = "alwaysScroll") 6460 }) getTranscriptMode()6461 public int getTranscriptMode() { 6462 return mTranscriptMode; 6463 } 6464 6465 @Override getSolidColor()6466 public int getSolidColor() { 6467 return mCacheColorHint; 6468 } 6469 6470 /** 6471 * When set to a non-zero value, the cache color hint indicates that this list is always drawn 6472 * on top of a solid, single-color, opaque background. 6473 * 6474 * Zero means that what's behind this object is translucent (non solid) or is not made of a 6475 * single color. This hint will not affect any existing background drawable set on this view ( 6476 * typically set via {@link #setBackgroundDrawable(Drawable)}). 6477 * 6478 * @param color The background color 6479 */ setCacheColorHint(@olorInt int color)6480 public void setCacheColorHint(@ColorInt int color) { 6481 if (color != mCacheColorHint) { 6482 mCacheColorHint = color; 6483 int count = getChildCount(); 6484 for (int i = 0; i < count; i++) { 6485 getChildAt(i).setDrawingCacheBackgroundColor(color); 6486 } 6487 mRecycler.setCacheColorHint(color); 6488 } 6489 } 6490 6491 /** 6492 * When set to a non-zero value, the cache color hint indicates that this list is always drawn 6493 * on top of a solid, single-color, opaque background 6494 * 6495 * @return The cache color hint 6496 */ 6497 @ViewDebug.ExportedProperty(category = "drawing") 6498 @InspectableProperty 6499 @ColorInt getCacheColorHint()6500 public int getCacheColorHint() { 6501 return mCacheColorHint; 6502 } 6503 6504 /** 6505 * Move all views (excluding headers and footers) held by this AbsListView into the supplied 6506 * List. This includes views displayed on the screen as well as views stored in AbsListView's 6507 * internal view recycler. 6508 * 6509 * @param views A list into which to put the reclaimed views 6510 */ reclaimViews(List<View> views)6511 public void reclaimViews(List<View> views) { 6512 int childCount = getChildCount(); 6513 RecyclerListener listener = mRecycler.mRecyclerListener; 6514 6515 // Reclaim views on screen 6516 for (int i = 0; i < childCount; i++) { 6517 View child = getChildAt(i); 6518 AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams(); 6519 // Don't reclaim header or footer views, or views that should be ignored 6520 if (lp != null && mRecycler.shouldRecycleViewType(lp.viewType)) { 6521 views.add(child); 6522 child.setAccessibilityDelegate(null); 6523 child.resetSubtreeAutofillIds(); 6524 if (listener != null) { 6525 // Pretend they went through the scrap heap 6526 listener.onMovedToScrapHeap(child); 6527 } 6528 } 6529 } 6530 mRecycler.reclaimScrapViews(views); 6531 removeAllViewsInLayout(); 6532 } 6533 finishGlows()6534 private void finishGlows() { 6535 if (shouldDisplayEdgeEffects()) { 6536 mEdgeGlowTop.finish(); 6537 mEdgeGlowBottom.finish(); 6538 } 6539 } 6540 6541 /** 6542 * Sets up this AbsListView to use a remote views adapter which connects to a RemoteViewsService 6543 * through the specified intent. 6544 * @param intent the intent used to identify the RemoteViewsService for the adapter to connect to. 6545 */ setRemoteViewsAdapter(Intent intent)6546 public void setRemoteViewsAdapter(Intent intent) { 6547 setRemoteViewsAdapter(intent, false); 6548 } 6549 6550 /** @hide **/ setRemoteViewsAdapterAsync(final Intent intent)6551 public Runnable setRemoteViewsAdapterAsync(final Intent intent) { 6552 return new RemoteViewsAdapter.AsyncRemoteAdapterAction(this, intent); 6553 } 6554 6555 /** @hide **/ 6556 @Override setRemoteViewsAdapter(Intent intent, boolean isAsync)6557 public void setRemoteViewsAdapter(Intent intent, boolean isAsync) { 6558 // Ensure that we don't already have a RemoteViewsAdapter that is bound to an existing 6559 // service handling the specified intent. 6560 if (mRemoteAdapter != null) { 6561 Intent.FilterComparison fcNew = new Intent.FilterComparison(intent); 6562 Intent.FilterComparison fcOld = new Intent.FilterComparison( 6563 mRemoteAdapter.getRemoteViewsServiceIntent()); 6564 if (fcNew.equals(fcOld)) { 6565 return; 6566 } 6567 } 6568 mDeferNotifyDataSetChanged = false; 6569 // Otherwise, create a new RemoteViewsAdapter for binding 6570 mRemoteAdapter = new RemoteViewsAdapter(getContext(), intent, this, isAsync); 6571 if (mRemoteAdapter.isDataReady()) { 6572 setAdapter(mRemoteAdapter); 6573 } 6574 } 6575 6576 /** 6577 * Sets up the onClickHandler to be used by the RemoteViewsAdapter when inflating RemoteViews 6578 * 6579 * @param handler The OnClickHandler to use when inflating RemoteViews. 6580 * 6581 * @hide 6582 */ setRemoteViewsInteractionHandler(InteractionHandler handler)6583 public void setRemoteViewsInteractionHandler(InteractionHandler handler) { 6584 // Ensure that we don't already have a RemoteViewsAdapter that is bound to an existing 6585 // service handling the specified intent. 6586 if (mRemoteAdapter != null) { 6587 mRemoteAdapter.setRemoteViewsInteractionHandler(handler); 6588 } 6589 } 6590 6591 /** 6592 * This defers a notifyDataSetChanged on the pending RemoteViewsAdapter if it has not 6593 * connected yet. 6594 */ 6595 @Override deferNotifyDataSetChanged()6596 public void deferNotifyDataSetChanged() { 6597 mDeferNotifyDataSetChanged = true; 6598 } 6599 6600 /** 6601 * Called back when the adapter connects to the RemoteViewsService. 6602 */ 6603 @Override onRemoteAdapterConnected()6604 public boolean onRemoteAdapterConnected() { 6605 if (mRemoteAdapter != mAdapter) { 6606 setAdapter(mRemoteAdapter); 6607 if (mDeferNotifyDataSetChanged) { 6608 mRemoteAdapter.notifyDataSetChanged(); 6609 mDeferNotifyDataSetChanged = false; 6610 } 6611 return false; 6612 } else if (mRemoteAdapter != null) { 6613 mRemoteAdapter.superNotifyDataSetChanged(); 6614 return true; 6615 } 6616 return false; 6617 } 6618 6619 /** 6620 * Called back when the adapter disconnects from the RemoteViewsService. 6621 */ 6622 @Override onRemoteAdapterDisconnected()6623 public void onRemoteAdapterDisconnected() { 6624 // If the remote adapter disconnects, we keep it around 6625 // since the currently displayed items are still cached. 6626 // Further, we want the service to eventually reconnect 6627 // when necessary, as triggered by this view requesting 6628 // items from the Adapter. 6629 } 6630 6631 /** 6632 * Hints the RemoteViewsAdapter, if it exists, about which views are currently 6633 * being displayed by the AbsListView. 6634 */ setVisibleRangeHint(int start, int end)6635 void setVisibleRangeHint(int start, int end) { 6636 if (mRemoteAdapter != null) { 6637 mRemoteAdapter.setVisibleRangeHint(start, end); 6638 } 6639 } 6640 6641 /** 6642 * Sets the edge effect color for both top and bottom edge effects. 6643 * 6644 * @param color The color for the edge effects. 6645 * @see #setTopEdgeEffectColor(int) 6646 * @see #setBottomEdgeEffectColor(int) 6647 * @see #getTopEdgeEffectColor() 6648 * @see #getBottomEdgeEffectColor() 6649 */ setEdgeEffectColor(@olorInt int color)6650 public void setEdgeEffectColor(@ColorInt int color) { 6651 setTopEdgeEffectColor(color); 6652 setBottomEdgeEffectColor(color); 6653 } 6654 6655 /** 6656 * Sets the bottom edge effect color. 6657 * 6658 * @param color The color for the bottom edge effect. 6659 * @see #setTopEdgeEffectColor(int) 6660 * @see #setEdgeEffectColor(int) 6661 * @see #getTopEdgeEffectColor() 6662 * @see #getBottomEdgeEffectColor() 6663 */ setBottomEdgeEffectColor(@olorInt int color)6664 public void setBottomEdgeEffectColor(@ColorInt int color) { 6665 mEdgeGlowBottom.setColor(color); 6666 invalidateEdgeEffects(); 6667 } 6668 6669 /** 6670 * Sets the top edge effect color. 6671 * 6672 * @param color The color for the top edge effect. 6673 * @see #setBottomEdgeEffectColor(int) 6674 * @see #setEdgeEffectColor(int) 6675 * @see #getTopEdgeEffectColor() 6676 * @see #getBottomEdgeEffectColor() 6677 */ setTopEdgeEffectColor(@olorInt int color)6678 public void setTopEdgeEffectColor(@ColorInt int color) { 6679 mEdgeGlowTop.setColor(color); 6680 invalidateEdgeEffects(); 6681 } 6682 6683 /** 6684 * Returns the top edge effect color. 6685 * 6686 * @return The top edge effect color. 6687 * @see #setEdgeEffectColor(int) 6688 * @see #setTopEdgeEffectColor(int) 6689 * @see #setBottomEdgeEffectColor(int) 6690 * @see #getBottomEdgeEffectColor() 6691 */ 6692 @ColorInt getTopEdgeEffectColor()6693 public int getTopEdgeEffectColor() { 6694 return mEdgeGlowTop.getColor(); 6695 } 6696 6697 /** 6698 * Returns the bottom edge effect color. 6699 * 6700 * @return The bottom edge effect color. 6701 * @see #setEdgeEffectColor(int) 6702 * @see #setTopEdgeEffectColor(int) 6703 * @see #setBottomEdgeEffectColor(int) 6704 * @see #getTopEdgeEffectColor() 6705 */ 6706 @ColorInt getBottomEdgeEffectColor()6707 public int getBottomEdgeEffectColor() { 6708 return mEdgeGlowBottom.getColor(); 6709 } 6710 6711 /** 6712 * Sets the recycler listener to be notified whenever a View is set aside in 6713 * the recycler for later reuse. This listener can be used to free resources 6714 * associated to the View. 6715 * 6716 * @param listener The recycler listener to be notified of views set aside 6717 * in the recycler. 6718 * 6719 * @see android.widget.AbsListView.RecyclerListener 6720 */ setRecyclerListener(RecyclerListener listener)6721 public void setRecyclerListener(RecyclerListener listener) { 6722 mRecycler.mRecyclerListener = listener; 6723 } 6724 6725 /** 6726 * {@inheritDoc} 6727 * 6728 * This method will initialize the fields of the {@link ViewStructure} 6729 * using the base implementation in {@link View}. On API level 33 and higher, it may also 6730 * write information about the positions of active views to the extras bundle provided by the 6731 * {@link ViewStructure}. 6732 * 6733 * NOTE: When overriding this method on API level 33, if not calling super() or if changing the 6734 * logic for child views, be sure to provide values for the first active child view position and 6735 * the list of active child views in the {@link ViewStructure}'s extras {@link Bundle} using the 6736 * "android.view.ViewStructure.extra.ACTIVE_CHILDREN_IDS" and 6737 * "android.view.ViewStructure.extra.FIRST_ACTIVE_POSITION" keys. 6738 * 6739 * @param structure {@link ViewStructure} to be filled in with structured view data. 6740 * @param flags optional flags. 6741 * 6742 * @see View#AUTOFILL_FLAG_INCLUDE_NOT_IMPORTANT_VIEWS 6743 */ 6744 @Override onProvideContentCaptureStructure( @onNull ViewStructure structure, int flags)6745 public void onProvideContentCaptureStructure( 6746 @NonNull ViewStructure structure, int flags) { 6747 super.onProvideContentCaptureStructure(structure, flags); 6748 if (!sContentCaptureReportingEnabledByDeviceConfig) { 6749 return; 6750 } 6751 6752 Bundle extras = structure.getExtras(); 6753 6754 if (extras == null) { 6755 Log.wtf(TAG, "Unexpected null extras Bundle in ViewStructure"); 6756 return; 6757 } 6758 6759 int childCount = getChildCount(); 6760 ArrayList<AutofillId> idsList = new ArrayList<>(childCount); 6761 6762 for (int i = 0; i < childCount; ++i) { 6763 View activeView = getChildAt(i); 6764 if (activeView == null) { 6765 continue; 6766 } 6767 6768 idsList.add(activeView.getAutofillId()); 6769 } 6770 6771 extras.putParcelableArrayList(ViewStructure.EXTRA_ACTIVE_CHILDREN_IDS, 6772 idsList); 6773 6774 extras.putInt(ViewStructure.EXTRA_FIRST_ACTIVE_POSITION, 6775 getFirstVisiblePosition()); 6776 } 6777 reportActiveViewsToContentCapture()6778 private void reportActiveViewsToContentCapture() { 6779 if (!sContentCaptureReportingEnabledByDeviceConfig) { 6780 return; 6781 } 6782 6783 ContentCaptureSession session = getContentCaptureSession(); 6784 if (session != null) { 6785 ViewStructure structure = session.newViewStructure(this); 6786 onProvideContentCaptureStructure(structure, /* flags= */ 0); 6787 session.notifyViewAppeared(structure); 6788 } 6789 } 6790 6791 class AdapterDataSetObserver extends AdapterView<ListAdapter>.AdapterDataSetObserver { 6792 @Override onChanged()6793 public void onChanged() { 6794 super.onChanged(); 6795 mReportChildrenToContentCaptureOnNextUpdate = true; 6796 if (mFastScroll != null) { 6797 mFastScroll.onSectionsChanged(); 6798 } 6799 } 6800 6801 @Override onInvalidated()6802 public void onInvalidated() { 6803 super.onInvalidated(); 6804 mReportChildrenToContentCaptureOnNextUpdate = true; 6805 if (mFastScroll != null) { 6806 mFastScroll.onSectionsChanged(); 6807 } 6808 } 6809 } 6810 6811 /** 6812 * A MultiChoiceModeListener receives events for {@link AbsListView#CHOICE_MODE_MULTIPLE_MODAL}. 6813 * It acts as the {@link ActionMode.Callback} for the selection mode and also receives 6814 * {@link #onItemCheckedStateChanged(ActionMode, int, long, boolean)} events when the user 6815 * selects and deselects list items. 6816 */ 6817 public interface MultiChoiceModeListener extends ActionMode.Callback { 6818 /** 6819 * Called when an item is checked or unchecked during selection mode. 6820 * 6821 * @param mode The {@link ActionMode} providing the selection mode 6822 * @param position Adapter position of the item that was checked or unchecked 6823 * @param id Adapter ID of the item that was checked or unchecked 6824 * @param checked <code>true</code> if the item is now checked, <code>false</code> 6825 * if the item is now unchecked. 6826 */ 6827 public void onItemCheckedStateChanged(ActionMode mode, 6828 int position, long id, boolean checked); 6829 } 6830 6831 class MultiChoiceModeWrapper implements MultiChoiceModeListener { 6832 private MultiChoiceModeListener mWrapped; 6833 setWrapped(MultiChoiceModeListener wrapped)6834 public void setWrapped(MultiChoiceModeListener wrapped) { 6835 mWrapped = wrapped; 6836 } 6837 hasWrappedCallback()6838 public boolean hasWrappedCallback() { 6839 return mWrapped != null; 6840 } 6841 6842 @Override onCreateActionMode(ActionMode mode, Menu menu)6843 public boolean onCreateActionMode(ActionMode mode, Menu menu) { 6844 if (mWrapped.onCreateActionMode(mode, menu)) { 6845 // Initialize checked graphic state? 6846 setLongClickable(false); 6847 return true; 6848 } 6849 return false; 6850 } 6851 6852 @Override onPrepareActionMode(ActionMode mode, Menu menu)6853 public boolean onPrepareActionMode(ActionMode mode, Menu menu) { 6854 return mWrapped.onPrepareActionMode(mode, menu); 6855 } 6856 6857 @Override onActionItemClicked(ActionMode mode, MenuItem item)6858 public boolean onActionItemClicked(ActionMode mode, MenuItem item) { 6859 return mWrapped.onActionItemClicked(mode, item); 6860 } 6861 6862 @Override onDestroyActionMode(ActionMode mode)6863 public void onDestroyActionMode(ActionMode mode) { 6864 mWrapped.onDestroyActionMode(mode); 6865 mChoiceActionMode = null; 6866 6867 // Ending selection mode means deselecting everything. 6868 clearChoices(); 6869 6870 mDataChanged = true; 6871 rememberSyncState(); 6872 requestLayout(); 6873 6874 setLongClickable(true); 6875 } 6876 6877 @Override onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked)6878 public void onItemCheckedStateChanged(ActionMode mode, 6879 int position, long id, boolean checked) { 6880 mWrapped.onItemCheckedStateChanged(mode, position, id, checked); 6881 6882 // If there are no items selected we no longer need the selection mode. 6883 if (getCheckedItemCount() == 0) { 6884 mode.finish(); 6885 } 6886 } 6887 } 6888 6889 /** 6890 * AbsListView extends LayoutParams to provide a place to hold the view type. 6891 */ 6892 public static class LayoutParams extends ViewGroup.LayoutParams { 6893 /** 6894 * View type for this view, as returned by 6895 * {@link android.widget.Adapter#getItemViewType(int) } 6896 */ 6897 @ViewDebug.ExportedProperty(category = "list", mapping = { 6898 @ViewDebug.IntToString(from = ITEM_VIEW_TYPE_IGNORE, to = "ITEM_VIEW_TYPE_IGNORE"), 6899 @ViewDebug.IntToString(from = ITEM_VIEW_TYPE_HEADER_OR_FOOTER, to = "ITEM_VIEW_TYPE_HEADER_OR_FOOTER") 6900 }) 6901 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) 6902 int viewType; 6903 6904 /** 6905 * When this boolean is set, the view has been added to the AbsListView 6906 * at least once. It is used to know whether headers/footers have already 6907 * been added to the list view and whether they should be treated as 6908 * recycled views or not. 6909 */ 6910 @ViewDebug.ExportedProperty(category = "list") 6911 boolean recycledHeaderFooter; 6912 6913 /** 6914 * When an AbsListView is measured with an AT_MOST measure spec, it needs 6915 * to obtain children views to measure itself. When doing so, the children 6916 * are not attached to the window, but put in the recycler which assumes 6917 * they've been attached before. Setting this flag will force the reused 6918 * view to be attached to the window rather than just attached to the 6919 * parent. 6920 */ 6921 @ViewDebug.ExportedProperty(category = "list") 6922 boolean forceAdd; 6923 6924 /** 6925 * The position the view was removed from when pulled out of the 6926 * scrap heap. 6927 * @hide 6928 */ 6929 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 6930 int scrappedFromPosition; 6931 6932 /** 6933 * The ID the view represents 6934 */ 6935 long itemId = -1; 6936 6937 /** Whether the adapter considers the item enabled. */ 6938 boolean isEnabled; 6939 LayoutParams(Context c, AttributeSet attrs)6940 public LayoutParams(Context c, AttributeSet attrs) { 6941 super(c, attrs); 6942 } 6943 LayoutParams(int w, int h)6944 public LayoutParams(int w, int h) { 6945 super(w, h); 6946 } 6947 LayoutParams(int w, int h, int viewType)6948 public LayoutParams(int w, int h, int viewType) { 6949 super(w, h); 6950 this.viewType = viewType; 6951 } 6952 LayoutParams(ViewGroup.LayoutParams source)6953 public LayoutParams(ViewGroup.LayoutParams source) { 6954 super(source); 6955 } 6956 6957 /** @hide */ 6958 @Override encodeProperties(@onNull ViewHierarchyEncoder encoder)6959 protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) { 6960 super.encodeProperties(encoder); 6961 6962 encoder.addProperty("list:viewType", viewType); 6963 encoder.addProperty("list:recycledHeaderFooter", recycledHeaderFooter); 6964 encoder.addProperty("list:forceAdd", forceAdd); 6965 encoder.addProperty("list:isEnabled", isEnabled); 6966 } 6967 } 6968 6969 /** 6970 * A RecyclerListener is used to receive a notification whenever a View is placed 6971 * inside the RecycleBin's scrap heap. This listener is used to free resources 6972 * associated to Views placed in the RecycleBin. 6973 * 6974 * @see android.widget.AbsListView#setRecyclerListener(android.widget.AbsListView.RecyclerListener) 6975 */ 6976 public static interface RecyclerListener { 6977 /** 6978 * Indicates that the specified View was moved into the recycler's scrap heap. 6979 * The view is not displayed on screen any more and any expensive resource 6980 * associated with the view should be discarded. 6981 * 6982 * @param view 6983 */ 6984 void onMovedToScrapHeap(View view); 6985 } 6986 6987 /** 6988 * The RecycleBin facilitates reuse of views across layouts. The RecycleBin has two levels of 6989 * storage: ActiveViews and ScrapViews. ActiveViews are those views which were onscreen at the 6990 * start of a layout. By construction, they are displaying current information. At the end of 6991 * layout, all views in ActiveViews are demoted to ScrapViews. ScrapViews are old views that 6992 * could potentially be used by the adapter to avoid allocating views unnecessarily. 6993 * 6994 * @see android.widget.AbsListView#setRecyclerListener(android.widget.AbsListView.RecyclerListener) 6995 * @see android.widget.AbsListView.RecyclerListener 6996 */ 6997 class RecycleBin { 6998 @UnsupportedAppUsage 6999 private RecyclerListener mRecyclerListener; 7000 7001 /** 7002 * The position of the first view stored in mActiveViews. 7003 */ 7004 private int mFirstActivePosition; 7005 7006 /** 7007 * Views that were on screen at the start of layout. This array is populated at the start of 7008 * layout, and at the end of layout all view in mActiveViews are moved to mScrapViews. 7009 * Views in mActiveViews represent a contiguous range of Views, with position of the first 7010 * view store in mFirstActivePosition. 7011 */ 7012 private View[] mActiveViews = new View[0]; 7013 7014 /** 7015 * Unsorted views that can be used by the adapter as a convert view. 7016 */ 7017 private ArrayList<View>[] mScrapViews; 7018 7019 private int mViewTypeCount; 7020 7021 private ArrayList<View> mCurrentScrap; 7022 7023 private ArrayList<View> mSkippedScrap; 7024 7025 private SparseArray<View> mTransientStateViews; 7026 private LongSparseArray<View> mTransientStateViewsById; 7027 setViewTypeCount(int viewTypeCount)7028 public void setViewTypeCount(int viewTypeCount) { 7029 if (viewTypeCount < 1) { 7030 throw new IllegalArgumentException("Can't have a viewTypeCount < 1"); 7031 } 7032 //noinspection unchecked 7033 ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount]; 7034 for (int i = 0; i < viewTypeCount; i++) { 7035 scrapViews[i] = new ArrayList<View>(); 7036 } 7037 mViewTypeCount = viewTypeCount; 7038 mCurrentScrap = scrapViews[0]; 7039 mScrapViews = scrapViews; 7040 } 7041 markChildrenDirty()7042 public void markChildrenDirty() { 7043 if (mViewTypeCount == 1) { 7044 final ArrayList<View> scrap = mCurrentScrap; 7045 final int scrapCount = scrap.size(); 7046 for (int i = 0; i < scrapCount; i++) { 7047 scrap.get(i).forceLayout(); 7048 } 7049 } else { 7050 final int typeCount = mViewTypeCount; 7051 for (int i = 0; i < typeCount; i++) { 7052 final ArrayList<View> scrap = mScrapViews[i]; 7053 final int scrapCount = scrap.size(); 7054 for (int j = 0; j < scrapCount; j++) { 7055 scrap.get(j).forceLayout(); 7056 } 7057 } 7058 } 7059 if (mTransientStateViews != null) { 7060 final int count = mTransientStateViews.size(); 7061 for (int i = 0; i < count; i++) { 7062 mTransientStateViews.valueAt(i).forceLayout(); 7063 } 7064 } 7065 if (mTransientStateViewsById != null) { 7066 final int count = mTransientStateViewsById.size(); 7067 for (int i = 0; i < count; i++) { 7068 mTransientStateViewsById.valueAt(i).forceLayout(); 7069 } 7070 } 7071 } 7072 shouldRecycleViewType(int viewType)7073 public boolean shouldRecycleViewType(int viewType) { 7074 return viewType >= 0; 7075 } 7076 7077 /** 7078 * Clears the scrap heap. 7079 */ 7080 @UnsupportedAppUsage clear()7081 void clear() { 7082 if (mViewTypeCount == 1) { 7083 final ArrayList<View> scrap = mCurrentScrap; 7084 clearScrap(scrap); 7085 } else { 7086 final int typeCount = mViewTypeCount; 7087 for (int i = 0; i < typeCount; i++) { 7088 final ArrayList<View> scrap = mScrapViews[i]; 7089 clearScrap(scrap); 7090 } 7091 } 7092 7093 clearTransientStateViews(); 7094 } 7095 7096 /** 7097 * Fill ActiveViews with all of the children of the AbsListView. 7098 * 7099 * @param childCount The minimum number of views mActiveViews should hold 7100 * @param firstActivePosition The position of the first view that will be stored in 7101 * mActiveViews 7102 */ fillActiveViews(int childCount, int firstActivePosition)7103 void fillActiveViews(int childCount, int firstActivePosition) { 7104 if (mActiveViews.length < childCount) { 7105 mActiveViews = new View[childCount]; 7106 } 7107 mFirstActivePosition = firstActivePosition; 7108 7109 //noinspection MismatchedReadAndWriteOfArray 7110 final View[] activeViews = mActiveViews; 7111 for (int i = 0; i < childCount; i++) { 7112 View child = getChildAt(i); 7113 AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams(); 7114 // Don't put header or footer views into the scrap heap 7115 if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { 7116 // Note: We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in active views. 7117 // However, we will NOT place them into scrap views. 7118 activeViews[i] = child; 7119 // Remember the position so that setupChild() doesn't reset state. 7120 lp.scrappedFromPosition = firstActivePosition + i; 7121 } 7122 } 7123 7124 if (mReportChildrenToContentCaptureOnNextUpdate && childCount > 0) { 7125 AbsListView.this.reportActiveViewsToContentCapture(); 7126 mReportChildrenToContentCaptureOnNextUpdate = false; 7127 } 7128 } 7129 7130 /** 7131 * Get the view corresponding to the specified position. The view will be removed from 7132 * mActiveViews if it is found. 7133 * 7134 * @param position The position to look up in mActiveViews 7135 * @return The view if it is found, null otherwise 7136 */ getActiveView(int position)7137 View getActiveView(int position) { 7138 int index = position - mFirstActivePosition; 7139 final View[] activeViews = mActiveViews; 7140 if (index >=0 && index < activeViews.length) { 7141 final View match = activeViews[index]; 7142 activeViews[index] = null; 7143 return match; 7144 } 7145 return null; 7146 } 7147 getTransientStateView(int position)7148 View getTransientStateView(int position) { 7149 if (mAdapter != null && mAdapterHasStableIds && mTransientStateViewsById != null) { 7150 long id = mAdapter.getItemId(position); 7151 View result = mTransientStateViewsById.get(id); 7152 mTransientStateViewsById.remove(id); 7153 return result; 7154 } 7155 if (mTransientStateViews != null) { 7156 final int index = mTransientStateViews.indexOfKey(position); 7157 if (index >= 0) { 7158 View result = mTransientStateViews.valueAt(index); 7159 mTransientStateViews.removeAt(index); 7160 return result; 7161 } 7162 } 7163 return null; 7164 } 7165 7166 /** 7167 * Dumps and fully detaches any currently saved views with transient 7168 * state. 7169 */ clearTransientStateViews()7170 void clearTransientStateViews() { 7171 final SparseArray<View> viewsByPos = mTransientStateViews; 7172 if (viewsByPos != null) { 7173 final int N = viewsByPos.size(); 7174 for (int i = 0; i < N; i++) { 7175 removeDetachedView(viewsByPos.valueAt(i), false); 7176 } 7177 viewsByPos.clear(); 7178 } 7179 7180 final LongSparseArray<View> viewsById = mTransientStateViewsById; 7181 if (viewsById != null) { 7182 final int N = viewsById.size(); 7183 for (int i = 0; i < N; i++) { 7184 removeDetachedView(viewsById.valueAt(i), false); 7185 } 7186 viewsById.clear(); 7187 } 7188 } 7189 7190 /** 7191 * @return A view from the ScrapViews collection. These are unordered. 7192 */ getScrapView(int position)7193 View getScrapView(int position) { 7194 final int whichScrap = mAdapter.getItemViewType(position); 7195 if (whichScrap < 0) { 7196 return null; 7197 } 7198 if (mViewTypeCount == 1) { 7199 return retrieveFromScrap(mCurrentScrap, position); 7200 } else if (whichScrap < mScrapViews.length) { 7201 return retrieveFromScrap(mScrapViews[whichScrap], position); 7202 } 7203 return null; 7204 } 7205 7206 /** 7207 * Puts a view into the list of scrap views. 7208 * <p> 7209 * If the list data hasn't changed or the adapter has stable IDs, views 7210 * with transient state will be preserved for later retrieval. 7211 * 7212 * @param scrap The view to add 7213 * @param position The view's position within its parent 7214 */ addScrapView(View scrap, int position)7215 void addScrapView(View scrap, int position) { 7216 final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams(); 7217 if (lp == null) { 7218 // Can't recycle, but we don't know anything about the view. 7219 // Ignore it completely. 7220 return; 7221 } 7222 7223 lp.scrappedFromPosition = position; 7224 7225 // Remove but don't scrap header or footer views, or views that 7226 // should otherwise not be recycled. 7227 final int viewType = lp.viewType; 7228 if (!shouldRecycleViewType(viewType)) { 7229 // Can't recycle. If it's not a header or footer, which have 7230 // special handling and should be ignored, then skip the scrap 7231 // heap and we'll fully detach the view later. 7232 if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { 7233 getSkippedScrap().add(scrap); 7234 } 7235 return; 7236 } 7237 7238 scrap.dispatchStartTemporaryDetach(); 7239 7240 // The the accessibility state of the view may change while temporary 7241 // detached and we do not allow detached views to fire accessibility 7242 // events. So we are announcing that the subtree changed giving a chance 7243 // to clients holding on to a view in this subtree to refresh it. 7244 notifyViewAccessibilityStateChangedIfNeeded( 7245 AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE); 7246 7247 // Don't scrap views that have transient state. 7248 final boolean scrapHasTransientState = scrap.hasTransientState(); 7249 if (scrapHasTransientState) { 7250 if (mAdapter != null && mAdapterHasStableIds) { 7251 // If the adapter has stable IDs, we can reuse the view for 7252 // the same data. 7253 if (mTransientStateViewsById == null) { 7254 mTransientStateViewsById = new LongSparseArray<>(); 7255 } 7256 mTransientStateViewsById.put(lp.itemId, scrap); 7257 } else if (!mDataChanged) { 7258 // If the data hasn't changed, we can reuse the views at 7259 // their old positions. 7260 if (mTransientStateViews == null) { 7261 mTransientStateViews = new SparseArray<>(); 7262 } 7263 mTransientStateViews.put(position, scrap); 7264 } else { 7265 // Otherwise, we'll have to remove the view and start over. 7266 clearScrapForRebind(scrap); 7267 getSkippedScrap().add(scrap); 7268 } 7269 } else { 7270 clearScrapForRebind(scrap); 7271 if (mViewTypeCount == 1) { 7272 mCurrentScrap.add(scrap); 7273 } else { 7274 mScrapViews[viewType].add(scrap); 7275 } 7276 7277 if (mRecyclerListener != null) { 7278 mRecyclerListener.onMovedToScrapHeap(scrap); 7279 } 7280 } 7281 } 7282 getSkippedScrap()7283 private ArrayList<View> getSkippedScrap() { 7284 if (mSkippedScrap == null) { 7285 mSkippedScrap = new ArrayList<>(); 7286 } 7287 return mSkippedScrap; 7288 } 7289 7290 /** 7291 * Finish the removal of any views that skipped the scrap heap. 7292 */ removeSkippedScrap()7293 void removeSkippedScrap() { 7294 if (mSkippedScrap == null) { 7295 return; 7296 } 7297 final int count = mSkippedScrap.size(); 7298 for (int i = 0; i < count; i++) { 7299 removeDetachedView(mSkippedScrap.get(i), false); 7300 } 7301 mSkippedScrap.clear(); 7302 } 7303 7304 /** 7305 * Move all views remaining in mActiveViews to mScrapViews. 7306 */ scrapActiveViews()7307 void scrapActiveViews() { 7308 final View[] activeViews = mActiveViews; 7309 final boolean hasListener = mRecyclerListener != null; 7310 final boolean multipleScraps = mViewTypeCount > 1; 7311 7312 ArrayList<View> scrapViews = mCurrentScrap; 7313 final int count = activeViews.length; 7314 for (int i = count - 1; i >= 0; i--) { 7315 final View victim = activeViews[i]; 7316 if (victim != null) { 7317 final AbsListView.LayoutParams lp 7318 = (AbsListView.LayoutParams) victim.getLayoutParams(); 7319 final int whichScrap = lp.viewType; 7320 7321 activeViews[i] = null; 7322 7323 if (victim.hasTransientState()) { 7324 // Store views with transient state for later use. 7325 victim.dispatchStartTemporaryDetach(); 7326 7327 if (mAdapter != null && mAdapterHasStableIds) { 7328 if (mTransientStateViewsById == null) { 7329 mTransientStateViewsById = new LongSparseArray<View>(); 7330 } 7331 long id = mAdapter.getItemId(mFirstActivePosition + i); 7332 mTransientStateViewsById.put(id, victim); 7333 } else if (!mDataChanged) { 7334 if (mTransientStateViews == null) { 7335 mTransientStateViews = new SparseArray<View>(); 7336 } 7337 mTransientStateViews.put(mFirstActivePosition + i, victim); 7338 } else if (whichScrap != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { 7339 // The data has changed, we can't keep this view. 7340 removeDetachedView(victim, false); 7341 } 7342 } else if (!shouldRecycleViewType(whichScrap)) { 7343 // Discard non-recyclable views except headers/footers. 7344 if (whichScrap != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { 7345 removeDetachedView(victim, false); 7346 } 7347 } else { 7348 // Store everything else on the appropriate scrap heap. 7349 if (multipleScraps) { 7350 scrapViews = mScrapViews[whichScrap]; 7351 } 7352 7353 lp.scrappedFromPosition = mFirstActivePosition + i; 7354 removeDetachedView(victim, false); 7355 scrapViews.add(victim); 7356 7357 if (hasListener) { 7358 mRecyclerListener.onMovedToScrapHeap(victim); 7359 } 7360 } 7361 } 7362 } 7363 pruneScrapViews(); 7364 } 7365 7366 /** 7367 * At the end of a layout pass, all temp detached views should either be re-attached or 7368 * completely detached. This method ensures that any remaining view in the scrap list is 7369 * fully detached. 7370 */ fullyDetachScrapViews()7371 void fullyDetachScrapViews() { 7372 final int viewTypeCount = mViewTypeCount; 7373 final ArrayList<View>[] scrapViews = mScrapViews; 7374 for (int i = 0; i < viewTypeCount; ++i) { 7375 final ArrayList<View> scrapPile = scrapViews[i]; 7376 for (int j = scrapPile.size() - 1; j >= 0; j--) { 7377 final View view = scrapPile.get(j); 7378 if (view.isTemporarilyDetached()) { 7379 removeDetachedView(view, false); 7380 } 7381 } 7382 } 7383 } 7384 7385 /** 7386 * Makes sure that the size of mScrapViews does not exceed the size of 7387 * mActiveViews, which can happen if an adapter does not recycle its 7388 * views. Removes cached transient state views that no longer have 7389 * transient state. 7390 */ pruneScrapViews()7391 private void pruneScrapViews() { 7392 final int maxViews = mActiveViews.length; 7393 final int viewTypeCount = mViewTypeCount; 7394 final ArrayList<View>[] scrapViews = mScrapViews; 7395 for (int i = 0; i < viewTypeCount; ++i) { 7396 final ArrayList<View> scrapPile = scrapViews[i]; 7397 int size = scrapPile.size(); 7398 while (size > maxViews) { 7399 scrapPile.remove(--size); 7400 } 7401 } 7402 7403 final SparseArray<View> transViewsByPos = mTransientStateViews; 7404 if (transViewsByPos != null) { 7405 for (int i = 0; i < transViewsByPos.size(); i++) { 7406 final View v = transViewsByPos.valueAt(i); 7407 if (!v.hasTransientState()) { 7408 removeDetachedView(v, false); 7409 transViewsByPos.removeAt(i); 7410 i--; 7411 } 7412 } 7413 } 7414 7415 final LongSparseArray<View> transViewsById = mTransientStateViewsById; 7416 if (transViewsById != null) { 7417 for (int i = 0; i < transViewsById.size(); i++) { 7418 final View v = transViewsById.valueAt(i); 7419 if (!v.hasTransientState()) { 7420 removeDetachedView(v, false); 7421 transViewsById.removeAt(i); 7422 i--; 7423 } 7424 } 7425 } 7426 } 7427 7428 /** 7429 * Puts all views in the scrap heap into the supplied list. 7430 */ reclaimScrapViews(List<View> views)7431 void reclaimScrapViews(List<View> views) { 7432 if (mViewTypeCount == 1) { 7433 views.addAll(mCurrentScrap); 7434 } else { 7435 final int viewTypeCount = mViewTypeCount; 7436 final ArrayList<View>[] scrapViews = mScrapViews; 7437 for (int i = 0; i < viewTypeCount; ++i) { 7438 final ArrayList<View> scrapPile = scrapViews[i]; 7439 views.addAll(scrapPile); 7440 } 7441 } 7442 } 7443 7444 /** 7445 * Updates the cache color hint of all known views. 7446 * 7447 * @param color The new cache color hint. 7448 */ setCacheColorHint(int color)7449 void setCacheColorHint(int color) { 7450 if (mViewTypeCount == 1) { 7451 final ArrayList<View> scrap = mCurrentScrap; 7452 final int scrapCount = scrap.size(); 7453 for (int i = 0; i < scrapCount; i++) { 7454 scrap.get(i).setDrawingCacheBackgroundColor(color); 7455 } 7456 } else { 7457 final int typeCount = mViewTypeCount; 7458 for (int i = 0; i < typeCount; i++) { 7459 final ArrayList<View> scrap = mScrapViews[i]; 7460 final int scrapCount = scrap.size(); 7461 for (int j = 0; j < scrapCount; j++) { 7462 scrap.get(j).setDrawingCacheBackgroundColor(color); 7463 } 7464 } 7465 } 7466 // Just in case this is called during a layout pass 7467 final View[] activeViews = mActiveViews; 7468 final int count = activeViews.length; 7469 for (int i = 0; i < count; ++i) { 7470 final View victim = activeViews[i]; 7471 if (victim != null) { 7472 victim.setDrawingCacheBackgroundColor(color); 7473 } 7474 } 7475 } 7476 retrieveFromScrap(ArrayList<View> scrapViews, int position)7477 private View retrieveFromScrap(ArrayList<View> scrapViews, int position) { 7478 final int size = scrapViews.size(); 7479 if (size > 0) { 7480 // See if we still have a view for this position or ID. 7481 // Traverse backwards to find the most recently used scrap view 7482 for (int i = size - 1; i >= 0; i--) { 7483 final View view = scrapViews.get(i); 7484 final AbsListView.LayoutParams params = 7485 (AbsListView.LayoutParams) view.getLayoutParams(); 7486 7487 if (mAdapterHasStableIds) { 7488 final long id = mAdapter.getItemId(position); 7489 if (id == params.itemId) { 7490 return scrapViews.remove(i); 7491 } 7492 } else if (params.scrappedFromPosition == position) { 7493 final View scrap = scrapViews.remove(i); 7494 clearScrapForRebind(scrap); 7495 return scrap; 7496 } 7497 } 7498 final View scrap = scrapViews.remove(size - 1); 7499 clearScrapForRebind(scrap); 7500 return scrap; 7501 } else { 7502 return null; 7503 } 7504 } 7505 clearScrap(final ArrayList<View> scrap)7506 private void clearScrap(final ArrayList<View> scrap) { 7507 final int scrapCount = scrap.size(); 7508 for (int j = 0; j < scrapCount; j++) { 7509 removeDetachedView(scrap.remove(scrapCount - 1 - j), false); 7510 } 7511 } 7512 clearScrapForRebind(View view)7513 private void clearScrapForRebind(View view) { 7514 view.clearAccessibilityFocus(); 7515 view.setAccessibilityDelegate(null); 7516 view.resetSubtreeAutofillIds(); 7517 } 7518 removeDetachedView(View child, boolean animate)7519 private void removeDetachedView(View child, boolean animate) { 7520 child.setAccessibilityDelegate(null); 7521 child.resetSubtreeAutofillIds(); 7522 AbsListView.this.removeDetachedView(child, animate); 7523 } 7524 } 7525 7526 /** 7527 * Returns the height of the view for the specified position. 7528 * 7529 * @param position the item position 7530 * @return view height in pixels 7531 */ getHeightForPosition(int position)7532 int getHeightForPosition(int position) { 7533 final int firstVisiblePosition = getFirstVisiblePosition(); 7534 final int childCount = getChildCount(); 7535 final int index = position - firstVisiblePosition; 7536 if (index >= 0 && index < childCount) { 7537 // Position is on-screen, use existing view. 7538 final View view = getChildAt(index); 7539 return view.getHeight(); 7540 } else { 7541 // Position is off-screen, obtain & recycle view. 7542 final View view = obtainView(position, mIsScrap); 7543 view.measure(mWidthMeasureSpec, MeasureSpec.UNSPECIFIED); 7544 final int height = view.getMeasuredHeight(); 7545 mRecycler.addScrapView(view, position); 7546 return height; 7547 } 7548 } 7549 7550 /** 7551 * Sets the selected item and positions the selection y pixels from the top edge 7552 * of the ListView. (If in touch mode, the item will not be selected but it will 7553 * still be positioned appropriately.) 7554 * 7555 * @param position Index (starting at 0) of the data item to be selected. 7556 * @param y The distance from the top edge of the ListView (plus padding) that the 7557 * item will be positioned. 7558 */ setSelectionFromTop(int position, int y)7559 public void setSelectionFromTop(int position, int y) { 7560 if (mAdapter == null) { 7561 return; 7562 } 7563 7564 if (!isInTouchMode()) { 7565 position = lookForSelectablePosition(position, true); 7566 if (position >= 0) { 7567 setNextSelectedPositionInt(position); 7568 } 7569 } else { 7570 mResurrectToPosition = position; 7571 } 7572 7573 if (position >= 0) { 7574 mLayoutMode = LAYOUT_SPECIFIC; 7575 mSpecificTop = mListPadding.top + y; 7576 7577 if (mNeedSync) { 7578 mSyncPosition = position; 7579 mSyncRowId = mAdapter.getItemId(position); 7580 } 7581 7582 if (mPositionScroller != null) { 7583 mPositionScroller.stop(); 7584 } 7585 requestLayout(); 7586 } 7587 } 7588 7589 /** @hide */ 7590 @Override encodeProperties(@onNull ViewHierarchyEncoder encoder)7591 protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) { 7592 super.encodeProperties(encoder); 7593 7594 encoder.addProperty("drawing:cacheColorHint", getCacheColorHint()); 7595 encoder.addProperty("list:fastScrollEnabled", isFastScrollEnabled()); 7596 encoder.addProperty("list:scrollingCacheEnabled", isScrollingCacheEnabled()); 7597 encoder.addProperty("list:smoothScrollbarEnabled", isSmoothScrollbarEnabled()); 7598 encoder.addProperty("list:stackFromBottom", isStackFromBottom()); 7599 encoder.addProperty("list:textFilterEnabled", isTextFilterEnabled()); 7600 7601 View selectedView = getSelectedView(); 7602 if (selectedView != null) { 7603 encoder.addPropertyKey("selectedView"); 7604 selectedView.encode(encoder); 7605 } 7606 } 7607 7608 /** 7609 * Abstract positon scroller used to handle smooth scrolling. 7610 */ 7611 static abstract class AbsPositionScroller { 7612 public abstract void start(int position); 7613 public abstract void start(int position, int boundPosition); 7614 public abstract void startWithOffset(int position, int offset); 7615 public abstract void startWithOffset(int position, int offset, int duration); 7616 public abstract void stop(); 7617 } 7618 7619 /** 7620 * Default position scroller that simulates a fling. 7621 */ 7622 class PositionScroller extends AbsPositionScroller implements Runnable { 7623 private static final int SCROLL_DURATION = 200; 7624 7625 private static final int MOVE_DOWN_POS = 1; 7626 private static final int MOVE_UP_POS = 2; 7627 private static final int MOVE_DOWN_BOUND = 3; 7628 private static final int MOVE_UP_BOUND = 4; 7629 private static final int MOVE_OFFSET = 5; 7630 7631 private int mMode; 7632 private int mTargetPos; 7633 private int mBoundPos; 7634 private int mLastSeenPos; 7635 private int mScrollDuration; 7636 private final int mExtraScroll; 7637 7638 private int mOffsetFromTop; 7639 PositionScroller()7640 PositionScroller() { 7641 mExtraScroll = ViewConfiguration.get(mContext).getScaledFadingEdgeLength(); 7642 } 7643 7644 @Override start(final int position)7645 public void start(final int position) { 7646 stop(); 7647 7648 if (mDataChanged) { 7649 // Wait until we're back in a stable state to try this. 7650 mPositionScrollAfterLayout = new Runnable() { 7651 @Override public void run() { 7652 start(position); 7653 } 7654 }; 7655 return; 7656 } 7657 7658 final int childCount = getChildCount(); 7659 if (childCount == 0) { 7660 // Can't scroll without children. 7661 return; 7662 } 7663 7664 final int firstPos = mFirstPosition; 7665 final int lastPos = firstPos + childCount - 1; 7666 7667 int viewTravelCount; 7668 int clampedPosition = Math.max(0, Math.min(getCount() - 1, position)); 7669 if (clampedPosition < firstPos) { 7670 viewTravelCount = firstPos - clampedPosition + 1; 7671 mMode = MOVE_UP_POS; 7672 } else if (clampedPosition > lastPos) { 7673 viewTravelCount = clampedPosition - lastPos + 1; 7674 mMode = MOVE_DOWN_POS; 7675 } else { 7676 scrollToVisible(clampedPosition, INVALID_POSITION, SCROLL_DURATION); 7677 return; 7678 } 7679 7680 if (viewTravelCount > 0) { 7681 mScrollDuration = SCROLL_DURATION / viewTravelCount; 7682 } else { 7683 mScrollDuration = SCROLL_DURATION; 7684 } 7685 mTargetPos = clampedPosition; 7686 mBoundPos = INVALID_POSITION; 7687 mLastSeenPos = INVALID_POSITION; 7688 7689 postOnAnimation(this); 7690 } 7691 7692 @Override start(final int position, final int boundPosition)7693 public void start(final int position, final int boundPosition) { 7694 stop(); 7695 7696 if (boundPosition == INVALID_POSITION) { 7697 start(position); 7698 return; 7699 } 7700 7701 if (mDataChanged) { 7702 // Wait until we're back in a stable state to try this. 7703 mPositionScrollAfterLayout = new Runnable() { 7704 @Override public void run() { 7705 start(position, boundPosition); 7706 } 7707 }; 7708 return; 7709 } 7710 7711 final int childCount = getChildCount(); 7712 if (childCount == 0) { 7713 // Can't scroll without children. 7714 return; 7715 } 7716 7717 final int firstPos = mFirstPosition; 7718 final int lastPos = firstPos + childCount - 1; 7719 7720 int viewTravelCount; 7721 int clampedPosition = Math.max(0, Math.min(getCount() - 1, position)); 7722 if (clampedPosition < firstPos) { 7723 final int boundPosFromLast = lastPos - boundPosition; 7724 if (boundPosFromLast < 1) { 7725 // Moving would shift our bound position off the screen. Abort. 7726 return; 7727 } 7728 7729 final int posTravel = firstPos - clampedPosition + 1; 7730 final int boundTravel = boundPosFromLast - 1; 7731 if (boundTravel < posTravel) { 7732 viewTravelCount = boundTravel; 7733 mMode = MOVE_UP_BOUND; 7734 } else { 7735 viewTravelCount = posTravel; 7736 mMode = MOVE_UP_POS; 7737 } 7738 } else if (clampedPosition > lastPos) { 7739 final int boundPosFromFirst = boundPosition - firstPos; 7740 if (boundPosFromFirst < 1) { 7741 // Moving would shift our bound position off the screen. Abort. 7742 return; 7743 } 7744 7745 final int posTravel = clampedPosition - lastPos + 1; 7746 final int boundTravel = boundPosFromFirst - 1; 7747 if (boundTravel < posTravel) { 7748 viewTravelCount = boundTravel; 7749 mMode = MOVE_DOWN_BOUND; 7750 } else { 7751 viewTravelCount = posTravel; 7752 mMode = MOVE_DOWN_POS; 7753 } 7754 } else { 7755 scrollToVisible(clampedPosition, boundPosition, SCROLL_DURATION); 7756 return; 7757 } 7758 7759 if (viewTravelCount > 0) { 7760 mScrollDuration = SCROLL_DURATION / viewTravelCount; 7761 } else { 7762 mScrollDuration = SCROLL_DURATION; 7763 } 7764 mTargetPos = clampedPosition; 7765 mBoundPos = boundPosition; 7766 mLastSeenPos = INVALID_POSITION; 7767 7768 postOnAnimation(this); 7769 } 7770 7771 @Override startWithOffset(int position, int offset)7772 public void startWithOffset(int position, int offset) { 7773 startWithOffset(position, offset, SCROLL_DURATION); 7774 } 7775 7776 @Override startWithOffset(final int position, int offset, final int duration)7777 public void startWithOffset(final int position, int offset, final int duration) { 7778 stop(); 7779 7780 if (mDataChanged) { 7781 // Wait until we're back in a stable state to try this. 7782 final int postOffset = offset; 7783 mPositionScrollAfterLayout = new Runnable() { 7784 @Override public void run() { 7785 startWithOffset(position, postOffset, duration); 7786 } 7787 }; 7788 return; 7789 } 7790 7791 final int childCount = getChildCount(); 7792 if (childCount == 0) { 7793 // Can't scroll without children. 7794 return; 7795 } 7796 7797 offset += getPaddingTop(); 7798 7799 mTargetPos = Math.max(0, Math.min(getCount() - 1, position)); 7800 mOffsetFromTop = offset; 7801 mBoundPos = INVALID_POSITION; 7802 mLastSeenPos = INVALID_POSITION; 7803 mMode = MOVE_OFFSET; 7804 7805 final int firstPos = mFirstPosition; 7806 final int lastPos = firstPos + childCount - 1; 7807 7808 int viewTravelCount; 7809 if (mTargetPos < firstPos) { 7810 viewTravelCount = firstPos - mTargetPos; 7811 } else if (mTargetPos > lastPos) { 7812 viewTravelCount = mTargetPos - lastPos; 7813 } else { 7814 // On-screen, just scroll. 7815 final int targetTop = getChildAt(mTargetPos - firstPos).getTop(); 7816 smoothScrollBy(targetTop - offset, duration, true, false); 7817 return; 7818 } 7819 7820 // Estimate how many screens we should travel 7821 final float screenTravelCount = (float) viewTravelCount / childCount; 7822 mScrollDuration = screenTravelCount < 1 ? 7823 duration : (int) (duration / screenTravelCount); 7824 mLastSeenPos = INVALID_POSITION; 7825 7826 postOnAnimation(this); 7827 } 7828 7829 /** 7830 * Scroll such that targetPos is in the visible padded region without scrolling 7831 * boundPos out of view. Assumes targetPos is onscreen. 7832 */ 7833 private void scrollToVisible(int targetPos, int boundPos, int duration) { 7834 final int firstPos = mFirstPosition; 7835 final int childCount = getChildCount(); 7836 final int lastPos = firstPos + childCount - 1; 7837 final int paddedTop = mListPadding.top; 7838 final int paddedBottom = getHeight() - mListPadding.bottom; 7839 7840 if (targetPos < firstPos || targetPos > lastPos) { 7841 Log.w(TAG, "scrollToVisible called with targetPos " + targetPos + 7842 " not visible [" + firstPos + ", " + lastPos + "]"); 7843 } 7844 if (boundPos < firstPos || boundPos > lastPos) { 7845 // boundPos doesn't matter, it's already offscreen. 7846 boundPos = INVALID_POSITION; 7847 } 7848 7849 final View targetChild = getChildAt(targetPos - firstPos); 7850 final int targetTop = targetChild.getTop(); 7851 final int targetBottom = targetChild.getBottom(); 7852 int scrollBy = 0; 7853 7854 if (targetBottom > paddedBottom) { 7855 scrollBy = targetBottom - paddedBottom; 7856 } 7857 if (targetTop < paddedTop) { 7858 scrollBy = targetTop - paddedTop; 7859 } 7860 7861 if (scrollBy == 0) { 7862 return; 7863 } 7864 7865 if (boundPos >= 0) { 7866 final View boundChild = getChildAt(boundPos - firstPos); 7867 final int boundTop = boundChild.getTop(); 7868 final int boundBottom = boundChild.getBottom(); 7869 final int absScroll = Math.abs(scrollBy); 7870 7871 if (scrollBy < 0 && boundBottom + absScroll > paddedBottom) { 7872 // Don't scroll the bound view off the bottom of the screen. 7873 scrollBy = Math.max(0, boundBottom - paddedBottom); 7874 } else if (scrollBy > 0 && boundTop - absScroll < paddedTop) { 7875 // Don't scroll the bound view off the top of the screen. 7876 scrollBy = Math.min(0, boundTop - paddedTop); 7877 } 7878 } 7879 7880 smoothScrollBy(scrollBy, duration); 7881 } 7882 7883 @Override stop()7884 public void stop() { 7885 removeCallbacks(this); 7886 } 7887 7888 @Override run()7889 public void run() { 7890 final int listHeight = getHeight(); 7891 final int firstPos = mFirstPosition; 7892 7893 switch (mMode) { 7894 case MOVE_DOWN_POS: { 7895 final int lastViewIndex = getChildCount() - 1; 7896 final int lastPos = firstPos + lastViewIndex; 7897 7898 if (lastViewIndex < 0) { 7899 return; 7900 } 7901 7902 if (lastPos == mLastSeenPos) { 7903 // No new views, let things keep going. 7904 postOnAnimation(this); 7905 return; 7906 } 7907 7908 final View lastView = getChildAt(lastViewIndex); 7909 final int lastViewHeight = lastView.getHeight(); 7910 final int lastViewTop = lastView.getTop(); 7911 final int lastViewPixelsShowing = listHeight - lastViewTop; 7912 final int extraScroll = lastPos < mItemCount - 1 ? 7913 Math.max(mListPadding.bottom, mExtraScroll) : mListPadding.bottom; 7914 7915 final int scrollBy = lastViewHeight - lastViewPixelsShowing + extraScroll; 7916 smoothScrollBy(scrollBy, mScrollDuration, true, lastPos < mTargetPos); 7917 7918 mLastSeenPos = lastPos; 7919 if (lastPos < mTargetPos) { 7920 postOnAnimation(this); 7921 } 7922 break; 7923 } 7924 7925 case MOVE_DOWN_BOUND: { 7926 final int nextViewIndex = 1; 7927 final int childCount = getChildCount(); 7928 7929 if (firstPos == mBoundPos || childCount <= nextViewIndex 7930 || firstPos + childCount >= mItemCount) { 7931 reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); 7932 return; 7933 } 7934 final int nextPos = firstPos + nextViewIndex; 7935 7936 if (nextPos == mLastSeenPos) { 7937 // No new views, let things keep going. 7938 postOnAnimation(this); 7939 return; 7940 } 7941 7942 final View nextView = getChildAt(nextViewIndex); 7943 final int nextViewHeight = nextView.getHeight(); 7944 final int nextViewTop = nextView.getTop(); 7945 final int extraScroll = Math.max(mListPadding.bottom, mExtraScroll); 7946 if (nextPos < mBoundPos) { 7947 smoothScrollBy(Math.max(0, nextViewHeight + nextViewTop - extraScroll), 7948 mScrollDuration, true, true); 7949 7950 mLastSeenPos = nextPos; 7951 7952 postOnAnimation(this); 7953 } else { 7954 if (nextViewTop > extraScroll) { 7955 smoothScrollBy(nextViewTop - extraScroll, mScrollDuration, true, false); 7956 } else { 7957 reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); 7958 } 7959 } 7960 break; 7961 } 7962 7963 case MOVE_UP_POS: { 7964 if (firstPos == mLastSeenPos) { 7965 // No new views, let things keep going. 7966 postOnAnimation(this); 7967 return; 7968 } 7969 7970 final View firstView = getChildAt(0); 7971 if (firstView == null) { 7972 return; 7973 } 7974 final int firstViewTop = firstView.getTop(); 7975 final int extraScroll = firstPos > 0 ? 7976 Math.max(mExtraScroll, mListPadding.top) : mListPadding.top; 7977 7978 smoothScrollBy(firstViewTop - extraScroll, mScrollDuration, true, 7979 firstPos > mTargetPos); 7980 7981 mLastSeenPos = firstPos; 7982 7983 if (firstPos > mTargetPos) { 7984 postOnAnimation(this); 7985 } 7986 break; 7987 } 7988 7989 case MOVE_UP_BOUND: { 7990 final int lastViewIndex = getChildCount() - 2; 7991 if (lastViewIndex < 0) { 7992 return; 7993 } 7994 final int lastPos = firstPos + lastViewIndex; 7995 7996 if (lastPos == mLastSeenPos) { 7997 // No new views, let things keep going. 7998 postOnAnimation(this); 7999 return; 8000 } 8001 8002 final View lastView = getChildAt(lastViewIndex); 8003 final int lastViewHeight = lastView.getHeight(); 8004 final int lastViewTop = lastView.getTop(); 8005 final int lastViewPixelsShowing = listHeight - lastViewTop; 8006 final int extraScroll = Math.max(mListPadding.top, mExtraScroll); 8007 mLastSeenPos = lastPos; 8008 if (lastPos > mBoundPos) { 8009 smoothScrollBy(-(lastViewPixelsShowing - extraScroll), mScrollDuration, true, 8010 true); 8011 postOnAnimation(this); 8012 } else { 8013 final int bottom = listHeight - extraScroll; 8014 final int lastViewBottom = lastViewTop + lastViewHeight; 8015 if (bottom > lastViewBottom) { 8016 smoothScrollBy(-(bottom - lastViewBottom), mScrollDuration, true, false); 8017 } else { 8018 reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); 8019 } 8020 } 8021 break; 8022 } 8023 8024 case MOVE_OFFSET: { 8025 if (mLastSeenPos == firstPos) { 8026 // No new views, let things keep going. 8027 postOnAnimation(this); 8028 return; 8029 } 8030 8031 mLastSeenPos = firstPos; 8032 8033 final int childCount = getChildCount(); 8034 8035 if (childCount <= 0) { 8036 return; 8037 } 8038 8039 final int position = mTargetPos; 8040 final int lastPos = firstPos + childCount - 1; 8041 8042 // Account for the visible "portion" of the first / last child when we estimate 8043 // how many screens we should travel to reach our target 8044 final View firstChild = getChildAt(0); 8045 final int firstChildHeight = firstChild.getHeight(); 8046 final View lastChild = getChildAt(childCount - 1); 8047 final int lastChildHeight = lastChild.getHeight(); 8048 final float firstPositionVisiblePart = (firstChildHeight == 0.0f) ? 1.0f 8049 : (float) (firstChildHeight + firstChild.getTop()) / firstChildHeight; 8050 final float lastPositionVisiblePart = (lastChildHeight == 0.0f) ? 1.0f 8051 : (float) (lastChildHeight + getHeight() - lastChild.getBottom()) 8052 / lastChildHeight; 8053 8054 float viewTravelCount = 0; 8055 if (position < firstPos) { 8056 viewTravelCount = firstPos - position + (1.0f - firstPositionVisiblePart) + 1; 8057 } else if (position > lastPos) { 8058 viewTravelCount = position - lastPos + (1.0f - lastPositionVisiblePart); 8059 } 8060 8061 // Estimate how many screens we should travel 8062 final float screenTravelCount = viewTravelCount / childCount; 8063 8064 final float modifier = Math.min(Math.abs(screenTravelCount), 1.f); 8065 if (position < firstPos) { 8066 final int distance = (int) (-getHeight() * modifier); 8067 final int duration = (int) (mScrollDuration * modifier); 8068 smoothScrollBy(distance, duration, true, true); 8069 postOnAnimation(this); 8070 } else if (position > lastPos) { 8071 final int distance = (int) (getHeight() * modifier); 8072 final int duration = (int) (mScrollDuration * modifier); 8073 smoothScrollBy(distance, duration, true, true); 8074 postOnAnimation(this); 8075 } else { 8076 // On-screen, just scroll. 8077 final int targetTop = getChildAt(position - firstPos).getTop(); 8078 final int distance = targetTop - mOffsetFromTop; 8079 final int duration = (int) (mScrollDuration * 8080 ((float) Math.abs(distance) / getHeight())); 8081 smoothScrollBy(distance, duration, true, false); 8082 } 8083 break; 8084 } 8085 8086 default: 8087 break; 8088 } 8089 } 8090 } 8091 } 8092