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