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 com.android.internal.R; 20 21 import android.content.Context; 22 import android.content.res.Resources; 23 import android.content.res.TypedArray; 24 import android.graphics.Canvas; 25 import android.graphics.Rect; 26 import android.graphics.drawable.Drawable; 27 import android.graphics.drawable.TransitionDrawable; 28 import android.os.Debug; 29 import android.os.Handler; 30 import android.os.Parcel; 31 import android.os.Parcelable; 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.view.ContextMenu.ContextMenuInfo; 38 import android.view.Gravity; 39 import android.view.HapticFeedbackConstants; 40 import android.view.KeyEvent; 41 import android.view.LayoutInflater; 42 import android.view.MotionEvent; 43 import android.view.VelocityTracker; 44 import android.view.View; 45 import android.view.ViewConfiguration; 46 import android.view.ViewDebug; 47 import android.view.ViewGroup; 48 import android.view.ViewTreeObserver; 49 import android.view.animation.AnimationUtils; 50 import android.view.inputmethod.BaseInputConnection; 51 import android.view.inputmethod.EditorInfo; 52 import android.view.inputmethod.InputConnection; 53 import android.view.inputmethod.InputConnectionWrapper; 54 import android.view.inputmethod.InputMethodManager; 55 56 import java.util.ArrayList; 57 import java.util.List; 58 59 /** 60 * Base class that can be used to implement virtualized lists of items. A list does 61 * not have a spatial definition here. For instance, subclases of this class can 62 * display the content of the list in a grid, in a carousel, as stack, etc. 63 * 64 * @attr ref android.R.styleable#AbsListView_listSelector 65 * @attr ref android.R.styleable#AbsListView_drawSelectorOnTop 66 * @attr ref android.R.styleable#AbsListView_stackFromBottom 67 * @attr ref android.R.styleable#AbsListView_scrollingCache 68 * @attr ref android.R.styleable#AbsListView_textFilterEnabled 69 * @attr ref android.R.styleable#AbsListView_transcriptMode 70 * @attr ref android.R.styleable#AbsListView_cacheColorHint 71 * @attr ref android.R.styleable#AbsListView_fastScrollEnabled 72 * @attr ref android.R.styleable#AbsListView_smoothScrollbar 73 */ 74 public abstract class AbsListView extends AdapterView<ListAdapter> implements TextWatcher, 75 ViewTreeObserver.OnGlobalLayoutListener, Filter.FilterListener, 76 ViewTreeObserver.OnTouchModeChangeListener { 77 78 /** 79 * Disables the transcript mode. 80 * 81 * @see #setTranscriptMode(int) 82 */ 83 public static final int TRANSCRIPT_MODE_DISABLED = 0; 84 /** 85 * The list will automatically scroll to the bottom when a data set change 86 * notification is received and only if the last item is already visible 87 * on screen. 88 * 89 * @see #setTranscriptMode(int) 90 */ 91 public static final int TRANSCRIPT_MODE_NORMAL = 1; 92 /** 93 * The list will automatically scroll to the bottom, no matter what items 94 * are currently visible. 95 * 96 * @see #setTranscriptMode(int) 97 */ 98 public static final int TRANSCRIPT_MODE_ALWAYS_SCROLL = 2; 99 100 /** 101 * Indicates that we are not in the middle of a touch gesture 102 */ 103 static final int TOUCH_MODE_REST = -1; 104 105 /** 106 * Indicates we just received the touch event and we are waiting to see if the it is a tap or a 107 * scroll gesture. 108 */ 109 static final int TOUCH_MODE_DOWN = 0; 110 111 /** 112 * Indicates the touch has been recognized as a tap and we are now waiting to see if the touch 113 * is a longpress 114 */ 115 static final int TOUCH_MODE_TAP = 1; 116 117 /** 118 * Indicates we have waited for everything we can wait for, but the user's finger is still down 119 */ 120 static final int TOUCH_MODE_DONE_WAITING = 2; 121 122 /** 123 * Indicates the touch gesture is a scroll 124 */ 125 static final int TOUCH_MODE_SCROLL = 3; 126 127 /** 128 * Indicates the view is in the process of being flung 129 */ 130 static final int TOUCH_MODE_FLING = 4; 131 132 /** 133 * Indicates the touch gesture is an overscroll - a scroll beyond the beginning or end. 134 */ 135 static final int TOUCH_MODE_OVERSCROLL = 5; 136 137 /** 138 * Indicates the view is being flung outside of normal content bounds 139 * and will spring back. 140 */ 141 static final int TOUCH_MODE_OVERFLING = 6; 142 143 /** 144 * Regular layout - usually an unsolicited layout from the view system 145 */ 146 static final int LAYOUT_NORMAL = 0; 147 148 /** 149 * Show the first item 150 */ 151 static final int LAYOUT_FORCE_TOP = 1; 152 153 /** 154 * Force the selected item to be on somewhere on the screen 155 */ 156 static final int LAYOUT_SET_SELECTION = 2; 157 158 /** 159 * Show the last item 160 */ 161 static final int LAYOUT_FORCE_BOTTOM = 3; 162 163 /** 164 * Make a mSelectedItem appear in a specific location and build the rest of 165 * the views from there. The top is specified by mSpecificTop. 166 */ 167 static final int LAYOUT_SPECIFIC = 4; 168 169 /** 170 * Layout to sync as a result of a data change. Restore mSyncPosition to have its top 171 * at mSpecificTop 172 */ 173 static final int LAYOUT_SYNC = 5; 174 175 /** 176 * Layout as a result of using the navigation keys 177 */ 178 static final int LAYOUT_MOVE_SELECTION = 6; 179 180 /** 181 * Controls how the next layout will happen 182 */ 183 int mLayoutMode = LAYOUT_NORMAL; 184 185 /** 186 * Should be used by subclasses to listen to changes in the dataset 187 */ 188 AdapterDataSetObserver mDataSetObserver; 189 190 /** 191 * The adapter containing the data to be displayed by this view 192 */ 193 ListAdapter mAdapter; 194 195 /** 196 * Indicates whether the list selector should be drawn on top of the children or behind 197 */ 198 boolean mDrawSelectorOnTop = false; 199 200 /** 201 * The drawable used to draw the selector 202 */ 203 Drawable mSelector; 204 205 /** 206 * Defines the selector's location and dimension at drawing time 207 */ 208 Rect mSelectorRect = new Rect(); 209 210 /** 211 * The data set used to store unused views that should be reused during the next layout 212 * to avoid creating new ones 213 */ 214 final RecycleBin mRecycler = new RecycleBin(); 215 216 /** 217 * The selection's left padding 218 */ 219 int mSelectionLeftPadding = 0; 220 221 /** 222 * The selection's top padding 223 */ 224 int mSelectionTopPadding = 0; 225 226 /** 227 * The selection's right padding 228 */ 229 int mSelectionRightPadding = 0; 230 231 /** 232 * The selection's bottom padding 233 */ 234 int mSelectionBottomPadding = 0; 235 236 /** 237 * This view's padding 238 */ 239 Rect mListPadding = new Rect(); 240 241 /** 242 * Subclasses must retain their measure spec from onMeasure() into this member 243 */ 244 int mWidthMeasureSpec = 0; 245 246 /** 247 * The top scroll indicator 248 */ 249 View mScrollUp; 250 251 /** 252 * The down scroll indicator 253 */ 254 View mScrollDown; 255 256 /** 257 * When the view is scrolling, this flag is set to true to indicate subclasses that 258 * the drawing cache was enabled on the children 259 */ 260 boolean mCachingStarted; 261 262 /** 263 * The position of the view that received the down motion event 264 */ 265 int mMotionPosition; 266 267 /** 268 * The offset to the top of the mMotionPosition view when the down motion event was received 269 */ 270 int mMotionViewOriginalTop; 271 272 /** 273 * The desired offset to the top of the mMotionPosition view after a scroll 274 */ 275 int mMotionViewNewTop; 276 277 /** 278 * The X value associated with the the down motion event 279 */ 280 int mMotionX; 281 282 /** 283 * The Y value associated with the the down motion event 284 */ 285 int mMotionY; 286 287 /** 288 * One of TOUCH_MODE_REST, TOUCH_MODE_DOWN, TOUCH_MODE_TAP, TOUCH_MODE_SCROLL, or 289 * TOUCH_MODE_DONE_WAITING 290 */ 291 int mTouchMode = TOUCH_MODE_REST; 292 293 /** 294 * Y value from on the previous motion event (if any) 295 */ 296 int mLastY; 297 298 /** 299 * How far the finger moved before we started scrolling 300 */ 301 int mMotionCorrection; 302 303 /** 304 * Determines speed during touch scrolling 305 */ 306 private VelocityTracker mVelocityTracker; 307 308 /** 309 * Handles one frame of a fling 310 */ 311 private FlingRunnable mFlingRunnable; 312 313 /** 314 * Handles scrolling between positions within the list. 315 */ 316 private PositionScroller mPositionScroller; 317 318 /** 319 * The offset in pixels form the top of the AdapterView to the top 320 * of the currently selected view. Used to save and restore state. 321 */ 322 int mSelectedTop = 0; 323 324 /** 325 * Indicates whether the list is stacked from the bottom edge or 326 * the top edge. 327 */ 328 boolean mStackFromBottom; 329 330 /** 331 * When set to true, the list automatically discards the children's 332 * bitmap cache after scrolling. 333 */ 334 boolean mScrollingCacheEnabled; 335 336 /** 337 * Whether or not to enable the fast scroll feature on this list 338 */ 339 boolean mFastScrollEnabled; 340 341 /** 342 * Optional callback to notify client when scroll position has changed 343 */ 344 private OnScrollListener mOnScrollListener; 345 346 /** 347 * Keeps track of our accessory window 348 */ 349 PopupWindow mPopup; 350 351 /** 352 * Used with type filter window 353 */ 354 EditText mTextFilter; 355 356 /** 357 * Indicates whether to use pixels-based or position-based scrollbar 358 * properties. 359 */ 360 private boolean mSmoothScrollbarEnabled = true; 361 362 /** 363 * Indicates that this view supports filtering 364 */ 365 private boolean mTextFilterEnabled; 366 367 /** 368 * Indicates that this view is currently displaying a filtered view of the data 369 */ 370 private boolean mFiltered; 371 372 /** 373 * Rectangle used for hit testing children 374 */ 375 private Rect mTouchFrame; 376 377 /** 378 * The position to resurrect the selected position to. 379 */ 380 int mResurrectToPosition = INVALID_POSITION; 381 382 private ContextMenuInfo mContextMenuInfo = null; 383 384 /** 385 * Maximum distance to record overscroll 386 */ 387 int mOverscrollMax; 388 389 /** 390 * Content height divided by this is the overscroll limit. 391 */ 392 static final int OVERSCROLL_LIMIT_DIVISOR = 3; 393 394 /** 395 * Used to request a layout when we changed touch mode 396 */ 397 private static final int TOUCH_MODE_UNKNOWN = -1; 398 private static final int TOUCH_MODE_ON = 0; 399 private static final int TOUCH_MODE_OFF = 1; 400 401 private int mLastTouchMode = TOUCH_MODE_UNKNOWN; 402 403 private static final boolean PROFILE_SCROLLING = false; 404 private boolean mScrollProfilingStarted = false; 405 406 private static final boolean PROFILE_FLINGING = false; 407 private boolean mFlingProfilingStarted = false; 408 409 /** 410 * The last CheckForLongPress runnable we posted, if any 411 */ 412 private CheckForLongPress mPendingCheckForLongPress; 413 414 /** 415 * The last CheckForTap runnable we posted, if any 416 */ 417 private Runnable mPendingCheckForTap; 418 419 /** 420 * The last CheckForKeyLongPress runnable we posted, if any 421 */ 422 private CheckForKeyLongPress mPendingCheckForKeyLongPress; 423 424 /** 425 * Acts upon click 426 */ 427 private AbsListView.PerformClick mPerformClick; 428 429 /** 430 * This view is in transcript mode -- it shows the bottom of the list when the data 431 * changes 432 */ 433 private int mTranscriptMode; 434 435 /** 436 * Indicates that this list is always drawn on top of a solid, single-color, opaque 437 * background 438 */ 439 private int mCacheColorHint; 440 441 /** 442 * The select child's view (from the adapter's getView) is enabled. 443 */ 444 private boolean mIsChildViewEnabled; 445 446 /** 447 * The last scroll state reported to clients through {@link OnScrollListener}. 448 */ 449 private int mLastScrollState = OnScrollListener.SCROLL_STATE_IDLE; 450 451 /** 452 * Helper object that renders and controls the fast scroll thumb. 453 */ 454 private FastScroller mFastScroller; 455 456 private boolean mGlobalLayoutListenerAddedFilter; 457 458 private int mTouchSlop; 459 private float mDensityScale; 460 461 private InputConnection mDefInputConnection; 462 private InputConnectionWrapper mPublicInputConnection; 463 464 private Runnable mClearScrollingCache; 465 private int mMinimumVelocity; 466 private int mMaximumVelocity; 467 468 final boolean[] mIsScrap = new boolean[1]; 469 470 // True when the popup should be hidden because of a call to 471 // dispatchDisplayHint() 472 private boolean mPopupHidden; 473 474 /** 475 * ID of the active pointer. This is used to retain consistency during 476 * drags/flings if multiple pointers are used. 477 */ 478 private int mActivePointerId = INVALID_POINTER; 479 480 /** 481 * Sentinel value for no current active pointer. 482 * Used by {@link #mActivePointerId}. 483 */ 484 private static final int INVALID_POINTER = -1; 485 486 /** 487 * Maximum distance to overscroll by during edge effects 488 */ 489 int mOverscrollDistance; 490 491 /** 492 * Maximum distance to overfling during edge effects 493 */ 494 int mOverflingDistance; 495 496 // These two EdgeGlows are always set and used together. 497 // Checking one for null is as good as checking both. 498 499 /** 500 * Tracks the state of the top edge glow. 501 */ 502 private EdgeGlow mEdgeGlowTop; 503 504 /** 505 * Tracks the state of the bottom edge glow. 506 */ 507 private EdgeGlow mEdgeGlowBottom; 508 509 /** 510 * An estimate of how many pixels are between the top of the list and 511 * the top of the first position in the adapter, based on the last time 512 * we saw it. Used to hint where to draw edge glows. 513 */ 514 private int mFirstPositionDistanceGuess; 515 516 /** 517 * An estimate of how many pixels are between the bottom of the list and 518 * the bottom of the last position in the adapter, based on the last time 519 * we saw it. Used to hint where to draw edge glows. 520 */ 521 private int mLastPositionDistanceGuess; 522 523 /** 524 * Used for determining when to cancel out of overscroll. 525 */ 526 private int mDirection = 0; 527 528 /** 529 * Interface definition for a callback to be invoked when the list or grid 530 * has been scrolled. 531 */ 532 public interface OnScrollListener { 533 534 /** 535 * The view is not scrolling. Note navigating the list using the trackball counts as 536 * being in the idle state since these transitions are not animated. 537 */ 538 public static int SCROLL_STATE_IDLE = 0; 539 540 /** 541 * The user is scrolling using touch, and their finger is still on the screen 542 */ 543 public static int SCROLL_STATE_TOUCH_SCROLL = 1; 544 545 /** 546 * The user had previously been scrolling using touch and had performed a fling. The 547 * animation is now coasting to a stop 548 */ 549 public static int SCROLL_STATE_FLING = 2; 550 551 /** 552 * Callback method to be invoked while the list view or grid view is being scrolled. If the 553 * view is being scrolled, this method will be called before the next frame of the scroll is 554 * rendered. In particular, it will be called before any calls to 555 * {@link Adapter#getView(int, View, ViewGroup)}. 556 * 557 * @param view The view whose scroll state is being reported 558 * 559 * @param scrollState The current scroll state. One of {@link #SCROLL_STATE_IDLE}, 560 * {@link #SCROLL_STATE_TOUCH_SCROLL} or {@link #SCROLL_STATE_IDLE}. 561 */ 562 public void onScrollStateChanged(AbsListView view, int scrollState); 563 564 /** 565 * Callback method to be invoked when the list or grid has been scrolled. This will be 566 * called after the scroll has completed 567 * @param view The view whose scroll state is being reported 568 * @param firstVisibleItem the index of the first visible cell (ignore if 569 * visibleItemCount == 0) 570 * @param visibleItemCount the number of visible cells 571 * @param totalItemCount the number of items in the list adaptor 572 */ 573 public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, 574 int totalItemCount); 575 } 576 577 public AbsListView(Context context) { 578 super(context); 579 initAbsListView(); 580 581 setVerticalScrollBarEnabled(true); 582 TypedArray a = context.obtainStyledAttributes(R.styleable.View); 583 initializeScrollbars(a); 584 a.recycle(); 585 } 586 587 public AbsListView(Context context, AttributeSet attrs) { 588 this(context, attrs, com.android.internal.R.attr.absListViewStyle); 589 } 590 591 public AbsListView(Context context, AttributeSet attrs, int defStyle) { 592 super(context, attrs, defStyle); 593 initAbsListView(); 594 595 TypedArray a = context.obtainStyledAttributes(attrs, 596 com.android.internal.R.styleable.AbsListView, defStyle, 0); 597 598 Drawable d = a.getDrawable(com.android.internal.R.styleable.AbsListView_listSelector); 599 if (d != null) { 600 setSelector(d); 601 } 602 603 mDrawSelectorOnTop = a.getBoolean( 604 com.android.internal.R.styleable.AbsListView_drawSelectorOnTop, false); 605 606 boolean stackFromBottom = a.getBoolean(R.styleable.AbsListView_stackFromBottom, false); 607 setStackFromBottom(stackFromBottom); 608 609 boolean scrollingCacheEnabled = a.getBoolean(R.styleable.AbsListView_scrollingCache, true); 610 setScrollingCacheEnabled(scrollingCacheEnabled); 611 612 boolean useTextFilter = a.getBoolean(R.styleable.AbsListView_textFilterEnabled, false); 613 setTextFilterEnabled(useTextFilter); 614 615 int transcriptMode = a.getInt(R.styleable.AbsListView_transcriptMode, 616 TRANSCRIPT_MODE_DISABLED); 617 setTranscriptMode(transcriptMode); 618 619 int color = a.getColor(R.styleable.AbsListView_cacheColorHint, 0); 620 setCacheColorHint(color); 621 622 boolean enableFastScroll = a.getBoolean(R.styleable.AbsListView_fastScrollEnabled, false); 623 setFastScrollEnabled(enableFastScroll); 624 625 boolean smoothScrollbar = a.getBoolean(R.styleable.AbsListView_smoothScrollbar, true); 626 setSmoothScrollbarEnabled(smoothScrollbar); 627 628 a.recycle(); 629 } 630 631 private void initAbsListView() { 632 // Setting focusable in touch mode will set the focusable property to true 633 setClickable(true); 634 setFocusableInTouchMode(true); 635 setWillNotDraw(false); 636 setAlwaysDrawnWithCacheEnabled(false); 637 setScrollingCacheEnabled(true); 638 639 final ViewConfiguration configuration = ViewConfiguration.get(mContext); 640 mTouchSlop = configuration.getScaledTouchSlop(); 641 mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); 642 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); 643 mOverscrollDistance = configuration.getScaledOverscrollDistance(); 644 mOverflingDistance = configuration.getScaledOverflingDistance(); 645 646 mDensityScale = getContext().getResources().getDisplayMetrics().density; 647 } 648 649 @Override 650 public void setOverScrollMode(int mode) { 651 if (mode != OVER_SCROLL_NEVER) { 652 if (mEdgeGlowTop == null) { 653 final Resources res = getContext().getResources(); 654 final Drawable edge = res.getDrawable(R.drawable.overscroll_edge); 655 final Drawable glow = res.getDrawable(R.drawable.overscroll_glow); 656 mEdgeGlowTop = new EdgeGlow(edge, glow); 657 mEdgeGlowBottom = new EdgeGlow(edge, glow); 658 } 659 } else { 660 mEdgeGlowTop = null; 661 mEdgeGlowBottom = null; 662 } 663 super.setOverScrollMode(mode); 664 } 665 666 /** 667 * @return true if all list content currently fits within the view boundaries 668 */ 669 private boolean contentFits() { 670 final int childCount = getChildCount(); 671 if (childCount != mItemCount) { 672 return false; 673 } 674 675 return getChildAt(0).getTop() >= 0 && getChildAt(childCount - 1).getBottom() <= mBottom; 676 } 677 678 /** 679 * Enables fast scrolling by letting the user quickly scroll through lists by 680 * dragging the fast scroll thumb. The adapter attached to the list may want 681 * to implement {@link SectionIndexer} if it wishes to display alphabet preview and 682 * jump between sections of the list. 683 * @see SectionIndexer 684 * @see #isFastScrollEnabled() 685 * @param enabled whether or not to enable fast scrolling 686 */ 687 public void setFastScrollEnabled(boolean enabled) { 688 mFastScrollEnabled = enabled; 689 if (enabled) { 690 if (mFastScroller == null) { 691 mFastScroller = new FastScroller(getContext(), this); 692 } 693 } else { 694 if (mFastScroller != null) { 695 mFastScroller.stop(); 696 mFastScroller = null; 697 } 698 } 699 } 700 701 /** 702 * Returns the current state of the fast scroll feature. 703 * @see #setFastScrollEnabled(boolean) 704 * @return true if fast scroll is enabled, false otherwise 705 */ 706 @ViewDebug.ExportedProperty 707 public boolean isFastScrollEnabled() { 708 return mFastScrollEnabled; 709 } 710 711 /** 712 * If fast scroll is visible, then don't draw the vertical scrollbar. 713 * @hide 714 */ 715 @Override 716 protected boolean isVerticalScrollBarHidden() { 717 return mFastScroller != null && mFastScroller.isVisible(); 718 } 719 720 /** 721 * When smooth scrollbar is enabled, the position and size of the scrollbar thumb 722 * is computed based on the number of visible pixels in the visible items. This 723 * however assumes that all list items have the same height. If you use a list in 724 * which items have different heights, the scrollbar will change appearance as the 725 * user scrolls through the list. To avoid this issue, you need to disable this 726 * property. 727 * 728 * When smooth scrollbar is disabled, the position and size of the scrollbar thumb 729 * is based solely on the number of items in the adapter and the position of the 730 * visible items inside the adapter. This provides a stable scrollbar as the user 731 * navigates through a list of items with varying heights. 732 * 733 * @param enabled Whether or not to enable smooth scrollbar. 734 * 735 * @see #setSmoothScrollbarEnabled(boolean) 736 * @attr ref android.R.styleable#AbsListView_smoothScrollbar 737 */ 738 public void setSmoothScrollbarEnabled(boolean enabled) { 739 mSmoothScrollbarEnabled = enabled; 740 } 741 742 /** 743 * Returns the current state of the fast scroll feature. 744 * 745 * @return True if smooth scrollbar is enabled is enabled, false otherwise. 746 * 747 * @see #setSmoothScrollbarEnabled(boolean) 748 */ 749 @ViewDebug.ExportedProperty 750 public boolean isSmoothScrollbarEnabled() { 751 return mSmoothScrollbarEnabled; 752 } 753 754 /** 755 * Set the listener that will receive notifications every time the list scrolls. 756 * 757 * @param l the scroll listener 758 */ 759 public void setOnScrollListener(OnScrollListener l) { 760 mOnScrollListener = l; 761 invokeOnItemScrollListener(); 762 } 763 764 /** 765 * Notify our scroll listener (if there is one) of a change in scroll state 766 */ 767 void invokeOnItemScrollListener() { 768 if (mFastScroller != null) { 769 mFastScroller.onScroll(this, mFirstPosition, getChildCount(), mItemCount); 770 } 771 if (mOnScrollListener != null) { 772 mOnScrollListener.onScroll(this, mFirstPosition, getChildCount(), mItemCount); 773 } 774 } 775 776 /** 777 * Indicates whether the children's drawing cache is used during a scroll. 778 * By default, the drawing cache is enabled but this will consume more memory. 779 * 780 * @return true if the scrolling cache is enabled, false otherwise 781 * 782 * @see #setScrollingCacheEnabled(boolean) 783 * @see View#setDrawingCacheEnabled(boolean) 784 */ 785 @ViewDebug.ExportedProperty 786 public boolean isScrollingCacheEnabled() { 787 return mScrollingCacheEnabled; 788 } 789 790 /** 791 * Enables or disables the children's drawing cache during a scroll. 792 * By default, the drawing cache is enabled but this will use more memory. 793 * 794 * When the scrolling cache is enabled, the caches are kept after the 795 * first scrolling. You can manually clear the cache by calling 796 * {@link android.view.ViewGroup#setChildrenDrawingCacheEnabled(boolean)}. 797 * 798 * @param enabled true to enable the scroll cache, false otherwise 799 * 800 * @see #isScrollingCacheEnabled() 801 * @see View#setDrawingCacheEnabled(boolean) 802 */ 803 public void setScrollingCacheEnabled(boolean enabled) { 804 if (mScrollingCacheEnabled && !enabled) { 805 clearScrollingCache(); 806 } 807 mScrollingCacheEnabled = enabled; 808 } 809 810 /** 811 * Enables or disables the type filter window. If enabled, typing when 812 * this view has focus will filter the children to match the users input. 813 * Note that the {@link Adapter} used by this view must implement the 814 * {@link Filterable} interface. 815 * 816 * @param textFilterEnabled true to enable type filtering, false otherwise 817 * 818 * @see Filterable 819 */ 820 public void setTextFilterEnabled(boolean textFilterEnabled) { 821 mTextFilterEnabled = textFilterEnabled; 822 } 823 824 /** 825 * Indicates whether type filtering is enabled for this view 826 * 827 * @return true if type filtering is enabled, false otherwise 828 * 829 * @see #setTextFilterEnabled(boolean) 830 * @see Filterable 831 */ 832 @ViewDebug.ExportedProperty 833 public boolean isTextFilterEnabled() { 834 return mTextFilterEnabled; 835 } 836 837 @Override 838 public void getFocusedRect(Rect r) { 839 View view = getSelectedView(); 840 if (view != null && view.getParent() == this) { 841 // the focused rectangle of the selected view offset into the 842 // coordinate space of this view. 843 view.getFocusedRect(r); 844 offsetDescendantRectToMyCoords(view, r); 845 } else { 846 // otherwise, just the norm 847 super.getFocusedRect(r); 848 } 849 } 850 851 private void useDefaultSelector() { 852 setSelector(getResources().getDrawable( 853 com.android.internal.R.drawable.list_selector_background)); 854 } 855 856 /** 857 * Indicates whether the content of this view is pinned to, or stacked from, 858 * the bottom edge. 859 * 860 * @return true if the content is stacked from the bottom edge, false otherwise 861 */ 862 @ViewDebug.ExportedProperty 863 public boolean isStackFromBottom() { 864 return mStackFromBottom; 865 } 866 867 /** 868 * When stack from bottom is set to true, the list fills its content starting from 869 * the bottom of the view. 870 * 871 * @param stackFromBottom true to pin the view's content to the bottom edge, 872 * false to pin the view's content to the top edge 873 */ 874 public void setStackFromBottom(boolean stackFromBottom) { 875 if (mStackFromBottom != stackFromBottom) { 876 mStackFromBottom = stackFromBottom; 877 requestLayoutIfNecessary(); 878 } 879 } 880 881 void requestLayoutIfNecessary() { 882 if (getChildCount() > 0) { 883 resetList(); 884 requestLayout(); 885 invalidate(); 886 } 887 } 888 889 static class SavedState extends BaseSavedState { 890 long selectedId; 891 long firstId; 892 int viewTop; 893 int position; 894 int height; 895 String filter; 896 897 /** 898 * Constructor called from {@link AbsListView#onSaveInstanceState()} 899 */ 900 SavedState(Parcelable superState) { 901 super(superState); 902 } 903 904 /** 905 * Constructor called from {@link #CREATOR} 906 */ 907 private SavedState(Parcel in) { 908 super(in); 909 selectedId = in.readLong(); 910 firstId = in.readLong(); 911 viewTop = in.readInt(); 912 position = in.readInt(); 913 height = in.readInt(); 914 filter = in.readString(); 915 } 916 917 @Override 918 public void writeToParcel(Parcel out, int flags) { 919 super.writeToParcel(out, flags); 920 out.writeLong(selectedId); 921 out.writeLong(firstId); 922 out.writeInt(viewTop); 923 out.writeInt(position); 924 out.writeInt(height); 925 out.writeString(filter); 926 } 927 928 @Override 929 public String toString() { 930 return "AbsListView.SavedState{" 931 + Integer.toHexString(System.identityHashCode(this)) 932 + " selectedId=" + selectedId 933 + " firstId=" + firstId 934 + " viewTop=" + viewTop 935 + " position=" + position 936 + " height=" + height 937 + " filter=" + filter + "}"; 938 } 939 940 public static final Parcelable.Creator<SavedState> CREATOR 941 = new Parcelable.Creator<SavedState>() { 942 public SavedState createFromParcel(Parcel in) { 943 return new SavedState(in); 944 } 945 946 public SavedState[] newArray(int size) { 947 return new SavedState[size]; 948 } 949 }; 950 } 951 952 @Override 953 public Parcelable onSaveInstanceState() { 954 /* 955 * This doesn't really make sense as the place to dismiss the 956 * popups, but there don't seem to be any other useful hooks 957 * that happen early enough to keep from getting complaints 958 * about having leaked the window. 959 */ 960 dismissPopup(); 961 962 Parcelable superState = super.onSaveInstanceState(); 963 964 SavedState ss = new SavedState(superState); 965 966 boolean haveChildren = getChildCount() > 0; 967 long selectedId = getSelectedItemId(); 968 ss.selectedId = selectedId; 969 ss.height = getHeight(); 970 971 if (selectedId >= 0) { 972 // Remember the selection 973 ss.viewTop = mSelectedTop; 974 ss.position = getSelectedItemPosition(); 975 ss.firstId = INVALID_POSITION; 976 } else { 977 if (haveChildren) { 978 // Remember the position of the first child 979 View v = getChildAt(0); 980 ss.viewTop = v.getTop(); 981 ss.position = mFirstPosition; 982 ss.firstId = mAdapter.getItemId(mFirstPosition); 983 } else { 984 ss.viewTop = 0; 985 ss.firstId = INVALID_POSITION; 986 ss.position = 0; 987 } 988 } 989 990 ss.filter = null; 991 if (mFiltered) { 992 final EditText textFilter = mTextFilter; 993 if (textFilter != null) { 994 Editable filterText = textFilter.getText(); 995 if (filterText != null) { 996 ss.filter = filterText.toString(); 997 } 998 } 999 } 1000 1001 return ss; 1002 } 1003 1004 @Override 1005 public void onRestoreInstanceState(Parcelable state) { 1006 SavedState ss = (SavedState) state; 1007 1008 super.onRestoreInstanceState(ss.getSuperState()); 1009 mDataChanged = true; 1010 1011 mSyncHeight = ss.height; 1012 1013 if (ss.selectedId >= 0) { 1014 mNeedSync = true; 1015 mSyncRowId = ss.selectedId; 1016 mSyncPosition = ss.position; 1017 mSpecificTop = ss.viewTop; 1018 mSyncMode = SYNC_SELECTED_POSITION; 1019 } else if (ss.firstId >= 0) { 1020 setSelectedPositionInt(INVALID_POSITION); 1021 // Do this before setting mNeedSync since setNextSelectedPosition looks at mNeedSync 1022 setNextSelectedPositionInt(INVALID_POSITION); 1023 mNeedSync = true; 1024 mSyncRowId = ss.firstId; 1025 mSyncPosition = ss.position; 1026 mSpecificTop = ss.viewTop; 1027 mSyncMode = SYNC_FIRST_POSITION; 1028 } 1029 1030 setFilterText(ss.filter); 1031 1032 requestLayout(); 1033 } 1034 1035 private boolean acceptFilter() { 1036 return mTextFilterEnabled && getAdapter() instanceof Filterable && 1037 ((Filterable) getAdapter()).getFilter() != null; 1038 } 1039 1040 /** 1041 * Sets the initial value for the text filter. 1042 * @param filterText The text to use for the filter. 1043 * 1044 * @see #setTextFilterEnabled 1045 */ 1046 public void setFilterText(String filterText) { 1047 // TODO: Should we check for acceptFilter()? 1048 if (mTextFilterEnabled && !TextUtils.isEmpty(filterText)) { 1049 createTextFilter(false); 1050 // This is going to call our listener onTextChanged, but we might not 1051 // be ready to bring up a window yet 1052 mTextFilter.setText(filterText); 1053 mTextFilter.setSelection(filterText.length()); 1054 if (mAdapter instanceof Filterable) { 1055 // if mPopup is non-null, then onTextChanged will do the filtering 1056 if (mPopup == null) { 1057 Filter f = ((Filterable) mAdapter).getFilter(); 1058 f.filter(filterText); 1059 } 1060 // Set filtered to true so we will display the filter window when our main 1061 // window is ready 1062 mFiltered = true; 1063 mDataSetObserver.clearSavedState(); 1064 } 1065 } 1066 } 1067 1068 /** 1069 * Returns the list's text filter, if available. 1070 * @return the list's text filter or null if filtering isn't enabled 1071 */ 1072 public CharSequence getTextFilter() { 1073 if (mTextFilterEnabled && mTextFilter != null) { 1074 return mTextFilter.getText(); 1075 } 1076 return null; 1077 } 1078 1079 @Override 1080 protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { 1081 super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); 1082 if (gainFocus && mSelectedPosition < 0 && !isInTouchMode()) { 1083 resurrectSelection(); 1084 } 1085 } 1086 1087 @Override 1088 public void requestLayout() { 1089 if (!mBlockLayoutRequests && !mInLayout) { 1090 super.requestLayout(); 1091 } 1092 } 1093 1094 /** 1095 * The list is empty. Clear everything out. 1096 */ 1097 void resetList() { 1098 removeAllViewsInLayout(); 1099 mFirstPosition = 0; 1100 mDataChanged = false; 1101 mNeedSync = false; 1102 mOldSelectedPosition = INVALID_POSITION; 1103 mOldSelectedRowId = INVALID_ROW_ID; 1104 setSelectedPositionInt(INVALID_POSITION); 1105 setNextSelectedPositionInt(INVALID_POSITION); 1106 mSelectedTop = 0; 1107 mSelectorRect.setEmpty(); 1108 invalidate(); 1109 } 1110 1111 @Override 1112 protected int computeVerticalScrollExtent() { 1113 final int count = getChildCount(); 1114 if (count > 0) { 1115 if (mSmoothScrollbarEnabled) { 1116 int extent = count * 100; 1117 1118 View view = getChildAt(0); 1119 final int top = view.getTop(); 1120 int height = view.getHeight(); 1121 if (height > 0) { 1122 extent += (top * 100) / height; 1123 } 1124 1125 view = getChildAt(count - 1); 1126 final int bottom = view.getBottom(); 1127 height = view.getHeight(); 1128 if (height > 0) { 1129 extent -= ((bottom - getHeight()) * 100) / height; 1130 } 1131 1132 return extent; 1133 } else { 1134 return 1; 1135 } 1136 } 1137 return 0; 1138 } 1139 1140 @Override 1141 protected int computeVerticalScrollOffset() { 1142 final int firstPosition = mFirstPosition; 1143 final int childCount = getChildCount(); 1144 if (firstPosition >= 0 && childCount > 0) { 1145 if (mSmoothScrollbarEnabled) { 1146 final View view = getChildAt(0); 1147 final int top = view.getTop(); 1148 int height = view.getHeight(); 1149 if (height > 0) { 1150 return Math.max(firstPosition * 100 - (top * 100) / height + 1151 (int)((float)mScrollY / getHeight() * mItemCount * 100), 0); 1152 } 1153 } else { 1154 int index; 1155 final int count = mItemCount; 1156 if (firstPosition == 0) { 1157 index = 0; 1158 } else if (firstPosition + childCount == count) { 1159 index = count; 1160 } else { 1161 index = firstPosition + childCount / 2; 1162 } 1163 return (int) (firstPosition + childCount * (index / (float) count)); 1164 } 1165 } 1166 return 0; 1167 } 1168 1169 @Override 1170 protected int computeVerticalScrollRange() { 1171 int result; 1172 if (mSmoothScrollbarEnabled) { 1173 result = Math.max(mItemCount * 100, 0); 1174 if (mScrollY != 0) { 1175 // Compensate for overscroll 1176 result += Math.abs((int) ((float) mScrollY / getHeight() * mItemCount * 100)); 1177 } 1178 } else { 1179 result = mItemCount; 1180 } 1181 return result; 1182 } 1183 1184 @Override 1185 protected float getTopFadingEdgeStrength() { 1186 final int count = getChildCount(); 1187 final float fadeEdge = super.getTopFadingEdgeStrength(); 1188 if (count == 0) { 1189 return fadeEdge; 1190 } else { 1191 if (mFirstPosition > 0) { 1192 return 1.0f; 1193 } 1194 1195 final int top = getChildAt(0).getTop(); 1196 final float fadeLength = (float) getVerticalFadingEdgeLength(); 1197 return top < mPaddingTop ? (float) -(top - mPaddingTop) / fadeLength : fadeEdge; 1198 } 1199 } 1200 1201 @Override 1202 protected float getBottomFadingEdgeStrength() { 1203 final int count = getChildCount(); 1204 final float fadeEdge = super.getBottomFadingEdgeStrength(); 1205 if (count == 0) { 1206 return fadeEdge; 1207 } else { 1208 if (mFirstPosition + count - 1 < mItemCount - 1) { 1209 return 1.0f; 1210 } 1211 1212 final int bottom = getChildAt(count - 1).getBottom(); 1213 final int height = getHeight(); 1214 final float fadeLength = (float) getVerticalFadingEdgeLength(); 1215 return bottom > height - mPaddingBottom ? 1216 (float) (bottom - height + mPaddingBottom) / fadeLength : fadeEdge; 1217 } 1218 } 1219 1220 @Override 1221 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 1222 if (mSelector == null) { 1223 useDefaultSelector(); 1224 } 1225 final Rect listPadding = mListPadding; 1226 listPadding.left = mSelectionLeftPadding + mPaddingLeft; 1227 listPadding.top = mSelectionTopPadding + mPaddingTop; 1228 listPadding.right = mSelectionRightPadding + mPaddingRight; 1229 listPadding.bottom = mSelectionBottomPadding + mPaddingBottom; 1230 } 1231 1232 /** 1233 * Subclasses should NOT override this method but 1234 * {@link #layoutChildren()} instead. 1235 */ 1236 @Override 1237 protected void onLayout(boolean changed, int l, int t, int r, int b) { 1238 super.onLayout(changed, l, t, r, b); 1239 mInLayout = true; 1240 if (changed) { 1241 int childCount = getChildCount(); 1242 for (int i = 0; i < childCount; i++) { 1243 getChildAt(i).forceLayout(); 1244 } 1245 mRecycler.markChildrenDirty(); 1246 } 1247 1248 layoutChildren(); 1249 mInLayout = false; 1250 1251 mOverscrollMax = (b - t) / OVERSCROLL_LIMIT_DIVISOR; 1252 } 1253 1254 /** 1255 * @hide 1256 */ 1257 @Override 1258 protected boolean setFrame(int left, int top, int right, int bottom) { 1259 final boolean changed = super.setFrame(left, top, right, bottom); 1260 1261 if (changed) { 1262 // Reposition the popup when the frame has changed. This includes 1263 // translating the widget, not just changing its dimension. The 1264 // filter popup needs to follow the widget. 1265 final boolean visible = getWindowVisibility() == View.VISIBLE; 1266 if (mFiltered && visible && mPopup != null && mPopup.isShowing()) { 1267 positionPopup(); 1268 } 1269 } 1270 1271 return changed; 1272 } 1273 1274 /** 1275 * Subclasses must override this method to layout their children. 1276 */ 1277 protected void layoutChildren() { 1278 } 1279 1280 void updateScrollIndicators() { 1281 if (mScrollUp != null) { 1282 boolean canScrollUp; 1283 // 0th element is not visible 1284 canScrollUp = mFirstPosition > 0; 1285 1286 // ... Or top of 0th element is not visible 1287 if (!canScrollUp) { 1288 if (getChildCount() > 0) { 1289 View child = getChildAt(0); 1290 canScrollUp = child.getTop() < mListPadding.top; 1291 } 1292 } 1293 1294 mScrollUp.setVisibility(canScrollUp ? View.VISIBLE : View.INVISIBLE); 1295 } 1296 1297 if (mScrollDown != null) { 1298 boolean canScrollDown; 1299 int count = getChildCount(); 1300 1301 // Last item is not visible 1302 canScrollDown = (mFirstPosition + count) < mItemCount; 1303 1304 // ... Or bottom of the last element is not visible 1305 if (!canScrollDown && count > 0) { 1306 View child = getChildAt(count - 1); 1307 canScrollDown = child.getBottom() > mBottom - mListPadding.bottom; 1308 } 1309 1310 mScrollDown.setVisibility(canScrollDown ? View.VISIBLE : View.INVISIBLE); 1311 } 1312 } 1313 1314 @Override 1315 @ViewDebug.ExportedProperty 1316 public View getSelectedView() { 1317 if (mItemCount > 0 && mSelectedPosition >= 0) { 1318 return getChildAt(mSelectedPosition - mFirstPosition); 1319 } else { 1320 return null; 1321 } 1322 } 1323 1324 /** 1325 * List padding is the maximum of the normal view's padding and the padding of the selector. 1326 * 1327 * @see android.view.View#getPaddingTop() 1328 * @see #getSelector() 1329 * 1330 * @return The top list padding. 1331 */ 1332 public int getListPaddingTop() { 1333 return mListPadding.top; 1334 } 1335 1336 /** 1337 * List padding is the maximum of the normal view's padding and the padding of the selector. 1338 * 1339 * @see android.view.View#getPaddingBottom() 1340 * @see #getSelector() 1341 * 1342 * @return The bottom list padding. 1343 */ 1344 public int getListPaddingBottom() { 1345 return mListPadding.bottom; 1346 } 1347 1348 /** 1349 * List padding is the maximum of the normal view's padding and the padding of the selector. 1350 * 1351 * @see android.view.View#getPaddingLeft() 1352 * @see #getSelector() 1353 * 1354 * @return The left list padding. 1355 */ 1356 public int getListPaddingLeft() { 1357 return mListPadding.left; 1358 } 1359 1360 /** 1361 * List padding is the maximum of the normal view's padding and the padding of the selector. 1362 * 1363 * @see android.view.View#getPaddingRight() 1364 * @see #getSelector() 1365 * 1366 * @return The right list padding. 1367 */ 1368 public int getListPaddingRight() { 1369 return mListPadding.right; 1370 } 1371 1372 /** 1373 * Get a view and have it show the data associated with the specified 1374 * position. This is called when we have already discovered that the view is 1375 * not available for reuse in the recycle bin. The only choices left are 1376 * converting an old view or making a new one. 1377 * 1378 * @param position The position to display 1379 * @param isScrap Array of at least 1 boolean, the first entry will become true if 1380 * the returned view was taken from the scrap heap, false if otherwise. 1381 * 1382 * @return A view displaying the data associated with the specified position 1383 */ 1384 View obtainView(int position, boolean[] isScrap) { 1385 isScrap[0] = false; 1386 View scrapView; 1387 1388 scrapView = mRecycler.getScrapView(position); 1389 1390 View child; 1391 if (scrapView != null) { 1392 if (ViewDebug.TRACE_RECYCLER) { 1393 ViewDebug.trace(scrapView, ViewDebug.RecyclerTraceType.RECYCLE_FROM_SCRAP_HEAP, 1394 position, -1); 1395 } 1396 1397 child = mAdapter.getView(position, scrapView, this); 1398 1399 if (ViewDebug.TRACE_RECYCLER) { 1400 ViewDebug.trace(child, ViewDebug.RecyclerTraceType.BIND_VIEW, 1401 position, getChildCount()); 1402 } 1403 1404 if (child != scrapView) { 1405 mRecycler.addScrapView(scrapView); 1406 if (mCacheColorHint != 0) { 1407 child.setDrawingCacheBackgroundColor(mCacheColorHint); 1408 } 1409 if (ViewDebug.TRACE_RECYCLER) { 1410 ViewDebug.trace(scrapView, ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP, 1411 position, -1); 1412 } 1413 } else { 1414 isScrap[0] = true; 1415 child.dispatchFinishTemporaryDetach(); 1416 } 1417 } else { 1418 child = mAdapter.getView(position, null, this); 1419 if (mCacheColorHint != 0) { 1420 child.setDrawingCacheBackgroundColor(mCacheColorHint); 1421 } 1422 if (ViewDebug.TRACE_RECYCLER) { 1423 ViewDebug.trace(child, ViewDebug.RecyclerTraceType.NEW_VIEW, 1424 position, getChildCount()); 1425 } 1426 } 1427 1428 return child; 1429 } 1430 1431 void positionSelector(View sel) { 1432 final Rect selectorRect = mSelectorRect; 1433 selectorRect.set(sel.getLeft(), sel.getTop(), sel.getRight(), sel.getBottom()); 1434 positionSelector(selectorRect.left, selectorRect.top, selectorRect.right, 1435 selectorRect.bottom); 1436 1437 final boolean isChildViewEnabled = mIsChildViewEnabled; 1438 if (sel.isEnabled() != isChildViewEnabled) { 1439 mIsChildViewEnabled = !isChildViewEnabled; 1440 refreshDrawableState(); 1441 } 1442 } 1443 1444 private void positionSelector(int l, int t, int r, int b) { 1445 mSelectorRect.set(l - mSelectionLeftPadding, t - mSelectionTopPadding, r 1446 + mSelectionRightPadding, b + mSelectionBottomPadding); 1447 } 1448 1449 @Override 1450 protected void dispatchDraw(Canvas canvas) { 1451 int saveCount = 0; 1452 final boolean clipToPadding = (mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK; 1453 if (clipToPadding) { 1454 saveCount = canvas.save(); 1455 final int scrollX = mScrollX; 1456 final int scrollY = mScrollY; 1457 canvas.clipRect(scrollX + mPaddingLeft, scrollY + mPaddingTop, 1458 scrollX + mRight - mLeft - mPaddingRight, 1459 scrollY + mBottom - mTop - mPaddingBottom); 1460 mGroupFlags &= ~CLIP_TO_PADDING_MASK; 1461 } 1462 1463 final boolean drawSelectorOnTop = mDrawSelectorOnTop; 1464 if (!drawSelectorOnTop) { 1465 drawSelector(canvas); 1466 } 1467 1468 super.dispatchDraw(canvas); 1469 1470 if (drawSelectorOnTop) { 1471 drawSelector(canvas); 1472 } 1473 1474 if (clipToPadding) { 1475 canvas.restoreToCount(saveCount); 1476 mGroupFlags |= CLIP_TO_PADDING_MASK; 1477 } 1478 } 1479 1480 @Override 1481 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 1482 if (getChildCount() > 0) { 1483 mDataChanged = true; 1484 rememberSyncState(); 1485 } 1486 1487 if (mFastScroller != null) { 1488 mFastScroller.onSizeChanged(w, h, oldw, oldh); 1489 } 1490 } 1491 1492 /** 1493 * @return True if the current touch mode requires that we draw the selector in the pressed 1494 * state. 1495 */ 1496 boolean touchModeDrawsInPressedState() { 1497 // FIXME use isPressed for this 1498 switch (mTouchMode) { 1499 case TOUCH_MODE_TAP: 1500 case TOUCH_MODE_DONE_WAITING: 1501 return true; 1502 default: 1503 return false; 1504 } 1505 } 1506 1507 /** 1508 * Indicates whether this view is in a state where the selector should be drawn. This will 1509 * happen if we have focus but are not in touch mode, or we are in the middle of displaying 1510 * the pressed state for an item. 1511 * 1512 * @return True if the selector should be shown 1513 */ 1514 boolean shouldShowSelector() { 1515 return (hasFocus() && !isInTouchMode()) || touchModeDrawsInPressedState(); 1516 } 1517 1518 private void drawSelector(Canvas canvas) { 1519 if (shouldShowSelector() && mSelectorRect != null && !mSelectorRect.isEmpty()) { 1520 final Drawable selector = mSelector; 1521 selector.setBounds(mSelectorRect); 1522 selector.draw(canvas); 1523 } 1524 } 1525 1526 /** 1527 * Controls whether the selection highlight drawable should be drawn on top of the item or 1528 * behind it. 1529 * 1530 * @param onTop If true, the selector will be drawn on the item it is highlighting. The default 1531 * is false. 1532 * 1533 * @attr ref android.R.styleable#AbsListView_drawSelectorOnTop 1534 */ 1535 public void setDrawSelectorOnTop(boolean onTop) { 1536 mDrawSelectorOnTop = onTop; 1537 } 1538 1539 /** 1540 * Set a Drawable that should be used to highlight the currently selected item. 1541 * 1542 * @param resID A Drawable resource to use as the selection highlight. 1543 * 1544 * @attr ref android.R.styleable#AbsListView_listSelector 1545 */ 1546 public void setSelector(int resID) { 1547 setSelector(getResources().getDrawable(resID)); 1548 } 1549 1550 public void setSelector(Drawable sel) { 1551 if (mSelector != null) { 1552 mSelector.setCallback(null); 1553 unscheduleDrawable(mSelector); 1554 } 1555 mSelector = sel; 1556 Rect padding = new Rect(); 1557 sel.getPadding(padding); 1558 mSelectionLeftPadding = padding.left; 1559 mSelectionTopPadding = padding.top; 1560 mSelectionRightPadding = padding.right; 1561 mSelectionBottomPadding = padding.bottom; 1562 sel.setCallback(this); 1563 sel.setState(getDrawableState()); 1564 } 1565 1566 /** 1567 * Returns the selector {@link android.graphics.drawable.Drawable} that is used to draw the 1568 * selection in the list. 1569 * 1570 * @return the drawable used to display the selector 1571 */ 1572 public Drawable getSelector() { 1573 return mSelector; 1574 } 1575 1576 /** 1577 * Sets the selector state to "pressed" and posts a CheckForKeyLongPress to see if 1578 * this is a long press. 1579 */ 1580 void keyPressed() { 1581 if (!isEnabled() || !isClickable()) { 1582 return; 1583 } 1584 1585 Drawable selector = mSelector; 1586 Rect selectorRect = mSelectorRect; 1587 if (selector != null && (isFocused() || touchModeDrawsInPressedState()) 1588 && selectorRect != null && !selectorRect.isEmpty()) { 1589 1590 final View v = getChildAt(mSelectedPosition - mFirstPosition); 1591 1592 if (v != null) { 1593 if (v.hasFocusable()) return; 1594 v.setPressed(true); 1595 } 1596 setPressed(true); 1597 1598 final boolean longClickable = isLongClickable(); 1599 Drawable d = selector.getCurrent(); 1600 if (d != null && d instanceof TransitionDrawable) { 1601 if (longClickable) { 1602 ((TransitionDrawable) d).startTransition( 1603 ViewConfiguration.getLongPressTimeout()); 1604 } else { 1605 ((TransitionDrawable) d).resetTransition(); 1606 } 1607 } 1608 if (longClickable && !mDataChanged) { 1609 if (mPendingCheckForKeyLongPress == null) { 1610 mPendingCheckForKeyLongPress = new CheckForKeyLongPress(); 1611 } 1612 mPendingCheckForKeyLongPress.rememberWindowAttachCount(); 1613 postDelayed(mPendingCheckForKeyLongPress, ViewConfiguration.getLongPressTimeout()); 1614 } 1615 } 1616 } 1617 1618 public void setScrollIndicators(View up, View down) { 1619 mScrollUp = up; 1620 mScrollDown = down; 1621 } 1622 1623 @Override 1624 protected void drawableStateChanged() { 1625 super.drawableStateChanged(); 1626 if (mSelector != null) { 1627 mSelector.setState(getDrawableState()); 1628 } 1629 } 1630 1631 @Override 1632 protected int[] onCreateDrawableState(int extraSpace) { 1633 // If the child view is enabled then do the default behavior. 1634 if (mIsChildViewEnabled) { 1635 // Common case 1636 return super.onCreateDrawableState(extraSpace); 1637 } 1638 1639 // The selector uses this View's drawable state. The selected child view 1640 // is disabled, so we need to remove the enabled state from the drawable 1641 // states. 1642 final int enabledState = ENABLED_STATE_SET[0]; 1643 1644 // If we don't have any extra space, it will return one of the static state arrays, 1645 // and clearing the enabled state on those arrays is a bad thing! If we specify 1646 // we need extra space, it will create+copy into a new array that safely mutable. 1647 int[] state = super.onCreateDrawableState(extraSpace + 1); 1648 int enabledPos = -1; 1649 for (int i = state.length - 1; i >= 0; i--) { 1650 if (state[i] == enabledState) { 1651 enabledPos = i; 1652 break; 1653 } 1654 } 1655 1656 // Remove the enabled state 1657 if (enabledPos >= 0) { 1658 System.arraycopy(state, enabledPos + 1, state, enabledPos, 1659 state.length - enabledPos - 1); 1660 } 1661 1662 return state; 1663 } 1664 1665 @Override 1666 public boolean verifyDrawable(Drawable dr) { 1667 return mSelector == dr || super.verifyDrawable(dr); 1668 } 1669 1670 @Override 1671 protected void onAttachedToWindow() { 1672 super.onAttachedToWindow(); 1673 1674 final ViewTreeObserver treeObserver = getViewTreeObserver(); 1675 if (treeObserver != null) { 1676 treeObserver.addOnTouchModeChangeListener(this); 1677 if (mTextFilterEnabled && mPopup != null && !mGlobalLayoutListenerAddedFilter) { 1678 treeObserver.addOnGlobalLayoutListener(this); 1679 } 1680 } 1681 } 1682 1683 @Override 1684 protected void onDetachedFromWindow() { 1685 super.onDetachedFromWindow(); 1686 1687 // Dismiss the popup in case onSaveInstanceState() was not invoked 1688 dismissPopup(); 1689 1690 // Detach any view left in the scrap heap 1691 mRecycler.clear(); 1692 1693 final ViewTreeObserver treeObserver = getViewTreeObserver(); 1694 if (treeObserver != null) { 1695 treeObserver.removeOnTouchModeChangeListener(this); 1696 if (mTextFilterEnabled && mPopup != null) { 1697 treeObserver.removeGlobalOnLayoutListener(this); 1698 mGlobalLayoutListenerAddedFilter = false; 1699 } 1700 } 1701 } 1702 1703 @Override 1704 public void onWindowFocusChanged(boolean hasWindowFocus) { 1705 super.onWindowFocusChanged(hasWindowFocus); 1706 1707 final int touchMode = isInTouchMode() ? TOUCH_MODE_ON : TOUCH_MODE_OFF; 1708 1709 if (!hasWindowFocus) { 1710 setChildrenDrawingCacheEnabled(false); 1711 if (mFlingRunnable != null) { 1712 removeCallbacks(mFlingRunnable); 1713 // let the fling runnable report it's new state which 1714 // should be idle 1715 mFlingRunnable.endFling(); 1716 if (mScrollY != 0) { 1717 mScrollY = 0; 1718 finishGlows(); 1719 invalidate(); 1720 } 1721 } 1722 // Always hide the type filter 1723 dismissPopup(); 1724 1725 if (touchMode == TOUCH_MODE_OFF) { 1726 // Remember the last selected element 1727 mResurrectToPosition = mSelectedPosition; 1728 } 1729 } else { 1730 if (mFiltered && !mPopupHidden) { 1731 // Show the type filter only if a filter is in effect 1732 showPopup(); 1733 } 1734 1735 // If we changed touch mode since the last time we had focus 1736 if (touchMode != mLastTouchMode && mLastTouchMode != TOUCH_MODE_UNKNOWN) { 1737 // If we come back in trackball mode, we bring the selection back 1738 if (touchMode == TOUCH_MODE_OFF) { 1739 // This will trigger a layout 1740 resurrectSelection(); 1741 1742 // If we come back in touch mode, then we want to hide the selector 1743 } else { 1744 hideSelector(); 1745 mLayoutMode = LAYOUT_NORMAL; 1746 layoutChildren(); 1747 } 1748 } 1749 } 1750 1751 mLastTouchMode = touchMode; 1752 } 1753 1754 /** 1755 * Creates the ContextMenuInfo returned from {@link #getContextMenuInfo()}. This 1756 * methods knows the view, position and ID of the item that received the 1757 * long press. 1758 * 1759 * @param view The view that received the long press. 1760 * @param position The position of the item that received the long press. 1761 * @param id The ID of the item that received the long press. 1762 * @return The extra information that should be returned by 1763 * {@link #getContextMenuInfo()}. 1764 */ 1765 ContextMenuInfo createContextMenuInfo(View view, int position, long id) { 1766 return new AdapterContextMenuInfo(view, position, id); 1767 } 1768 1769 /** 1770 * A base class for Runnables that will check that their view is still attached to 1771 * the original window as when the Runnable was created. 1772 * 1773 */ 1774 private class WindowRunnnable { 1775 private int mOriginalAttachCount; 1776 1777 public void rememberWindowAttachCount() { 1778 mOriginalAttachCount = getWindowAttachCount(); 1779 } 1780 1781 public boolean sameWindow() { 1782 return hasWindowFocus() && getWindowAttachCount() == mOriginalAttachCount; 1783 } 1784 } 1785 1786 private class PerformClick extends WindowRunnnable implements Runnable { 1787 View mChild; 1788 int mClickMotionPosition; 1789 1790 public void run() { 1791 // The data has changed since we posted this action in the event queue, 1792 // bail out before bad things happen 1793 if (mDataChanged) return; 1794 1795 final ListAdapter adapter = mAdapter; 1796 final int motionPosition = mClickMotionPosition; 1797 if (adapter != null && mItemCount > 0 && 1798 motionPosition != INVALID_POSITION && 1799 motionPosition < adapter.getCount() && sameWindow()) { 1800 performItemClick(mChild, motionPosition, adapter.getItemId(motionPosition)); 1801 } 1802 } 1803 } 1804 1805 private class CheckForLongPress extends WindowRunnnable implements Runnable { 1806 public void run() { 1807 final int motionPosition = mMotionPosition; 1808 final View child = getChildAt(motionPosition - mFirstPosition); 1809 if (child != null) { 1810 final int longPressPosition = mMotionPosition; 1811 final long longPressId = mAdapter.getItemId(mMotionPosition); 1812 1813 boolean handled = false; 1814 if (sameWindow() && !mDataChanged) { 1815 handled = performLongPress(child, longPressPosition, longPressId); 1816 } 1817 if (handled) { 1818 mTouchMode = TOUCH_MODE_REST; 1819 setPressed(false); 1820 child.setPressed(false); 1821 } else { 1822 mTouchMode = TOUCH_MODE_DONE_WAITING; 1823 } 1824 1825 } 1826 } 1827 } 1828 1829 private class CheckForKeyLongPress extends WindowRunnnable implements Runnable { 1830 public void run() { 1831 if (isPressed() && mSelectedPosition >= 0) { 1832 int index = mSelectedPosition - mFirstPosition; 1833 View v = getChildAt(index); 1834 1835 if (!mDataChanged) { 1836 boolean handled = false; 1837 if (sameWindow()) { 1838 handled = performLongPress(v, mSelectedPosition, mSelectedRowId); 1839 } 1840 if (handled) { 1841 setPressed(false); 1842 v.setPressed(false); 1843 } 1844 } else { 1845 setPressed(false); 1846 if (v != null) v.setPressed(false); 1847 } 1848 } 1849 } 1850 } 1851 1852 private boolean performLongPress(final View child, 1853 final int longPressPosition, final long longPressId) { 1854 boolean handled = false; 1855 1856 if (mOnItemLongClickListener != null) { 1857 handled = mOnItemLongClickListener.onItemLongClick(AbsListView.this, child, 1858 longPressPosition, longPressId); 1859 } 1860 if (!handled) { 1861 mContextMenuInfo = createContextMenuInfo(child, longPressPosition, longPressId); 1862 handled = super.showContextMenuForChild(AbsListView.this); 1863 } 1864 if (handled) { 1865 performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); 1866 } 1867 return handled; 1868 } 1869 1870 @Override 1871 protected ContextMenuInfo getContextMenuInfo() { 1872 return mContextMenuInfo; 1873 } 1874 1875 @Override 1876 public boolean showContextMenuForChild(View originalView) { 1877 final int longPressPosition = getPositionForView(originalView); 1878 if (longPressPosition >= 0) { 1879 final long longPressId = mAdapter.getItemId(longPressPosition); 1880 boolean handled = false; 1881 1882 if (mOnItemLongClickListener != null) { 1883 handled = mOnItemLongClickListener.onItemLongClick(AbsListView.this, originalView, 1884 longPressPosition, longPressId); 1885 } 1886 if (!handled) { 1887 mContextMenuInfo = createContextMenuInfo( 1888 getChildAt(longPressPosition - mFirstPosition), 1889 longPressPosition, longPressId); 1890 handled = super.showContextMenuForChild(originalView); 1891 } 1892 1893 return handled; 1894 } 1895 return false; 1896 } 1897 1898 @Override 1899 public boolean onKeyDown(int keyCode, KeyEvent event) { 1900 return false; 1901 } 1902 1903 @Override 1904 public boolean onKeyUp(int keyCode, KeyEvent event) { 1905 switch (keyCode) { 1906 case KeyEvent.KEYCODE_DPAD_CENTER: 1907 case KeyEvent.KEYCODE_ENTER: 1908 if (!isEnabled()) { 1909 return true; 1910 } 1911 if (isClickable() && isPressed() && 1912 mSelectedPosition >= 0 && mAdapter != null && 1913 mSelectedPosition < mAdapter.getCount()) { 1914 1915 final View view = getChildAt(mSelectedPosition - mFirstPosition); 1916 if (view != null) { 1917 performItemClick(view, mSelectedPosition, mSelectedRowId); 1918 view.setPressed(false); 1919 } 1920 setPressed(false); 1921 return true; 1922 } 1923 break; 1924 } 1925 return super.onKeyUp(keyCode, event); 1926 } 1927 1928 @Override 1929 protected void dispatchSetPressed(boolean pressed) { 1930 // Don't dispatch setPressed to our children. We call setPressed on ourselves to 1931 // get the selector in the right state, but we don't want to press each child. 1932 } 1933 1934 /** 1935 * Maps a point to a position in the list. 1936 * 1937 * @param x X in local coordinate 1938 * @param y Y in local coordinate 1939 * @return The position of the item which contains the specified point, or 1940 * {@link #INVALID_POSITION} if the point does not intersect an item. 1941 */ 1942 public int pointToPosition(int x, int y) { 1943 Rect frame = mTouchFrame; 1944 if (frame == null) { 1945 mTouchFrame = new Rect(); 1946 frame = mTouchFrame; 1947 } 1948 1949 final int count = getChildCount(); 1950 for (int i = count - 1; i >= 0; i--) { 1951 final View child = getChildAt(i); 1952 if (child.getVisibility() == View.VISIBLE) { 1953 child.getHitRect(frame); 1954 if (frame.contains(x, y)) { 1955 return mFirstPosition + i; 1956 } 1957 } 1958 } 1959 return INVALID_POSITION; 1960 } 1961 1962 1963 /** 1964 * Maps a point to a the rowId of the item which intersects that point. 1965 * 1966 * @param x X in local coordinate 1967 * @param y Y in local coordinate 1968 * @return The rowId of the item which contains the specified point, or {@link #INVALID_ROW_ID} 1969 * if the point does not intersect an item. 1970 */ 1971 public long pointToRowId(int x, int y) { 1972 int position = pointToPosition(x, y); 1973 if (position >= 0) { 1974 return mAdapter.getItemId(position); 1975 } 1976 return INVALID_ROW_ID; 1977 } 1978 1979 final class CheckForTap implements Runnable { 1980 public void run() { 1981 if (mTouchMode == TOUCH_MODE_DOWN) { 1982 mTouchMode = TOUCH_MODE_TAP; 1983 final View child = getChildAt(mMotionPosition - mFirstPosition); 1984 if (child != null && !child.hasFocusable()) { 1985 mLayoutMode = LAYOUT_NORMAL; 1986 1987 if (!mDataChanged) { 1988 layoutChildren(); 1989 child.setPressed(true); 1990 positionSelector(child); 1991 setPressed(true); 1992 1993 final int longPressTimeout = ViewConfiguration.getLongPressTimeout(); 1994 final boolean longClickable = isLongClickable(); 1995 1996 if (mSelector != null) { 1997 Drawable d = mSelector.getCurrent(); 1998 if (d != null && d instanceof TransitionDrawable) { 1999 if (longClickable) { 2000 ((TransitionDrawable) d).startTransition(longPressTimeout); 2001 } else { 2002 ((TransitionDrawable) d).resetTransition(); 2003 } 2004 } 2005 } 2006 2007 if (longClickable) { 2008 if (mPendingCheckForLongPress == null) { 2009 mPendingCheckForLongPress = new CheckForLongPress(); 2010 } 2011 mPendingCheckForLongPress.rememberWindowAttachCount(); 2012 postDelayed(mPendingCheckForLongPress, longPressTimeout); 2013 } else { 2014 mTouchMode = TOUCH_MODE_DONE_WAITING; 2015 } 2016 } else { 2017 mTouchMode = TOUCH_MODE_DONE_WAITING; 2018 } 2019 } 2020 } 2021 } 2022 } 2023 2024 private boolean startScrollIfNeeded(int deltaY) { 2025 // Check if we have moved far enough that it looks more like a 2026 // scroll than a tap 2027 final int distance = Math.abs(deltaY); 2028 final boolean overscroll = mScrollY != 0; 2029 if (overscroll || distance > mTouchSlop) { 2030 createScrollingCache(); 2031 mTouchMode = overscroll ? TOUCH_MODE_OVERSCROLL : TOUCH_MODE_SCROLL; 2032 mMotionCorrection = deltaY; 2033 final Handler handler = getHandler(); 2034 // Handler should not be null unless the AbsListView is not attached to a 2035 // window, which would make it very hard to scroll it... but the monkeys 2036 // say it's possible. 2037 if (handler != null) { 2038 handler.removeCallbacks(mPendingCheckForLongPress); 2039 } 2040 setPressed(false); 2041 View motionView = getChildAt(mMotionPosition - mFirstPosition); 2042 if (motionView != null) { 2043 motionView.setPressed(false); 2044 } 2045 reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); 2046 // Time to start stealing events! Once we've stolen them, don't let anyone 2047 // steal from us 2048 requestDisallowInterceptTouchEvent(true); 2049 return true; 2050 } 2051 2052 return false; 2053 } 2054 2055 public void onTouchModeChanged(boolean isInTouchMode) { 2056 if (isInTouchMode) { 2057 // Get rid of the selection when we enter touch mode 2058 hideSelector(); 2059 // Layout, but only if we already have done so previously. 2060 // (Otherwise may clobber a LAYOUT_SYNC layout that was requested to restore 2061 // state.) 2062 if (getHeight() > 0 && getChildCount() > 0) { 2063 // We do not lose focus initiating a touch (since AbsListView is focusable in 2064 // touch mode). Force an initial layout to get rid of the selection. 2065 layoutChildren(); 2066 } 2067 } else { 2068 int touchMode = mTouchMode; 2069 if (touchMode == TOUCH_MODE_OVERSCROLL || touchMode == TOUCH_MODE_OVERFLING) { 2070 if (mFlingRunnable != null) { 2071 mFlingRunnable.endFling(); 2072 } 2073 2074 if (mScrollY != 0) { 2075 mScrollY = 0; 2076 finishGlows(); 2077 invalidate(); 2078 } 2079 } 2080 } 2081 } 2082 2083 @Override 2084 public boolean onTouchEvent(MotionEvent ev) { 2085 if (!isEnabled()) { 2086 // A disabled view that is clickable still consumes the touch 2087 // events, it just doesn't respond to them. 2088 return isClickable() || isLongClickable(); 2089 } 2090 2091 if (mFastScroller != null) { 2092 boolean intercepted = mFastScroller.onTouchEvent(ev); 2093 if (intercepted) { 2094 return true; 2095 } 2096 } 2097 2098 final int action = ev.getAction(); 2099 2100 View v; 2101 int deltaY; 2102 2103 if (mVelocityTracker == null) { 2104 mVelocityTracker = VelocityTracker.obtain(); 2105 } 2106 mVelocityTracker.addMovement(ev); 2107 2108 switch (action & MotionEvent.ACTION_MASK) { 2109 case MotionEvent.ACTION_DOWN: { 2110 switch (mTouchMode) { 2111 case TOUCH_MODE_OVERFLING: { 2112 mFlingRunnable.endFling(); 2113 mTouchMode = TOUCH_MODE_OVERSCROLL; 2114 mMotionY = mLastY = (int) ev.getY(); 2115 mMotionCorrection = 0; 2116 mActivePointerId = ev.getPointerId(0); 2117 break; 2118 } 2119 2120 default: { 2121 mActivePointerId = ev.getPointerId(0); 2122 final int x = (int) ev.getX(); 2123 final int y = (int) ev.getY(); 2124 int motionPosition = pointToPosition(x, y); 2125 if (!mDataChanged) { 2126 if ((mTouchMode != TOUCH_MODE_FLING) && (motionPosition >= 0) 2127 && (getAdapter().isEnabled(motionPosition))) { 2128 // User clicked on an actual view (and was not stopping a fling). It might be a 2129 // click or a scroll. Assume it is a click until proven otherwise 2130 mTouchMode = TOUCH_MODE_DOWN; 2131 // FIXME Debounce 2132 if (mPendingCheckForTap == null) { 2133 mPendingCheckForTap = new CheckForTap(); 2134 } 2135 postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); 2136 } else { 2137 if (ev.getEdgeFlags() != 0 && motionPosition < 0) { 2138 // If we couldn't find a view to click on, but the down event was touching 2139 // the edge, we will bail out and try again. This allows the edge correcting 2140 // code in ViewRoot to try to find a nearby view to select 2141 return false; 2142 } 2143 2144 if (mTouchMode == TOUCH_MODE_FLING) { 2145 // Stopped a fling. It is a scroll. 2146 createScrollingCache(); 2147 mTouchMode = TOUCH_MODE_SCROLL; 2148 mMotionCorrection = 0; 2149 motionPosition = findMotionRow(y); 2150 reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); 2151 } 2152 } 2153 } 2154 2155 if (motionPosition >= 0) { 2156 // Remember where the motion event started 2157 v = getChildAt(motionPosition - mFirstPosition); 2158 mMotionViewOriginalTop = v.getTop(); 2159 } 2160 mMotionX = x; 2161 mMotionY = y; 2162 mMotionPosition = motionPosition; 2163 mLastY = Integer.MIN_VALUE; 2164 break; 2165 } 2166 } 2167 break; 2168 } 2169 2170 case MotionEvent.ACTION_MOVE: { 2171 final int pointerIndex = ev.findPointerIndex(mActivePointerId); 2172 final int y = (int) ev.getY(pointerIndex); 2173 deltaY = y - mMotionY; 2174 switch (mTouchMode) { 2175 case TOUCH_MODE_DOWN: 2176 case TOUCH_MODE_TAP: 2177 case TOUCH_MODE_DONE_WAITING: 2178 // Check if we have moved far enough that it looks more like a 2179 // scroll than a tap 2180 startScrollIfNeeded(deltaY); 2181 break; 2182 case TOUCH_MODE_SCROLL: 2183 if (PROFILE_SCROLLING) { 2184 if (!mScrollProfilingStarted) { 2185 Debug.startMethodTracing("AbsListViewScroll"); 2186 mScrollProfilingStarted = true; 2187 } 2188 } 2189 2190 if (y != mLastY) { 2191 // We may be here after stopping a fling and continuing to scroll. 2192 // If so, we haven't disallowed intercepting touch events yet. 2193 // Make sure that we do so in case we're in a parent that can intercept. 2194 if ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) == 0 && 2195 Math.abs(deltaY) > mTouchSlop) { 2196 requestDisallowInterceptTouchEvent(true); 2197 } 2198 2199 final int rawDeltaY = deltaY; 2200 deltaY -= mMotionCorrection; 2201 int incrementalDeltaY = mLastY != Integer.MIN_VALUE ? y - mLastY : deltaY; 2202 2203 final int motionIndex; 2204 if (mMotionPosition >= 0) { 2205 motionIndex = mMotionPosition - mFirstPosition; 2206 } else { 2207 // If we don't have a motion position that we can reliably track, 2208 // pick something in the middle to make a best guess at things below. 2209 motionIndex = getChildCount() / 2; 2210 } 2211 2212 int motionViewPrevTop = 0; 2213 View motionView = this.getChildAt(motionIndex); 2214 if (motionView != null) { 2215 motionViewPrevTop = motionView.getTop(); 2216 } 2217 2218 // No need to do all this work if we're not going to move anyway 2219 boolean atEdge = false; 2220 if (incrementalDeltaY != 0) { 2221 atEdge = trackMotionScroll(deltaY, incrementalDeltaY); 2222 } 2223 2224 // Check to see if we have bumped into the scroll limit 2225 motionView = this.getChildAt(motionIndex); 2226 if (motionView != null) { 2227 // Check if the top of the motion view is where it is 2228 // supposed to be 2229 final int motionViewRealTop = motionView.getTop(); 2230 if (atEdge) { 2231 // Apply overscroll 2232 2233 int overscroll = -incrementalDeltaY - 2234 (motionViewRealTop - motionViewPrevTop); 2235 overScrollBy(0, overscroll, 0, mScrollY, 0, 0, 2236 0, mOverscrollDistance, true); 2237 if (Math.abs(mOverscrollDistance) == Math.abs(mScrollY)) { 2238 // Don't allow overfling if we're at the edge. 2239 mVelocityTracker.clear(); 2240 } 2241 2242 final int overscrollMode = getOverScrollMode(); 2243 if (overscrollMode == OVER_SCROLL_ALWAYS || 2244 (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && 2245 !contentFits())) { 2246 mDirection = 0; // Reset when entering overscroll. 2247 mTouchMode = TOUCH_MODE_OVERSCROLL; 2248 if (rawDeltaY > 0) { 2249 mEdgeGlowTop.onPull((float) overscroll / getHeight()); 2250 if (!mEdgeGlowBottom.isFinished()) { 2251 mEdgeGlowBottom.onRelease(); 2252 } 2253 } else if (rawDeltaY < 0) { 2254 mEdgeGlowBottom.onPull((float) overscroll / getHeight()); 2255 if (!mEdgeGlowTop.isFinished()) { 2256 mEdgeGlowTop.onRelease(); 2257 } 2258 } 2259 } 2260 } 2261 mMotionY = y; 2262 invalidate(); 2263 } 2264 mLastY = y; 2265 } 2266 break; 2267 2268 case TOUCH_MODE_OVERSCROLL: 2269 if (y != mLastY) { 2270 final int rawDeltaY = deltaY; 2271 deltaY -= mMotionCorrection; 2272 int incrementalDeltaY = mLastY != Integer.MIN_VALUE ? y - mLastY : deltaY; 2273 2274 final int oldScroll = mScrollY; 2275 final int newScroll = oldScroll - incrementalDeltaY; 2276 int newDirection = y > mLastY ? 1 : -1; 2277 2278 if (mDirection == 0) { 2279 mDirection = newDirection; 2280 } 2281 2282 if (mDirection != newDirection) { 2283 // Coming back to 'real' list scrolling 2284 incrementalDeltaY = -newScroll; 2285 mScrollY = 0; 2286 2287 // No need to do all this work if we're not going to move anyway 2288 if (incrementalDeltaY != 0) { 2289 trackMotionScroll(incrementalDeltaY, incrementalDeltaY); 2290 } 2291 2292 // Check to see if we are back in 2293 View motionView = this.getChildAt(mMotionPosition - mFirstPosition); 2294 if (motionView != null) { 2295 mTouchMode = TOUCH_MODE_SCROLL; 2296 2297 // We did not scroll the full amount. Treat this essentially like the 2298 // start of a new touch scroll 2299 final int motionPosition = findClosestMotionRow(y); 2300 2301 mMotionCorrection = 0; 2302 motionView = getChildAt(motionPosition - mFirstPosition); 2303 mMotionViewOriginalTop = motionView.getTop(); 2304 mMotionY = y; 2305 mMotionPosition = motionPosition; 2306 } 2307 } else { 2308 overScrollBy(0, -incrementalDeltaY, 0, mScrollY, 0, 0, 2309 0, mOverscrollDistance, true); 2310 final int overscrollMode = getOverScrollMode(); 2311 if (overscrollMode == OVER_SCROLL_ALWAYS || 2312 (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && 2313 !contentFits())) { 2314 if (rawDeltaY > 0) { 2315 mEdgeGlowTop.onPull((float) -incrementalDeltaY / getHeight()); 2316 if (!mEdgeGlowBottom.isFinished()) { 2317 mEdgeGlowBottom.onRelease(); 2318 } 2319 } else if (rawDeltaY < 0) { 2320 mEdgeGlowBottom.onPull((float) -incrementalDeltaY / getHeight()); 2321 if (!mEdgeGlowTop.isFinished()) { 2322 mEdgeGlowTop.onRelease(); 2323 } 2324 } 2325 invalidate(); 2326 } 2327 if (Math.abs(mOverscrollDistance) == Math.abs(mScrollY)) { 2328 // Don't allow overfling if we're at the edge. 2329 mVelocityTracker.clear(); 2330 } 2331 } 2332 mLastY = y; 2333 mDirection = newDirection; 2334 } 2335 break; 2336 } 2337 2338 break; 2339 } 2340 2341 case MotionEvent.ACTION_UP: { 2342 switch (mTouchMode) { 2343 case TOUCH_MODE_DOWN: 2344 case TOUCH_MODE_TAP: 2345 case TOUCH_MODE_DONE_WAITING: 2346 final int motionPosition = mMotionPosition; 2347 final View child = getChildAt(motionPosition - mFirstPosition); 2348 if (child != null && !child.hasFocusable()) { 2349 if (mTouchMode != TOUCH_MODE_DOWN) { 2350 child.setPressed(false); 2351 } 2352 2353 if (mPerformClick == null) { 2354 mPerformClick = new PerformClick(); 2355 } 2356 2357 final AbsListView.PerformClick performClick = mPerformClick; 2358 performClick.mChild = child; 2359 performClick.mClickMotionPosition = motionPosition; 2360 performClick.rememberWindowAttachCount(); 2361 2362 mResurrectToPosition = motionPosition; 2363 2364 if (mTouchMode == TOUCH_MODE_DOWN || mTouchMode == TOUCH_MODE_TAP) { 2365 final Handler handler = getHandler(); 2366 if (handler != null) { 2367 handler.removeCallbacks(mTouchMode == TOUCH_MODE_DOWN ? 2368 mPendingCheckForTap : mPendingCheckForLongPress); 2369 } 2370 mLayoutMode = LAYOUT_NORMAL; 2371 if (!mDataChanged && mAdapter.isEnabled(motionPosition)) { 2372 mTouchMode = TOUCH_MODE_TAP; 2373 setSelectedPositionInt(mMotionPosition); 2374 layoutChildren(); 2375 child.setPressed(true); 2376 positionSelector(child); 2377 setPressed(true); 2378 if (mSelector != null) { 2379 Drawable d = mSelector.getCurrent(); 2380 if (d != null && d instanceof TransitionDrawable) { 2381 ((TransitionDrawable) d).resetTransition(); 2382 } 2383 } 2384 postDelayed(new Runnable() { 2385 public void run() { 2386 child.setPressed(false); 2387 setPressed(false); 2388 if (!mDataChanged) { 2389 post(performClick); 2390 } 2391 mTouchMode = TOUCH_MODE_REST; 2392 } 2393 }, ViewConfiguration.getPressedStateDuration()); 2394 } else { 2395 mTouchMode = TOUCH_MODE_REST; 2396 } 2397 return true; 2398 } else if (!mDataChanged && mAdapter.isEnabled(motionPosition)) { 2399 post(performClick); 2400 } 2401 } 2402 mTouchMode = TOUCH_MODE_REST; 2403 break; 2404 case TOUCH_MODE_SCROLL: 2405 final int childCount = getChildCount(); 2406 if (childCount > 0) { 2407 final int firstChildTop = getChildAt(0).getTop(); 2408 final int lastChildBottom = getChildAt(childCount - 1).getBottom(); 2409 final int contentTop = mListPadding.top; 2410 final int contentBottom = getHeight() - mListPadding.bottom; 2411 if (mFirstPosition == 0 && firstChildTop >= contentTop && 2412 mFirstPosition + childCount < mItemCount && 2413 lastChildBottom <= getHeight() - contentBottom) { 2414 mTouchMode = TOUCH_MODE_REST; 2415 reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); 2416 } else { 2417 final VelocityTracker velocityTracker = mVelocityTracker; 2418 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 2419 final int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId); 2420 2421 // Fling if we have enough velocity and we aren't at a boundary. 2422 // Since we can potentially overfling more than we can overscroll, don't 2423 // allow the weird behavior where you can scroll to a boundary then 2424 // fling further. 2425 if (Math.abs(initialVelocity) > mMinimumVelocity && 2426 !((mFirstPosition == 0 && 2427 firstChildTop == contentTop - mOverscrollDistance) || 2428 (mFirstPosition + childCount == mItemCount && 2429 lastChildBottom == contentBottom + mOverscrollDistance))) { 2430 if (mFlingRunnable == null) { 2431 mFlingRunnable = new FlingRunnable(); 2432 } 2433 reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING); 2434 2435 mFlingRunnable.start(-initialVelocity); 2436 } else { 2437 mTouchMode = TOUCH_MODE_REST; 2438 reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); 2439 } 2440 } 2441 } else { 2442 mTouchMode = TOUCH_MODE_REST; 2443 reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); 2444 } 2445 break; 2446 2447 case TOUCH_MODE_OVERSCROLL: 2448 if (mFlingRunnable == null) { 2449 mFlingRunnable = new FlingRunnable(); 2450 } 2451 final VelocityTracker velocityTracker = mVelocityTracker; 2452 velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 2453 final int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId); 2454 2455 reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING); 2456 if (Math.abs(initialVelocity) > mMinimumVelocity) { 2457 mFlingRunnable.startOverfling(-initialVelocity); 2458 } else { 2459 mFlingRunnable.startSpringback(); 2460 } 2461 2462 break; 2463 } 2464 2465 setPressed(false); 2466 2467 if (mEdgeGlowTop != null) { 2468 mEdgeGlowTop.onRelease(); 2469 mEdgeGlowBottom.onRelease(); 2470 } 2471 2472 // Need to redraw since we probably aren't drawing the selector anymore 2473 invalidate(); 2474 2475 final Handler handler = getHandler(); 2476 if (handler != null) { 2477 handler.removeCallbacks(mPendingCheckForLongPress); 2478 } 2479 2480 if (mVelocityTracker != null) { 2481 mVelocityTracker.recycle(); 2482 mVelocityTracker = null; 2483 } 2484 2485 mActivePointerId = INVALID_POINTER; 2486 2487 if (PROFILE_SCROLLING) { 2488 if (mScrollProfilingStarted) { 2489 Debug.stopMethodTracing(); 2490 mScrollProfilingStarted = false; 2491 } 2492 } 2493 break; 2494 } 2495 2496 case MotionEvent.ACTION_CANCEL: { 2497 switch (mTouchMode) { 2498 case TOUCH_MODE_OVERSCROLL: 2499 if (mFlingRunnable == null) { 2500 mFlingRunnable = new FlingRunnable(); 2501 } 2502 mFlingRunnable.startSpringback(); 2503 break; 2504 2505 case TOUCH_MODE_OVERFLING: 2506 // Do nothing - let it play out. 2507 break; 2508 2509 default: 2510 mTouchMode = TOUCH_MODE_REST; 2511 setPressed(false); 2512 View motionView = this.getChildAt(mMotionPosition - mFirstPosition); 2513 if (motionView != null) { 2514 motionView.setPressed(false); 2515 } 2516 clearScrollingCache(); 2517 2518 final Handler handler = getHandler(); 2519 if (handler != null) { 2520 handler.removeCallbacks(mPendingCheckForLongPress); 2521 } 2522 2523 if (mVelocityTracker != null) { 2524 mVelocityTracker.recycle(); 2525 mVelocityTracker = null; 2526 } 2527 } 2528 2529 if (mEdgeGlowTop != null) { 2530 mEdgeGlowTop.onRelease(); 2531 mEdgeGlowBottom.onRelease(); 2532 } 2533 mActivePointerId = INVALID_POINTER; 2534 break; 2535 } 2536 2537 case MotionEvent.ACTION_POINTER_UP: { 2538 onSecondaryPointerUp(ev); 2539 final int x = mMotionX; 2540 final int y = mMotionY; 2541 final int motionPosition = pointToPosition(x, y); 2542 if (motionPosition >= 0) { 2543 // Remember where the motion event started 2544 v = getChildAt(motionPosition - mFirstPosition); 2545 mMotionViewOriginalTop = v.getTop(); 2546 mMotionPosition = motionPosition; 2547 } 2548 mLastY = y; 2549 break; 2550 } 2551 } 2552 2553 return true; 2554 } 2555 2556 @Override 2557 protected void onOverScrolled(int scrollX, int scrollY, 2558 boolean clampedX, boolean clampedY) { 2559 mScrollY = scrollY; 2560 2561 if (clampedY) { 2562 // Velocity is broken by hitting the limit; don't start a fling off of this. 2563 if (mVelocityTracker != null) { 2564 mVelocityTracker.clear(); 2565 } 2566 } 2567 awakenScrollBars(); 2568 } 2569 2570 @Override 2571 public void draw(Canvas canvas) { 2572 super.draw(canvas); 2573 if (mEdgeGlowTop != null) { 2574 final int scrollY = mScrollY; 2575 if (!mEdgeGlowTop.isFinished()) { 2576 final int restoreCount = canvas.save(); 2577 final int width = getWidth(); 2578 2579 canvas.translate(-width / 2, Math.min(0, scrollY + mFirstPositionDistanceGuess)); 2580 mEdgeGlowTop.setSize(width * 2, getHeight()); 2581 if (mEdgeGlowTop.draw(canvas)) { 2582 invalidate(); 2583 } 2584 canvas.restoreToCount(restoreCount); 2585 } 2586 if (!mEdgeGlowBottom.isFinished()) { 2587 final int restoreCount = canvas.save(); 2588 final int width = getWidth(); 2589 final int height = getHeight(); 2590 2591 canvas.translate(-width / 2, 2592 Math.max(height, scrollY + mLastPositionDistanceGuess)); 2593 canvas.rotate(180, width, 0); 2594 mEdgeGlowBottom.setSize(width * 2, height); 2595 if (mEdgeGlowBottom.draw(canvas)) { 2596 invalidate(); 2597 } 2598 canvas.restoreToCount(restoreCount); 2599 } 2600 } 2601 if (mFastScroller != null) { 2602 final int scrollY = mScrollY; 2603 if (scrollY != 0) { 2604 // Pin to the top/bottom during overscroll 2605 int restoreCount = canvas.save(); 2606 canvas.translate(0, (float) scrollY); 2607 mFastScroller.draw(canvas); 2608 canvas.restoreToCount(restoreCount); 2609 } else { 2610 mFastScroller.draw(canvas); 2611 } 2612 } 2613 } 2614 2615 @Override 2616 public boolean onInterceptTouchEvent(MotionEvent ev) { 2617 int action = ev.getAction(); 2618 View v; 2619 2620 if (mFastScroller != null) { 2621 boolean intercepted = mFastScroller.onInterceptTouchEvent(ev); 2622 if (intercepted) { 2623 return true; 2624 } 2625 } 2626 2627 switch (action & MotionEvent.ACTION_MASK) { 2628 case MotionEvent.ACTION_DOWN: { 2629 int touchMode = mTouchMode; 2630 if (touchMode == TOUCH_MODE_OVERFLING || touchMode == TOUCH_MODE_OVERSCROLL) { 2631 mMotionCorrection = 0; 2632 return true; 2633 } 2634 2635 final int x = (int) ev.getX(); 2636 final int y = (int) ev.getY(); 2637 mActivePointerId = ev.getPointerId(0); 2638 2639 int motionPosition = findMotionRow(y); 2640 if (touchMode != TOUCH_MODE_FLING && motionPosition >= 0) { 2641 // User clicked on an actual view (and was not stopping a fling). 2642 // Remember where the motion event started 2643 v = getChildAt(motionPosition - mFirstPosition); 2644 mMotionViewOriginalTop = v.getTop(); 2645 mMotionX = x; 2646 mMotionY = y; 2647 mMotionPosition = motionPosition; 2648 mTouchMode = TOUCH_MODE_DOWN; 2649 clearScrollingCache(); 2650 } 2651 mLastY = Integer.MIN_VALUE; 2652 if (touchMode == TOUCH_MODE_FLING) { 2653 return true; 2654 } 2655 break; 2656 } 2657 2658 case MotionEvent.ACTION_MOVE: { 2659 switch (mTouchMode) { 2660 case TOUCH_MODE_DOWN: 2661 final int pointerIndex = ev.findPointerIndex(mActivePointerId); 2662 final int y = (int) ev.getY(pointerIndex); 2663 if (startScrollIfNeeded(y - mMotionY)) { 2664 return true; 2665 } 2666 break; 2667 } 2668 break; 2669 } 2670 2671 case MotionEvent.ACTION_UP: { 2672 mTouchMode = TOUCH_MODE_REST; 2673 mActivePointerId = INVALID_POINTER; 2674 reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); 2675 break; 2676 } 2677 2678 case MotionEvent.ACTION_POINTER_UP: { 2679 onSecondaryPointerUp(ev); 2680 break; 2681 } 2682 } 2683 2684 return false; 2685 } 2686 2687 private void onSecondaryPointerUp(MotionEvent ev) { 2688 final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> 2689 MotionEvent.ACTION_POINTER_INDEX_SHIFT; 2690 final int pointerId = ev.getPointerId(pointerIndex); 2691 if (pointerId == mActivePointerId) { 2692 // This was our active pointer going up. Choose a new 2693 // active pointer and adjust accordingly. 2694 // TODO: Make this decision more intelligent. 2695 final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 2696 mMotionX = (int) ev.getX(newPointerIndex); 2697 mMotionY = (int) ev.getY(newPointerIndex); 2698 mMotionCorrection = 0; 2699 mActivePointerId = ev.getPointerId(newPointerIndex); 2700 if (mVelocityTracker != null) { 2701 mVelocityTracker.clear(); 2702 } 2703 } 2704 } 2705 2706 /** 2707 * {@inheritDoc} 2708 */ 2709 @Override 2710 public void addTouchables(ArrayList<View> views) { 2711 final int count = getChildCount(); 2712 final int firstPosition = mFirstPosition; 2713 final ListAdapter adapter = mAdapter; 2714 2715 if (adapter == null) { 2716 return; 2717 } 2718 2719 for (int i = 0; i < count; i++) { 2720 final View child = getChildAt(i); 2721 if (adapter.isEnabled(firstPosition + i)) { 2722 views.add(child); 2723 } 2724 child.addTouchables(views); 2725 } 2726 } 2727 2728 /** 2729 * Fires an "on scroll state changed" event to the registered 2730 * {@link android.widget.AbsListView.OnScrollListener}, if any. The state change 2731 * is fired only if the specified state is different from the previously known state. 2732 * 2733 * @param newState The new scroll state. 2734 */ 2735 void reportScrollStateChange(int newState) { 2736 if (newState != mLastScrollState) { 2737 if (mOnScrollListener != null) { 2738 mOnScrollListener.onScrollStateChanged(this, newState); 2739 mLastScrollState = newState; 2740 } 2741 } 2742 } 2743 2744 /** 2745 * Responsible for fling behavior. Use {@link #start(int)} to 2746 * initiate a fling. Each frame of the fling is handled in {@link #run()}. 2747 * A FlingRunnable will keep re-posting itself until the fling is done. 2748 * 2749 */ 2750 private class FlingRunnable implements Runnable { 2751 /** 2752 * Tracks the decay of a fling scroll 2753 */ 2754 private final OverScroller mScroller; 2755 2756 /** 2757 * Y value reported by mScroller on the previous fling 2758 */ 2759 private int mLastFlingY; 2760 2761 FlingRunnable() { 2762 mScroller = new OverScroller(getContext()); 2763 } 2764 2765 void start(int initialVelocity) { 2766 int initialY = initialVelocity < 0 ? Integer.MAX_VALUE : 0; 2767 mLastFlingY = initialY; 2768 mScroller.fling(0, initialY, 0, initialVelocity, 2769 0, Integer.MAX_VALUE, 0, Integer.MAX_VALUE); 2770 mTouchMode = TOUCH_MODE_FLING; 2771 post(this); 2772 2773 if (PROFILE_FLINGING) { 2774 if (!mFlingProfilingStarted) { 2775 Debug.startMethodTracing("AbsListViewFling"); 2776 mFlingProfilingStarted = true; 2777 } 2778 } 2779 } 2780 2781 void startSpringback() { 2782 if (mScroller.springBack(0, mScrollY, 0, 0, 0, 0)) { 2783 mTouchMode = TOUCH_MODE_OVERFLING; 2784 invalidate(); 2785 post(this); 2786 } else { 2787 mTouchMode = TOUCH_MODE_REST; 2788 } 2789 } 2790 2791 void startOverfling(int initialVelocity) { 2792 final int min = mScrollY > 0 ? Integer.MIN_VALUE : 0; 2793 final int max = mScrollY > 0 ? 0 : Integer.MAX_VALUE; 2794 mScroller.fling(0, mScrollY, 0, initialVelocity, 0, 0, min, max, 0, getHeight()); 2795 mTouchMode = TOUCH_MODE_OVERFLING; 2796 invalidate(); 2797 post(this); 2798 } 2799 2800 void edgeReached(int delta) { 2801 mScroller.notifyVerticalEdgeReached(mScrollY, 0, mOverflingDistance); 2802 final int overscrollMode = getOverScrollMode(); 2803 if (overscrollMode == OVER_SCROLL_ALWAYS || 2804 (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && !contentFits())) { 2805 mTouchMode = TOUCH_MODE_OVERFLING; 2806 final int vel = (int) mScroller.getCurrVelocity(); 2807 if (delta > 0) { 2808 mEdgeGlowTop.onAbsorb(vel); 2809 } else { 2810 mEdgeGlowBottom.onAbsorb(vel); 2811 } 2812 } 2813 invalidate(); 2814 post(this); 2815 } 2816 2817 void startScroll(int distance, int duration) { 2818 int initialY = distance < 0 ? Integer.MAX_VALUE : 0; 2819 mLastFlingY = initialY; 2820 mScroller.startScroll(0, initialY, 0, distance, duration); 2821 mTouchMode = TOUCH_MODE_FLING; 2822 post(this); 2823 } 2824 2825 private void endFling() { 2826 mTouchMode = TOUCH_MODE_REST; 2827 2828 reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); 2829 clearScrollingCache(); 2830 2831 removeCallbacks(this); 2832 2833 if (mPositionScroller != null) { 2834 removeCallbacks(mPositionScroller); 2835 } 2836 } 2837 2838 public void run() { 2839 switch (mTouchMode) { 2840 default: 2841 return; 2842 2843 case TOUCH_MODE_FLING: { 2844 if (mItemCount == 0 || getChildCount() == 0) { 2845 endFling(); 2846 return; 2847 } 2848 2849 final OverScroller scroller = mScroller; 2850 boolean more = scroller.computeScrollOffset(); 2851 final int y = scroller.getCurrY(); 2852 2853 // Flip sign to convert finger direction to list items direction 2854 // (e.g. finger moving down means list is moving towards the top) 2855 int delta = mLastFlingY - y; 2856 2857 // Pretend that each frame of a fling scroll is a touch scroll 2858 if (delta > 0) { 2859 // List is moving towards the top. Use first view as mMotionPosition 2860 mMotionPosition = mFirstPosition; 2861 final View firstView = getChildAt(0); 2862 mMotionViewOriginalTop = firstView.getTop(); 2863 2864 // Don't fling more than 1 screen 2865 delta = Math.min(getHeight() - mPaddingBottom - mPaddingTop - 1, delta); 2866 } else { 2867 // List is moving towards the bottom. Use last view as mMotionPosition 2868 int offsetToLast = getChildCount() - 1; 2869 mMotionPosition = mFirstPosition + offsetToLast; 2870 2871 final View lastView = getChildAt(offsetToLast); 2872 mMotionViewOriginalTop = lastView.getTop(); 2873 2874 // Don't fling more than 1 screen 2875 delta = Math.max(-(getHeight() - mPaddingBottom - mPaddingTop - 1), delta); 2876 } 2877 2878 // Check to see if we have bumped into the scroll limit 2879 View motionView = getChildAt(mMotionPosition - mFirstPosition); 2880 int oldTop = 0; 2881 if (motionView != null) { 2882 oldTop = motionView.getTop(); 2883 } 2884 2885 final boolean atEnd = trackMotionScroll(delta, delta); 2886 if (atEnd) { 2887 if (motionView != null) { 2888 // Tweak the scroll for how far we overshot 2889 int overshoot = -(delta - (motionView.getTop() - oldTop)); 2890 overScrollBy(0, overshoot, 0, mScrollY, 0, 0, 2891 0, mOverflingDistance, false); 2892 } 2893 if (more) { 2894 edgeReached(delta); 2895 } 2896 break; 2897 } 2898 2899 if (more && !atEnd) { 2900 invalidate(); 2901 mLastFlingY = y; 2902 post(this); 2903 } else { 2904 endFling(); 2905 2906 if (PROFILE_FLINGING) { 2907 if (mFlingProfilingStarted) { 2908 Debug.stopMethodTracing(); 2909 mFlingProfilingStarted = false; 2910 } 2911 } 2912 } 2913 break; 2914 } 2915 2916 case TOUCH_MODE_OVERFLING: { 2917 final OverScroller scroller = mScroller; 2918 if (scroller.computeScrollOffset()) { 2919 final int scrollY = mScrollY; 2920 final int deltaY = scroller.getCurrY() - scrollY; 2921 if (overScrollBy(0, deltaY, 0, scrollY, 0, 0, 2922 0, mOverflingDistance, false)) { 2923 startSpringback(); 2924 } else { 2925 invalidate(); 2926 post(this); 2927 } 2928 } else { 2929 endFling(); 2930 } 2931 break; 2932 } 2933 } 2934 2935 } 2936 } 2937 2938 2939 class PositionScroller implements Runnable { 2940 private static final int SCROLL_DURATION = 400; 2941 2942 private static final int MOVE_DOWN_POS = 1; 2943 private static final int MOVE_UP_POS = 2; 2944 private static final int MOVE_DOWN_BOUND = 3; 2945 private static final int MOVE_UP_BOUND = 4; 2946 2947 private int mMode; 2948 private int mTargetPos; 2949 private int mBoundPos; 2950 private int mLastSeenPos; 2951 private int mScrollDuration; 2952 private final int mExtraScroll; 2953 2954 PositionScroller() { 2955 mExtraScroll = ViewConfiguration.get(mContext).getScaledFadingEdgeLength(); 2956 } 2957 2958 void start(int position) { 2959 final int firstPos = mFirstPosition; 2960 final int lastPos = firstPos + getChildCount() - 1; 2961 2962 int viewTravelCount = 0; 2963 if (position <= firstPos) { 2964 viewTravelCount = firstPos - position + 1; 2965 mMode = MOVE_UP_POS; 2966 } else if (position >= lastPos) { 2967 viewTravelCount = position - lastPos + 1; 2968 mMode = MOVE_DOWN_POS; 2969 } else { 2970 // Already on screen, nothing to do 2971 return; 2972 } 2973 2974 if (viewTravelCount > 0) { 2975 mScrollDuration = SCROLL_DURATION / viewTravelCount; 2976 } else { 2977 mScrollDuration = SCROLL_DURATION; 2978 } 2979 mTargetPos = position; 2980 mBoundPos = INVALID_POSITION; 2981 mLastSeenPos = INVALID_POSITION; 2982 2983 post(this); 2984 } 2985 2986 void start(int position, int boundPosition) { 2987 if (boundPosition == INVALID_POSITION) { 2988 start(position); 2989 return; 2990 } 2991 2992 final int firstPos = mFirstPosition; 2993 final int lastPos = firstPos + getChildCount() - 1; 2994 2995 int viewTravelCount = 0; 2996 if (position <= firstPos) { 2997 final int boundPosFromLast = lastPos - boundPosition; 2998 if (boundPosFromLast < 1) { 2999 // Moving would shift our bound position off the screen. Abort. 3000 return; 3001 } 3002 3003 final int posTravel = firstPos - position + 1; 3004 final int boundTravel = boundPosFromLast - 1; 3005 if (boundTravel < posTravel) { 3006 viewTravelCount = boundTravel; 3007 mMode = MOVE_UP_BOUND; 3008 } else { 3009 viewTravelCount = posTravel; 3010 mMode = MOVE_UP_POS; 3011 } 3012 } else if (position >= lastPos) { 3013 final int boundPosFromFirst = boundPosition - firstPos; 3014 if (boundPosFromFirst < 1) { 3015 // Moving would shift our bound position off the screen. Abort. 3016 return; 3017 } 3018 3019 final int posTravel = position - lastPos + 1; 3020 final int boundTravel = boundPosFromFirst - 1; 3021 if (boundTravel < posTravel) { 3022 viewTravelCount = boundTravel; 3023 mMode = MOVE_DOWN_BOUND; 3024 } else { 3025 viewTravelCount = posTravel; 3026 mMode = MOVE_DOWN_POS; 3027 } 3028 } else { 3029 // Already on screen, nothing to do 3030 return; 3031 } 3032 3033 if (viewTravelCount > 0) { 3034 mScrollDuration = SCROLL_DURATION / viewTravelCount; 3035 } else { 3036 mScrollDuration = SCROLL_DURATION; 3037 } 3038 mTargetPos = position; 3039 mBoundPos = boundPosition; 3040 mLastSeenPos = INVALID_POSITION; 3041 3042 post(this); 3043 } 3044 3045 void stop() { 3046 removeCallbacks(this); 3047 } 3048 3049 public void run() { 3050 final int listHeight = getHeight(); 3051 final int firstPos = mFirstPosition; 3052 3053 switch (mMode) { 3054 case MOVE_DOWN_POS: { 3055 final int lastViewIndex = getChildCount() - 1; 3056 final int lastPos = firstPos + lastViewIndex; 3057 3058 if (lastViewIndex < 0) { 3059 return; 3060 } 3061 3062 if (lastPos == mLastSeenPos) { 3063 // No new views, let things keep going. 3064 post(this); 3065 return; 3066 } 3067 3068 final View lastView = getChildAt(lastViewIndex); 3069 final int lastViewHeight = lastView.getHeight(); 3070 final int lastViewTop = lastView.getTop(); 3071 final int lastViewPixelsShowing = listHeight - lastViewTop; 3072 final int extraScroll = lastPos < mItemCount - 1 ? mExtraScroll : mListPadding.bottom; 3073 3074 smoothScrollBy(lastViewHeight - lastViewPixelsShowing + extraScroll, 3075 mScrollDuration); 3076 3077 mLastSeenPos = lastPos; 3078 if (lastPos < mTargetPos) { 3079 post(this); 3080 } 3081 break; 3082 } 3083 3084 case MOVE_DOWN_BOUND: { 3085 final int nextViewIndex = 1; 3086 final int childCount = getChildCount(); 3087 3088 if (firstPos == mBoundPos || childCount <= nextViewIndex 3089 || firstPos + childCount >= mItemCount) { 3090 return; 3091 } 3092 final int nextPos = firstPos + nextViewIndex; 3093 3094 if (nextPos == mLastSeenPos) { 3095 // No new views, let things keep going. 3096 post(this); 3097 return; 3098 } 3099 3100 final View nextView = getChildAt(nextViewIndex); 3101 final int nextViewHeight = nextView.getHeight(); 3102 final int nextViewTop = nextView.getTop(); 3103 final int extraScroll = mExtraScroll; 3104 if (nextPos < mBoundPos) { 3105 smoothScrollBy(Math.max(0, nextViewHeight + nextViewTop - extraScroll), 3106 mScrollDuration); 3107 3108 mLastSeenPos = nextPos; 3109 3110 post(this); 3111 } else { 3112 if (nextViewTop > extraScroll) { 3113 smoothScrollBy(nextViewTop - extraScroll, mScrollDuration); 3114 } 3115 } 3116 break; 3117 } 3118 3119 case MOVE_UP_POS: { 3120 if (firstPos == mLastSeenPos) { 3121 // No new views, let things keep going. 3122 post(this); 3123 return; 3124 } 3125 3126 final View firstView = getChildAt(0); 3127 if (firstView == null) { 3128 return; 3129 } 3130 final int firstViewTop = firstView.getTop(); 3131 final int extraScroll = firstPos > 0 ? mExtraScroll : mListPadding.top; 3132 3133 smoothScrollBy(firstViewTop - extraScroll, mScrollDuration); 3134 3135 mLastSeenPos = firstPos; 3136 3137 if (firstPos > mTargetPos) { 3138 post(this); 3139 } 3140 break; 3141 } 3142 3143 case MOVE_UP_BOUND: { 3144 final int lastViewIndex = getChildCount() - 2; 3145 if (lastViewIndex < 0) { 3146 return; 3147 } 3148 final int lastPos = firstPos + lastViewIndex; 3149 3150 if (lastPos == mLastSeenPos) { 3151 // No new views, let things keep going. 3152 post(this); 3153 return; 3154 } 3155 3156 final View lastView = getChildAt(lastViewIndex); 3157 final int lastViewHeight = lastView.getHeight(); 3158 final int lastViewTop = lastView.getTop(); 3159 final int lastViewPixelsShowing = listHeight - lastViewTop; 3160 mLastSeenPos = lastPos; 3161 if (lastPos > mBoundPos) { 3162 smoothScrollBy(-(lastViewPixelsShowing - mExtraScroll), mScrollDuration); 3163 post(this); 3164 } else { 3165 final int bottom = listHeight - mExtraScroll; 3166 final int lastViewBottom = lastViewTop + lastViewHeight; 3167 if (bottom > lastViewBottom) { 3168 smoothScrollBy(-(bottom - lastViewBottom), mScrollDuration); 3169 } 3170 } 3171 break; 3172 } 3173 3174 default: 3175 break; 3176 } 3177 } 3178 } 3179 3180 /** 3181 * Smoothly scroll to the specified adapter position. The view will 3182 * scroll such that the indicated position is displayed. 3183 * @param position Scroll to this adapter position. 3184 */ 3185 public void smoothScrollToPosition(int position) { 3186 if (mPositionScroller == null) { 3187 mPositionScroller = new PositionScroller(); 3188 } 3189 mPositionScroller.start(position); 3190 } 3191 3192 /** 3193 * Smoothly scroll to the specified adapter position. The view will 3194 * scroll such that the indicated position is displayed, but it will 3195 * stop early if scrolling further would scroll boundPosition out of 3196 * view. 3197 * @param position Scroll to this adapter position. 3198 * @param boundPosition Do not scroll if it would move this adapter 3199 * position out of view. 3200 */ 3201 public void smoothScrollToPosition(int position, int boundPosition) { 3202 if (mPositionScroller == null) { 3203 mPositionScroller = new PositionScroller(); 3204 } 3205 mPositionScroller.start(position, boundPosition); 3206 } 3207 3208 /** 3209 * Smoothly scroll by distance pixels over duration milliseconds. 3210 * @param distance Distance to scroll in pixels. 3211 * @param duration Duration of the scroll animation in milliseconds. 3212 */ 3213 public void smoothScrollBy(int distance, int duration) { 3214 if (mFlingRunnable == null) { 3215 mFlingRunnable = new FlingRunnable(); 3216 } else { 3217 mFlingRunnable.endFling(); 3218 } 3219 mFlingRunnable.startScroll(distance, duration); 3220 } 3221 3222 private void createScrollingCache() { 3223 if (mScrollingCacheEnabled && !mCachingStarted) { 3224 setChildrenDrawnWithCacheEnabled(true); 3225 setChildrenDrawingCacheEnabled(true); 3226 mCachingStarted = true; 3227 } 3228 } 3229 3230 private void clearScrollingCache() { 3231 if (mClearScrollingCache == null) { 3232 mClearScrollingCache = new Runnable() { 3233 public void run() { 3234 if (mCachingStarted) { 3235 mCachingStarted = false; 3236 setChildrenDrawnWithCacheEnabled(false); 3237 if ((mPersistentDrawingCache & PERSISTENT_SCROLLING_CACHE) == 0) { 3238 setChildrenDrawingCacheEnabled(false); 3239 } 3240 if (!isAlwaysDrawnWithCacheEnabled()) { 3241 invalidate(); 3242 } 3243 } 3244 } 3245 }; 3246 } 3247 post(mClearScrollingCache); 3248 } 3249 3250 /** 3251 * Track a motion scroll 3252 * 3253 * @param deltaY Amount to offset mMotionView. This is the accumulated delta since the motion 3254 * began. Positive numbers mean the user's finger is moving down the screen. 3255 * @param incrementalDeltaY Change in deltaY from the previous event. 3256 * @return true if we're already at the beginning/end of the list and have nothing to do. 3257 */ 3258 boolean trackMotionScroll(int deltaY, int incrementalDeltaY) { 3259 final int childCount = getChildCount(); 3260 if (childCount == 0) { 3261 return true; 3262 } 3263 3264 final int firstTop = getChildAt(0).getTop(); 3265 final int lastBottom = getChildAt(childCount - 1).getBottom(); 3266 3267 final Rect listPadding = mListPadding; 3268 3269 // FIXME account for grid vertical spacing too? 3270 final int spaceAbove = listPadding.top - firstTop; 3271 final int end = getHeight() - listPadding.bottom; 3272 final int spaceBelow = lastBottom - end; 3273 3274 final int height = getHeight() - mPaddingBottom - mPaddingTop; 3275 if (deltaY < 0) { 3276 deltaY = Math.max(-(height - 1), deltaY); 3277 } else { 3278 deltaY = Math.min(height - 1, deltaY); 3279 } 3280 3281 if (incrementalDeltaY < 0) { 3282 incrementalDeltaY = Math.max(-(height - 1), incrementalDeltaY); 3283 } else { 3284 incrementalDeltaY = Math.min(height - 1, incrementalDeltaY); 3285 } 3286 3287 final int firstPosition = mFirstPosition; 3288 3289 // Update our guesses for where the first and last views are 3290 if (firstPosition == 0) { 3291 mFirstPositionDistanceGuess = firstTop - mListPadding.top; 3292 } else { 3293 mFirstPositionDistanceGuess += incrementalDeltaY; 3294 } 3295 if (firstPosition + childCount == mItemCount) { 3296 mLastPositionDistanceGuess = lastBottom + mListPadding.bottom; 3297 } else { 3298 mLastPositionDistanceGuess += incrementalDeltaY; 3299 } 3300 3301 if (firstPosition == 0 && firstTop >= listPadding.top && incrementalDeltaY >= 0) { 3302 // Don't need to move views down if the top of the first position 3303 // is already visible 3304 return incrementalDeltaY != 0; 3305 } 3306 3307 if (firstPosition + childCount == mItemCount && lastBottom <= end && 3308 incrementalDeltaY <= 0) { 3309 // Don't need to move views up if the bottom of the last position 3310 // is already visible 3311 return incrementalDeltaY != 0; 3312 } 3313 3314 final boolean down = incrementalDeltaY < 0; 3315 3316 final boolean inTouchMode = isInTouchMode(); 3317 if (inTouchMode) { 3318 hideSelector(); 3319 } 3320 3321 final int headerViewsCount = getHeaderViewsCount(); 3322 final int footerViewsStart = mItemCount - getFooterViewsCount(); 3323 3324 int start = 0; 3325 int count = 0; 3326 3327 if (down) { 3328 final int top = listPadding.top - incrementalDeltaY; 3329 for (int i = 0; i < childCount; i++) { 3330 final View child = getChildAt(i); 3331 if (child.getBottom() >= top) { 3332 break; 3333 } else { 3334 count++; 3335 int position = firstPosition + i; 3336 if (position >= headerViewsCount && position < footerViewsStart) { 3337 mRecycler.addScrapView(child); 3338 3339 if (ViewDebug.TRACE_RECYCLER) { 3340 ViewDebug.trace(child, 3341 ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP, 3342 firstPosition + i, -1); 3343 } 3344 } 3345 } 3346 } 3347 } else { 3348 final int bottom = getHeight() - listPadding.bottom - incrementalDeltaY; 3349 for (int i = childCount - 1; i >= 0; i--) { 3350 final View child = getChildAt(i); 3351 if (child.getTop() <= bottom) { 3352 break; 3353 } else { 3354 start = i; 3355 count++; 3356 int position = firstPosition + i; 3357 if (position >= headerViewsCount && position < footerViewsStart) { 3358 mRecycler.addScrapView(child); 3359 3360 if (ViewDebug.TRACE_RECYCLER) { 3361 ViewDebug.trace(child, 3362 ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP, 3363 firstPosition + i, -1); 3364 } 3365 } 3366 } 3367 } 3368 } 3369 3370 mMotionViewNewTop = mMotionViewOriginalTop + deltaY; 3371 3372 mBlockLayoutRequests = true; 3373 3374 if (count > 0) { 3375 detachViewsFromParent(start, count); 3376 } 3377 offsetChildrenTopAndBottom(incrementalDeltaY); 3378 3379 if (down) { 3380 mFirstPosition += count; 3381 } 3382 3383 invalidate(); 3384 3385 final int absIncrementalDeltaY = Math.abs(incrementalDeltaY); 3386 if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) { 3387 fillGap(down); 3388 } 3389 3390 if (!inTouchMode && mSelectedPosition != INVALID_POSITION) { 3391 final int childIndex = mSelectedPosition - mFirstPosition; 3392 if (childIndex >= 0 && childIndex < getChildCount()) { 3393 positionSelector(getChildAt(childIndex)); 3394 } 3395 } 3396 3397 mBlockLayoutRequests = false; 3398 3399 invokeOnItemScrollListener(); 3400 awakenScrollBars(); 3401 3402 return false; 3403 } 3404 3405 /** 3406 * Returns the number of header views in the list. Header views are special views 3407 * at the top of the list that should not be recycled during a layout. 3408 * 3409 * @return The number of header views, 0 in the default implementation. 3410 */ 3411 int getHeaderViewsCount() { 3412 return 0; 3413 } 3414 3415 /** 3416 * Returns the number of footer views in the list. Footer views are special views 3417 * at the bottom of the list that should not be recycled during a layout. 3418 * 3419 * @return The number of footer views, 0 in the default implementation. 3420 */ 3421 int getFooterViewsCount() { 3422 return 0; 3423 } 3424 3425 /** 3426 * Fills the gap left open by a touch-scroll. During a touch scroll, children that 3427 * remain on screen are shifted and the other ones are discarded. The role of this 3428 * method is to fill the gap thus created by performing a partial layout in the 3429 * empty space. 3430 * 3431 * @param down true if the scroll is going down, false if it is going up 3432 */ 3433 abstract void fillGap(boolean down); 3434 3435 void hideSelector() { 3436 if (mSelectedPosition != INVALID_POSITION) { 3437 if (mLayoutMode != LAYOUT_SPECIFIC) { 3438 mResurrectToPosition = mSelectedPosition; 3439 } 3440 if (mNextSelectedPosition >= 0 && mNextSelectedPosition != mSelectedPosition) { 3441 mResurrectToPosition = mNextSelectedPosition; 3442 } 3443 setSelectedPositionInt(INVALID_POSITION); 3444 setNextSelectedPositionInt(INVALID_POSITION); 3445 mSelectedTop = 0; 3446 mSelectorRect.setEmpty(); 3447 } 3448 } 3449 3450 /** 3451 * @return A position to select. First we try mSelectedPosition. If that has been clobbered by 3452 * entering touch mode, we then try mResurrectToPosition. Values are pinned to the range 3453 * of items available in the adapter 3454 */ 3455 int reconcileSelectedPosition() { 3456 int position = mSelectedPosition; 3457 if (position < 0) { 3458 position = mResurrectToPosition; 3459 } 3460 position = Math.max(0, position); 3461 position = Math.min(position, mItemCount - 1); 3462 return position; 3463 } 3464 3465 /** 3466 * Find the row closest to y. This row will be used as the motion row when scrolling 3467 * 3468 * @param y Where the user touched 3469 * @return The position of the first (or only) item in the row containing y 3470 */ 3471 abstract int findMotionRow(int y); 3472 3473 /** 3474 * Find the row closest to y. This row will be used as the motion row when scrolling. 3475 * 3476 * @param y Where the user touched 3477 * @return The position of the first (or only) item in the row closest to y 3478 */ 3479 int findClosestMotionRow(int y) { 3480 final int childCount = getChildCount(); 3481 if (childCount == 0) { 3482 return INVALID_POSITION; 3483 } 3484 3485 final int motionRow = findMotionRow(y); 3486 return motionRow != INVALID_POSITION ? motionRow : mFirstPosition + childCount - 1; 3487 } 3488 3489 /** 3490 * Causes all the views to be rebuilt and redrawn. 3491 */ 3492 public void invalidateViews() { 3493 mDataChanged = true; 3494 rememberSyncState(); 3495 requestLayout(); 3496 invalidate(); 3497 } 3498 3499 /** 3500 * Makes the item at the supplied position selected. 3501 * 3502 * @param position the position of the new selection 3503 */ 3504 abstract void setSelectionInt(int position); 3505 3506 /** 3507 * Attempt to bring the selection back if the user is switching from touch 3508 * to trackball mode 3509 * @return Whether selection was set to something. 3510 */ 3511 boolean resurrectSelection() { 3512 final int childCount = getChildCount(); 3513 3514 if (childCount <= 0) { 3515 return false; 3516 } 3517 3518 int selectedTop = 0; 3519 int selectedPos; 3520 int childrenTop = mListPadding.top; 3521 int childrenBottom = mBottom - mTop - mListPadding.bottom; 3522 final int firstPosition = mFirstPosition; 3523 final int toPosition = mResurrectToPosition; 3524 boolean down = true; 3525 3526 if (toPosition >= firstPosition && toPosition < firstPosition + childCount) { 3527 selectedPos = toPosition; 3528 3529 final View selected = getChildAt(selectedPos - mFirstPosition); 3530 selectedTop = selected.getTop(); 3531 int selectedBottom = selected.getBottom(); 3532 3533 // We are scrolled, don't get in the fade 3534 if (selectedTop < childrenTop) { 3535 selectedTop = childrenTop + getVerticalFadingEdgeLength(); 3536 } else if (selectedBottom > childrenBottom) { 3537 selectedTop = childrenBottom - selected.getMeasuredHeight() 3538 - getVerticalFadingEdgeLength(); 3539 } 3540 } else { 3541 if (toPosition < firstPosition) { 3542 // Default to selecting whatever is first 3543 selectedPos = firstPosition; 3544 for (int i = 0; i < childCount; i++) { 3545 final View v = getChildAt(i); 3546 final int top = v.getTop(); 3547 3548 if (i == 0) { 3549 // Remember the position of the first item 3550 selectedTop = top; 3551 // See if we are scrolled at all 3552 if (firstPosition > 0 || top < childrenTop) { 3553 // If we are scrolled, don't select anything that is 3554 // in the fade region 3555 childrenTop += getVerticalFadingEdgeLength(); 3556 } 3557 } 3558 if (top >= childrenTop) { 3559 // Found a view whose top is fully visisble 3560 selectedPos = firstPosition + i; 3561 selectedTop = top; 3562 break; 3563 } 3564 } 3565 } else { 3566 final int itemCount = mItemCount; 3567 down = false; 3568 selectedPos = firstPosition + childCount - 1; 3569 3570 for (int i = childCount - 1; i >= 0; i--) { 3571 final View v = getChildAt(i); 3572 final int top = v.getTop(); 3573 final int bottom = v.getBottom(); 3574 3575 if (i == childCount - 1) { 3576 selectedTop = top; 3577 if (firstPosition + childCount < itemCount || bottom > childrenBottom) { 3578 childrenBottom -= getVerticalFadingEdgeLength(); 3579 } 3580 } 3581 3582 if (bottom <= childrenBottom) { 3583 selectedPos = firstPosition + i; 3584 selectedTop = top; 3585 break; 3586 } 3587 } 3588 } 3589 } 3590 3591 mResurrectToPosition = INVALID_POSITION; 3592 removeCallbacks(mFlingRunnable); 3593 mTouchMode = TOUCH_MODE_REST; 3594 clearScrollingCache(); 3595 mSpecificTop = selectedTop; 3596 selectedPos = lookForSelectablePosition(selectedPos, down); 3597 if (selectedPos >= firstPosition && selectedPos <= getLastVisiblePosition()) { 3598 mLayoutMode = LAYOUT_SPECIFIC; 3599 setSelectionInt(selectedPos); 3600 invokeOnItemScrollListener(); 3601 } else { 3602 selectedPos = INVALID_POSITION; 3603 } 3604 reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); 3605 3606 return selectedPos >= 0; 3607 } 3608 3609 @Override 3610 protected void handleDataChanged() { 3611 int count = mItemCount; 3612 if (count > 0) { 3613 3614 int newPos; 3615 3616 int selectablePos; 3617 3618 // Find the row we are supposed to sync to 3619 if (mNeedSync) { 3620 // Update this first, since setNextSelectedPositionInt inspects it 3621 mNeedSync = false; 3622 3623 if (mTranscriptMode == TRANSCRIPT_MODE_ALWAYS_SCROLL || 3624 (mTranscriptMode == TRANSCRIPT_MODE_NORMAL && 3625 mFirstPosition + getChildCount() >= mOldItemCount)) { 3626 mLayoutMode = LAYOUT_FORCE_BOTTOM; 3627 return; 3628 } 3629 3630 switch (mSyncMode) { 3631 case SYNC_SELECTED_POSITION: 3632 if (isInTouchMode()) { 3633 // We saved our state when not in touch mode. (We know this because 3634 // mSyncMode is SYNC_SELECTED_POSITION.) Now we are trying to 3635 // restore in touch mode. Just leave mSyncPosition as it is (possibly 3636 // adjusting if the available range changed) and return. 3637 mLayoutMode = LAYOUT_SYNC; 3638 mSyncPosition = Math.min(Math.max(0, mSyncPosition), count - 1); 3639 3640 return; 3641 } else { 3642 // See if we can find a position in the new data with the same 3643 // id as the old selection. This will change mSyncPosition. 3644 newPos = findSyncPosition(); 3645 if (newPos >= 0) { 3646 // Found it. Now verify that new selection is still selectable 3647 selectablePos = lookForSelectablePosition(newPos, true); 3648 if (selectablePos == newPos) { 3649 // Same row id is selected 3650 mSyncPosition = newPos; 3651 3652 if (mSyncHeight == getHeight()) { 3653 // If we are at the same height as when we saved state, try 3654 // to restore the scroll position too. 3655 mLayoutMode = LAYOUT_SYNC; 3656 } else { 3657 // We are not the same height as when the selection was saved, so 3658 // don't try to restore the exact position 3659 mLayoutMode = LAYOUT_SET_SELECTION; 3660 } 3661 3662 // Restore selection 3663 setNextSelectedPositionInt(newPos); 3664 return; 3665 } 3666 } 3667 } 3668 break; 3669 case SYNC_FIRST_POSITION: 3670 // Leave mSyncPosition as it is -- just pin to available range 3671 mLayoutMode = LAYOUT_SYNC; 3672 mSyncPosition = Math.min(Math.max(0, mSyncPosition), count - 1); 3673 3674 return; 3675 } 3676 } 3677 3678 if (!isInTouchMode()) { 3679 // We couldn't find matching data -- try to use the same position 3680 newPos = getSelectedItemPosition(); 3681 3682 // Pin position to the available range 3683 if (newPos >= count) { 3684 newPos = count - 1; 3685 } 3686 if (newPos < 0) { 3687 newPos = 0; 3688 } 3689 3690 // Make sure we select something selectable -- first look down 3691 selectablePos = lookForSelectablePosition(newPos, true); 3692 3693 if (selectablePos >= 0) { 3694 setNextSelectedPositionInt(selectablePos); 3695 return; 3696 } else { 3697 // Looking down didn't work -- try looking up 3698 selectablePos = lookForSelectablePosition(newPos, false); 3699 if (selectablePos >= 0) { 3700 setNextSelectedPositionInt(selectablePos); 3701 return; 3702 } 3703 } 3704 } else { 3705 3706 // We already know where we want to resurrect the selection 3707 if (mResurrectToPosition >= 0) { 3708 return; 3709 } 3710 } 3711 3712 } 3713 3714 // Nothing is selected. Give up and reset everything. 3715 mLayoutMode = mStackFromBottom ? LAYOUT_FORCE_BOTTOM : LAYOUT_FORCE_TOP; 3716 mSelectedPosition = INVALID_POSITION; 3717 mSelectedRowId = INVALID_ROW_ID; 3718 mNextSelectedPosition = INVALID_POSITION; 3719 mNextSelectedRowId = INVALID_ROW_ID; 3720 mNeedSync = false; 3721 checkSelectionChanged(); 3722 } 3723 3724 @Override 3725 protected void onDisplayHint(int hint) { 3726 super.onDisplayHint(hint); 3727 switch (hint) { 3728 case INVISIBLE: 3729 if (mPopup != null && mPopup.isShowing()) { 3730 dismissPopup(); 3731 } 3732 break; 3733 case VISIBLE: 3734 if (mFiltered && mPopup != null && !mPopup.isShowing()) { 3735 showPopup(); 3736 } 3737 break; 3738 } 3739 mPopupHidden = hint == INVISIBLE; 3740 } 3741 3742 /** 3743 * Removes the filter window 3744 */ 3745 private void dismissPopup() { 3746 if (mPopup != null) { 3747 mPopup.dismiss(); 3748 } 3749 } 3750 3751 /** 3752 * Shows the filter window 3753 */ 3754 private void showPopup() { 3755 // Make sure we have a window before showing the popup 3756 if (getWindowVisibility() == View.VISIBLE) { 3757 createTextFilter(true); 3758 positionPopup(); 3759 // Make sure we get focus if we are showing the popup 3760 checkFocus(); 3761 } 3762 } 3763 3764 private void positionPopup() { 3765 int screenHeight = getResources().getDisplayMetrics().heightPixels; 3766 final int[] xy = new int[2]; 3767 getLocationOnScreen(xy); 3768 // TODO: The 20 below should come from the theme 3769 // TODO: And the gravity should be defined in the theme as well 3770 final int bottomGap = screenHeight - xy[1] - getHeight() + (int) (mDensityScale * 20); 3771 if (!mPopup.isShowing()) { 3772 mPopup.showAtLocation(this, Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 3773 xy[0], bottomGap); 3774 } else { 3775 mPopup.update(xy[0], bottomGap, -1, -1); 3776 } 3777 } 3778 3779 /** 3780 * What is the distance between the source and destination rectangles given the direction of 3781 * focus navigation between them? The direction basically helps figure out more quickly what is 3782 * self evident by the relationship between the rects... 3783 * 3784 * @param source the source rectangle 3785 * @param dest the destination rectangle 3786 * @param direction the direction 3787 * @return the distance between the rectangles 3788 */ 3789 static int getDistance(Rect source, Rect dest, int direction) { 3790 int sX, sY; // source x, y 3791 int dX, dY; // dest x, y 3792 switch (direction) { 3793 case View.FOCUS_RIGHT: 3794 sX = source.right; 3795 sY = source.top + source.height() / 2; 3796 dX = dest.left; 3797 dY = dest.top + dest.height() / 2; 3798 break; 3799 case View.FOCUS_DOWN: 3800 sX = source.left + source.width() / 2; 3801 sY = source.bottom; 3802 dX = dest.left + dest.width() / 2; 3803 dY = dest.top; 3804 break; 3805 case View.FOCUS_LEFT: 3806 sX = source.left; 3807 sY = source.top + source.height() / 2; 3808 dX = dest.right; 3809 dY = dest.top + dest.height() / 2; 3810 break; 3811 case View.FOCUS_UP: 3812 sX = source.left + source.width() / 2; 3813 sY = source.top; 3814 dX = dest.left + dest.width() / 2; 3815 dY = dest.bottom; 3816 break; 3817 default: 3818 throw new IllegalArgumentException("direction must be one of " 3819 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}."); 3820 } 3821 int deltaX = dX - sX; 3822 int deltaY = dY - sY; 3823 return deltaY * deltaY + deltaX * deltaX; 3824 } 3825 3826 @Override 3827 protected boolean isInFilterMode() { 3828 return mFiltered; 3829 } 3830 3831 /** 3832 * Sends a key to the text filter window 3833 * 3834 * @param keyCode The keycode for the event 3835 * @param event The actual key event 3836 * 3837 * @return True if the text filter handled the event, false otherwise. 3838 */ 3839 boolean sendToTextFilter(int keyCode, int count, KeyEvent event) { 3840 if (!acceptFilter()) { 3841 return false; 3842 } 3843 3844 boolean handled = false; 3845 boolean okToSend = true; 3846 switch (keyCode) { 3847 case KeyEvent.KEYCODE_DPAD_UP: 3848 case KeyEvent.KEYCODE_DPAD_DOWN: 3849 case KeyEvent.KEYCODE_DPAD_LEFT: 3850 case KeyEvent.KEYCODE_DPAD_RIGHT: 3851 case KeyEvent.KEYCODE_DPAD_CENTER: 3852 case KeyEvent.KEYCODE_ENTER: 3853 okToSend = false; 3854 break; 3855 case KeyEvent.KEYCODE_BACK: 3856 if (mFiltered && mPopup != null && mPopup.isShowing()) { 3857 if (event.getAction() == KeyEvent.ACTION_DOWN 3858 && event.getRepeatCount() == 0) { 3859 getKeyDispatcherState().startTracking(event, this); 3860 handled = true; 3861 } else if (event.getAction() == KeyEvent.ACTION_UP 3862 && event.isTracking() && !event.isCanceled()) { 3863 handled = true; 3864 mTextFilter.setText(""); 3865 } 3866 } 3867 okToSend = false; 3868 break; 3869 case KeyEvent.KEYCODE_SPACE: 3870 // Only send spaces once we are filtered 3871 okToSend = mFiltered; 3872 break; 3873 } 3874 3875 if (okToSend) { 3876 createTextFilter(true); 3877 3878 KeyEvent forwardEvent = event; 3879 if (forwardEvent.getRepeatCount() > 0) { 3880 forwardEvent = KeyEvent.changeTimeRepeat(event, event.getEventTime(), 0); 3881 } 3882 3883 int action = event.getAction(); 3884 switch (action) { 3885 case KeyEvent.ACTION_DOWN: 3886 handled = mTextFilter.onKeyDown(keyCode, forwardEvent); 3887 break; 3888 3889 case KeyEvent.ACTION_UP: 3890 handled = mTextFilter.onKeyUp(keyCode, forwardEvent); 3891 break; 3892 3893 case KeyEvent.ACTION_MULTIPLE: 3894 handled = mTextFilter.onKeyMultiple(keyCode, count, event); 3895 break; 3896 } 3897 } 3898 return handled; 3899 } 3900 3901 /** 3902 * Return an InputConnection for editing of the filter text. 3903 */ 3904 @Override 3905 public InputConnection onCreateInputConnection(EditorInfo outAttrs) { 3906 if (isTextFilterEnabled()) { 3907 // XXX we need to have the text filter created, so we can get an 3908 // InputConnection to proxy to. Unfortunately this means we pretty 3909 // much need to make it as soon as a list view gets focus. 3910 createTextFilter(false); 3911 if (mPublicInputConnection == null) { 3912 mDefInputConnection = new BaseInputConnection(this, false); 3913 mPublicInputConnection = new InputConnectionWrapper( 3914 mTextFilter.onCreateInputConnection(outAttrs), true) { 3915 @Override 3916 public boolean reportFullscreenMode(boolean enabled) { 3917 // Use our own input connection, since it is 3918 // the "real" one the IME is talking with. 3919 return mDefInputConnection.reportFullscreenMode(enabled); 3920 } 3921 3922 @Override 3923 public boolean performEditorAction(int editorAction) { 3924 // The editor is off in its own window; we need to be 3925 // the one that does this. 3926 if (editorAction == EditorInfo.IME_ACTION_DONE) { 3927 InputMethodManager imm = (InputMethodManager) 3928 getContext().getSystemService( 3929 Context.INPUT_METHOD_SERVICE); 3930 if (imm != null) { 3931 imm.hideSoftInputFromWindow(getWindowToken(), 0); 3932 } 3933 return true; 3934 } 3935 return false; 3936 } 3937 3938 @Override 3939 public boolean sendKeyEvent(KeyEvent event) { 3940 // Use our own input connection, since the filter 3941 // text view may not be shown in a window so has 3942 // no ViewRoot to dispatch events with. 3943 return mDefInputConnection.sendKeyEvent(event); 3944 } 3945 }; 3946 } 3947 outAttrs.inputType = EditorInfo.TYPE_CLASS_TEXT 3948 | EditorInfo.TYPE_TEXT_VARIATION_FILTER; 3949 outAttrs.imeOptions = EditorInfo.IME_ACTION_DONE; 3950 return mPublicInputConnection; 3951 } 3952 return null; 3953 } 3954 3955 /** 3956 * For filtering we proxy an input connection to an internal text editor, 3957 * and this allows the proxying to happen. 3958 */ 3959 @Override 3960 public boolean checkInputConnectionProxy(View view) { 3961 return view == mTextFilter; 3962 } 3963 3964 /** 3965 * Creates the window for the text filter and populates it with an EditText field; 3966 * 3967 * @param animateEntrance true if the window should appear with an animation 3968 */ 3969 private void createTextFilter(boolean animateEntrance) { 3970 if (mPopup == null) { 3971 Context c = getContext(); 3972 PopupWindow p = new PopupWindow(c); 3973 LayoutInflater layoutInflater = (LayoutInflater) 3974 c.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 3975 mTextFilter = (EditText) layoutInflater.inflate( 3976 com.android.internal.R.layout.typing_filter, null); 3977 // For some reason setting this as the "real" input type changes 3978 // the text view in some way that it doesn't work, and I don't 3979 // want to figure out why this is. 3980 mTextFilter.setRawInputType(EditorInfo.TYPE_CLASS_TEXT 3981 | EditorInfo.TYPE_TEXT_VARIATION_FILTER); 3982 mTextFilter.setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI); 3983 mTextFilter.addTextChangedListener(this); 3984 p.setFocusable(false); 3985 p.setTouchable(false); 3986 p.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); 3987 p.setContentView(mTextFilter); 3988 p.setWidth(LayoutParams.WRAP_CONTENT); 3989 p.setHeight(LayoutParams.WRAP_CONTENT); 3990 p.setBackgroundDrawable(null); 3991 mPopup = p; 3992 getViewTreeObserver().addOnGlobalLayoutListener(this); 3993 mGlobalLayoutListenerAddedFilter = true; 3994 } 3995 if (animateEntrance) { 3996 mPopup.setAnimationStyle(com.android.internal.R.style.Animation_TypingFilter); 3997 } else { 3998 mPopup.setAnimationStyle(com.android.internal.R.style.Animation_TypingFilterRestore); 3999 } 4000 } 4001 4002 /** 4003 * Clear the text filter. 4004 */ 4005 public void clearTextFilter() { 4006 if (mFiltered) { 4007 mTextFilter.setText(""); 4008 mFiltered = false; 4009 if (mPopup != null && mPopup.isShowing()) { 4010 dismissPopup(); 4011 } 4012 } 4013 } 4014 4015 /** 4016 * Returns if the ListView currently has a text filter. 4017 */ 4018 public boolean hasTextFilter() { 4019 return mFiltered; 4020 } 4021 4022 public void onGlobalLayout() { 4023 if (isShown()) { 4024 // Show the popup if we are filtered 4025 if (mFiltered && mPopup != null && !mPopup.isShowing() && !mPopupHidden) { 4026 showPopup(); 4027 } 4028 } else { 4029 // Hide the popup when we are no longer visible 4030 if (mPopup != null && mPopup.isShowing()) { 4031 dismissPopup(); 4032 } 4033 } 4034 4035 } 4036 4037 /** 4038 * For our text watcher that is associated with the text filter. Does 4039 * nothing. 4040 */ 4041 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 4042 } 4043 4044 /** 4045 * For our text watcher that is associated with the text filter. Performs 4046 * the actual filtering as the text changes, and takes care of hiding and 4047 * showing the popup displaying the currently entered filter text. 4048 */ 4049 public void onTextChanged(CharSequence s, int start, int before, int count) { 4050 if (mPopup != null && isTextFilterEnabled()) { 4051 int length = s.length(); 4052 boolean showing = mPopup.isShowing(); 4053 if (!showing && length > 0) { 4054 // Show the filter popup if necessary 4055 showPopup(); 4056 mFiltered = true; 4057 } else if (showing && length == 0) { 4058 // Remove the filter popup if the user has cleared all text 4059 dismissPopup(); 4060 mFiltered = false; 4061 } 4062 if (mAdapter instanceof Filterable) { 4063 Filter f = ((Filterable) mAdapter).getFilter(); 4064 // Filter should not be null when we reach this part 4065 if (f != null) { 4066 f.filter(s, this); 4067 } else { 4068 throw new IllegalStateException("You cannot call onTextChanged with a non " 4069 + "filterable adapter"); 4070 } 4071 } 4072 } 4073 } 4074 4075 /** 4076 * For our text watcher that is associated with the text filter. Does 4077 * nothing. 4078 */ 4079 public void afterTextChanged(Editable s) { 4080 } 4081 4082 public void onFilterComplete(int count) { 4083 if (mSelectedPosition < 0 && count > 0) { 4084 mResurrectToPosition = INVALID_POSITION; 4085 resurrectSelection(); 4086 } 4087 } 4088 4089 @Override 4090 protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { 4091 return new LayoutParams(p); 4092 } 4093 4094 @Override 4095 public LayoutParams generateLayoutParams(AttributeSet attrs) { 4096 return new AbsListView.LayoutParams(getContext(), attrs); 4097 } 4098 4099 @Override 4100 protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 4101 return p instanceof AbsListView.LayoutParams; 4102 } 4103 4104 /** 4105 * Puts the list or grid into transcript mode. In this mode the list or grid will always scroll 4106 * to the bottom to show new items. 4107 * 4108 * @param mode the transcript mode to set 4109 * 4110 * @see #TRANSCRIPT_MODE_DISABLED 4111 * @see #TRANSCRIPT_MODE_NORMAL 4112 * @see #TRANSCRIPT_MODE_ALWAYS_SCROLL 4113 */ 4114 public void setTranscriptMode(int mode) { 4115 mTranscriptMode = mode; 4116 } 4117 4118 /** 4119 * Returns the current transcript mode. 4120 * 4121 * @return {@link #TRANSCRIPT_MODE_DISABLED}, {@link #TRANSCRIPT_MODE_NORMAL} or 4122 * {@link #TRANSCRIPT_MODE_ALWAYS_SCROLL} 4123 */ 4124 public int getTranscriptMode() { 4125 return mTranscriptMode; 4126 } 4127 4128 @Override 4129 public int getSolidColor() { 4130 return mCacheColorHint; 4131 } 4132 4133 /** 4134 * When set to a non-zero value, the cache color hint indicates that this list is always drawn 4135 * on top of a solid, single-color, opaque background 4136 * 4137 * @param color The background color 4138 */ 4139 public void setCacheColorHint(int color) { 4140 if (color != mCacheColorHint) { 4141 mCacheColorHint = color; 4142 int count = getChildCount(); 4143 for (int i = 0; i < count; i++) { 4144 getChildAt(i).setDrawingCacheBackgroundColor(color); 4145 } 4146 mRecycler.setCacheColorHint(color); 4147 } 4148 } 4149 4150 /** 4151 * When set to a non-zero value, the cache color hint indicates that this list is always drawn 4152 * on top of a solid, single-color, opaque background 4153 * 4154 * @return The cache color hint 4155 */ 4156 public int getCacheColorHint() { 4157 return mCacheColorHint; 4158 } 4159 4160 /** 4161 * Move all views (excluding headers and footers) held by this AbsListView into the supplied 4162 * List. This includes views displayed on the screen as well as views stored in AbsListView's 4163 * internal view recycler. 4164 * 4165 * @param views A list into which to put the reclaimed views 4166 */ 4167 public void reclaimViews(List<View> views) { 4168 int childCount = getChildCount(); 4169 RecyclerListener listener = mRecycler.mRecyclerListener; 4170 4171 // Reclaim views on screen 4172 for (int i = 0; i < childCount; i++) { 4173 View child = getChildAt(i); 4174 AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams(); 4175 // Don't reclaim header or footer views, or views that should be ignored 4176 if (lp != null && mRecycler.shouldRecycleViewType(lp.viewType)) { 4177 views.add(child); 4178 if (listener != null) { 4179 // Pretend they went through the scrap heap 4180 listener.onMovedToScrapHeap(child); 4181 } 4182 } 4183 } 4184 mRecycler.reclaimScrapViews(views); 4185 removeAllViewsInLayout(); 4186 } 4187 4188 /** 4189 * @hide 4190 */ 4191 @Override 4192 protected boolean onConsistencyCheck(int consistency) { 4193 boolean result = super.onConsistencyCheck(consistency); 4194 4195 final boolean checkLayout = (consistency & ViewDebug.CONSISTENCY_LAYOUT) != 0; 4196 4197 if (checkLayout) { 4198 // The active recycler must be empty 4199 final View[] activeViews = mRecycler.mActiveViews; 4200 int count = activeViews.length; 4201 for (int i = 0; i < count; i++) { 4202 if (activeViews[i] != null) { 4203 result = false; 4204 Log.d(ViewDebug.CONSISTENCY_LOG_TAG, 4205 "AbsListView " + this + " has a view in its active recycler: " + 4206 activeViews[i]); 4207 } 4208 } 4209 4210 // All views in the recycler must NOT be on screen and must NOT have a parent 4211 final ArrayList<View> scrap = mRecycler.mCurrentScrap; 4212 if (!checkScrap(scrap)) result = false; 4213 final ArrayList<View>[] scraps = mRecycler.mScrapViews; 4214 count = scraps.length; 4215 for (int i = 0; i < count; i++) { 4216 if (!checkScrap(scraps[i])) result = false; 4217 } 4218 } 4219 4220 return result; 4221 } 4222 4223 private boolean checkScrap(ArrayList<View> scrap) { 4224 if (scrap == null) return true; 4225 boolean result = true; 4226 4227 final int count = scrap.size(); 4228 for (int i = 0; i < count; i++) { 4229 final View view = scrap.get(i); 4230 if (view.getParent() != null) { 4231 result = false; 4232 Log.d(ViewDebug.CONSISTENCY_LOG_TAG, "AbsListView " + this + 4233 " has a view in its scrap heap still attached to a parent: " + view); 4234 } 4235 if (indexOfChild(view) >= 0) { 4236 result = false; 4237 Log.d(ViewDebug.CONSISTENCY_LOG_TAG, "AbsListView " + this + 4238 " has a view in its scrap heap that is also a direct child: " + view); 4239 } 4240 } 4241 4242 return result; 4243 } 4244 4245 private void finishGlows() { 4246 if (mEdgeGlowTop != null) { 4247 mEdgeGlowTop.finish(); 4248 mEdgeGlowBottom.finish(); 4249 } 4250 } 4251 4252 /** 4253 * Sets the recycler listener to be notified whenever a View is set aside in 4254 * the recycler for later reuse. This listener can be used to free resources 4255 * associated to the View. 4256 * 4257 * @param listener The recycler listener to be notified of views set aside 4258 * in the recycler. 4259 * 4260 * @see android.widget.AbsListView.RecycleBin 4261 * @see android.widget.AbsListView.RecyclerListener 4262 */ 4263 public void setRecyclerListener(RecyclerListener listener) { 4264 mRecycler.mRecyclerListener = listener; 4265 } 4266 4267 /** 4268 * AbsListView extends LayoutParams to provide a place to hold the view type. 4269 */ 4270 public static class LayoutParams extends ViewGroup.LayoutParams { 4271 /** 4272 * View type for this view, as returned by 4273 * {@link android.widget.Adapter#getItemViewType(int) } 4274 */ 4275 @ViewDebug.ExportedProperty(category = "list", mapping = { 4276 @ViewDebug.IntToString(from = ITEM_VIEW_TYPE_IGNORE, to = "ITEM_VIEW_TYPE_IGNORE"), 4277 @ViewDebug.IntToString(from = ITEM_VIEW_TYPE_HEADER_OR_FOOTER, to = "ITEM_VIEW_TYPE_HEADER_OR_FOOTER") 4278 }) 4279 int viewType; 4280 4281 /** 4282 * When this boolean is set, the view has been added to the AbsListView 4283 * at least once. It is used to know whether headers/footers have already 4284 * been added to the list view and whether they should be treated as 4285 * recycled views or not. 4286 */ 4287 @ViewDebug.ExportedProperty(category = "list") 4288 boolean recycledHeaderFooter; 4289 4290 /** 4291 * When an AbsListView is measured with an AT_MOST measure spec, it needs 4292 * to obtain children views to measure itself. When doing so, the children 4293 * are not attached to the window, but put in the recycler which assumes 4294 * they've been attached before. Setting this flag will force the reused 4295 * view to be attached to the window rather than just attached to the 4296 * parent. 4297 */ 4298 @ViewDebug.ExportedProperty(category = "list") 4299 boolean forceAdd; 4300 4301 public LayoutParams(Context c, AttributeSet attrs) { 4302 super(c, attrs); 4303 } 4304 4305 public LayoutParams(int w, int h) { 4306 super(w, h); 4307 } 4308 4309 public LayoutParams(int w, int h, int viewType) { 4310 super(w, h); 4311 this.viewType = viewType; 4312 } 4313 4314 public LayoutParams(ViewGroup.LayoutParams source) { 4315 super(source); 4316 } 4317 } 4318 4319 /** 4320 * A RecyclerListener is used to receive a notification whenever a View is placed 4321 * inside the RecycleBin's scrap heap. This listener is used to free resources 4322 * associated to Views placed in the RecycleBin. 4323 * 4324 * @see android.widget.AbsListView.RecycleBin 4325 * @see android.widget.AbsListView#setRecyclerListener(android.widget.AbsListView.RecyclerListener) 4326 */ 4327 public static interface RecyclerListener { 4328 /** 4329 * Indicates that the specified View was moved into the recycler's scrap heap. 4330 * The view is not displayed on screen any more and any expensive resource 4331 * associated with the view should be discarded. 4332 * 4333 * @param view 4334 */ 4335 void onMovedToScrapHeap(View view); 4336 } 4337 4338 /** 4339 * The RecycleBin facilitates reuse of views across layouts. The RecycleBin has two levels of 4340 * storage: ActiveViews and ScrapViews. ActiveViews are those views which were onscreen at the 4341 * start of a layout. By construction, they are displaying current information. At the end of 4342 * layout, all views in ActiveViews are demoted to ScrapViews. ScrapViews are old views that 4343 * could potentially be used by the adapter to avoid allocating views unnecessarily. 4344 * 4345 * @see android.widget.AbsListView#setRecyclerListener(android.widget.AbsListView.RecyclerListener) 4346 * @see android.widget.AbsListView.RecyclerListener 4347 */ 4348 class RecycleBin { 4349 private RecyclerListener mRecyclerListener; 4350 4351 /** 4352 * The position of the first view stored in mActiveViews. 4353 */ 4354 private int mFirstActivePosition; 4355 4356 /** 4357 * Views that were on screen at the start of layout. This array is populated at the start of 4358 * layout, and at the end of layout all view in mActiveViews are moved to mScrapViews. 4359 * Views in mActiveViews represent a contiguous range of Views, with position of the first 4360 * view store in mFirstActivePosition. 4361 */ 4362 private View[] mActiveViews = new View[0]; 4363 4364 /** 4365 * Unsorted views that can be used by the adapter as a convert view. 4366 */ 4367 private ArrayList<View>[] mScrapViews; 4368 4369 private int mViewTypeCount; 4370 4371 private ArrayList<View> mCurrentScrap; 4372 4373 public void setViewTypeCount(int viewTypeCount) { 4374 if (viewTypeCount < 1) { 4375 throw new IllegalArgumentException("Can't have a viewTypeCount < 1"); 4376 } 4377 //noinspection unchecked 4378 ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount]; 4379 for (int i = 0; i < viewTypeCount; i++) { 4380 scrapViews[i] = new ArrayList<View>(); 4381 } 4382 mViewTypeCount = viewTypeCount; 4383 mCurrentScrap = scrapViews[0]; 4384 mScrapViews = scrapViews; 4385 } 4386 4387 public void markChildrenDirty() { 4388 if (mViewTypeCount == 1) { 4389 final ArrayList<View> scrap = mCurrentScrap; 4390 final int scrapCount = scrap.size(); 4391 for (int i = 0; i < scrapCount; i++) { 4392 scrap.get(i).forceLayout(); 4393 } 4394 } else { 4395 final int typeCount = mViewTypeCount; 4396 for (int i = 0; i < typeCount; i++) { 4397 final ArrayList<View> scrap = mScrapViews[i]; 4398 final int scrapCount = scrap.size(); 4399 for (int j = 0; j < scrapCount; j++) { 4400 scrap.get(j).forceLayout(); 4401 } 4402 } 4403 } 4404 } 4405 4406 public boolean shouldRecycleViewType(int viewType) { 4407 return viewType >= 0; 4408 } 4409 4410 /** 4411 * Clears the scrap heap. 4412 */ 4413 void clear() { 4414 if (mViewTypeCount == 1) { 4415 final ArrayList<View> scrap = mCurrentScrap; 4416 final int scrapCount = scrap.size(); 4417 for (int i = 0; i < scrapCount; i++) { 4418 removeDetachedView(scrap.remove(scrapCount - 1 - i), false); 4419 } 4420 } else { 4421 final int typeCount = mViewTypeCount; 4422 for (int i = 0; i < typeCount; i++) { 4423 final ArrayList<View> scrap = mScrapViews[i]; 4424 final int scrapCount = scrap.size(); 4425 for (int j = 0; j < scrapCount; j++) { 4426 removeDetachedView(scrap.remove(scrapCount - 1 - j), false); 4427 } 4428 } 4429 } 4430 } 4431 4432 /** 4433 * Fill ActiveViews with all of the children of the AbsListView. 4434 * 4435 * @param childCount The minimum number of views mActiveViews should hold 4436 * @param firstActivePosition The position of the first view that will be stored in 4437 * mActiveViews 4438 */ 4439 void fillActiveViews(int childCount, int firstActivePosition) { 4440 if (mActiveViews.length < childCount) { 4441 mActiveViews = new View[childCount]; 4442 } 4443 mFirstActivePosition = firstActivePosition; 4444 4445 final View[] activeViews = mActiveViews; 4446 for (int i = 0; i < childCount; i++) { 4447 View child = getChildAt(i); 4448 AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams(); 4449 // Don't put header or footer views into the scrap heap 4450 if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { 4451 // Note: We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in active views. 4452 // However, we will NOT place them into scrap views. 4453 activeViews[i] = child; 4454 } 4455 } 4456 } 4457 4458 /** 4459 * Get the view corresponding to the specified position. The view will be removed from 4460 * mActiveViews if it is found. 4461 * 4462 * @param position The position to look up in mActiveViews 4463 * @return The view if it is found, null otherwise 4464 */ 4465 View getActiveView(int position) { 4466 int index = position - mFirstActivePosition; 4467 final View[] activeViews = mActiveViews; 4468 if (index >=0 && index < activeViews.length) { 4469 final View match = activeViews[index]; 4470 activeViews[index] = null; 4471 return match; 4472 } 4473 return null; 4474 } 4475 4476 /** 4477 * @return A view from the ScrapViews collection. These are unordered. 4478 */ 4479 View getScrapView(int position) { 4480 ArrayList<View> scrapViews; 4481 if (mViewTypeCount == 1) { 4482 scrapViews = mCurrentScrap; 4483 int size = scrapViews.size(); 4484 if (size > 0) { 4485 return scrapViews.remove(size - 1); 4486 } else { 4487 return null; 4488 } 4489 } else { 4490 int whichScrap = mAdapter.getItemViewType(position); 4491 if (whichScrap >= 0 && whichScrap < mScrapViews.length) { 4492 scrapViews = mScrapViews[whichScrap]; 4493 int size = scrapViews.size(); 4494 if (size > 0) { 4495 return scrapViews.remove(size - 1); 4496 } 4497 } 4498 } 4499 return null; 4500 } 4501 4502 /** 4503 * Put a view into the ScapViews list. These views are unordered. 4504 * 4505 * @param scrap The view to add 4506 */ 4507 void addScrapView(View scrap) { 4508 AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams(); 4509 if (lp == null) { 4510 return; 4511 } 4512 4513 // Don't put header or footer views or views that should be ignored 4514 // into the scrap heap 4515 int viewType = lp.viewType; 4516 if (!shouldRecycleViewType(viewType)) { 4517 if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { 4518 removeDetachedView(scrap, false); 4519 } 4520 return; 4521 } 4522 4523 if (mViewTypeCount == 1) { 4524 scrap.dispatchStartTemporaryDetach(); 4525 mCurrentScrap.add(scrap); 4526 } else { 4527 scrap.dispatchStartTemporaryDetach(); 4528 mScrapViews[viewType].add(scrap); 4529 } 4530 4531 if (mRecyclerListener != null) { 4532 mRecyclerListener.onMovedToScrapHeap(scrap); 4533 } 4534 } 4535 4536 /** 4537 * Move all views remaining in mActiveViews to mScrapViews. 4538 */ 4539 void scrapActiveViews() { 4540 final View[] activeViews = mActiveViews; 4541 final boolean hasListener = mRecyclerListener != null; 4542 final boolean multipleScraps = mViewTypeCount > 1; 4543 4544 ArrayList<View> scrapViews = mCurrentScrap; 4545 final int count = activeViews.length; 4546 for (int i = count - 1; i >= 0; i--) { 4547 final View victim = activeViews[i]; 4548 if (victim != null) { 4549 int whichScrap = ((AbsListView.LayoutParams) victim.getLayoutParams()).viewType; 4550 4551 activeViews[i] = null; 4552 4553 if (!shouldRecycleViewType(whichScrap)) { 4554 // Do not move views that should be ignored 4555 if (whichScrap != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { 4556 removeDetachedView(victim, false); 4557 } 4558 continue; 4559 } 4560 4561 if (multipleScraps) { 4562 scrapViews = mScrapViews[whichScrap]; 4563 } 4564 victim.dispatchStartTemporaryDetach(); 4565 scrapViews.add(victim); 4566 4567 if (hasListener) { 4568 mRecyclerListener.onMovedToScrapHeap(victim); 4569 } 4570 4571 if (ViewDebug.TRACE_RECYCLER) { 4572 ViewDebug.trace(victim, 4573 ViewDebug.RecyclerTraceType.MOVE_FROM_ACTIVE_TO_SCRAP_HEAP, 4574 mFirstActivePosition + i, -1); 4575 } 4576 } 4577 } 4578 4579 pruneScrapViews(); 4580 } 4581 4582 /** 4583 * Makes sure that the size of mScrapViews does not exceed the size of mActiveViews. 4584 * (This can happen if an adapter does not recycle its views). 4585 */ 4586 private void pruneScrapViews() { 4587 final int maxViews = mActiveViews.length; 4588 final int viewTypeCount = mViewTypeCount; 4589 final ArrayList<View>[] scrapViews = mScrapViews; 4590 for (int i = 0; i < viewTypeCount; ++i) { 4591 final ArrayList<View> scrapPile = scrapViews[i]; 4592 int size = scrapPile.size(); 4593 final int extras = size - maxViews; 4594 size--; 4595 for (int j = 0; j < extras; j++) { 4596 removeDetachedView(scrapPile.remove(size--), false); 4597 } 4598 } 4599 } 4600 4601 /** 4602 * Puts all views in the scrap heap into the supplied list. 4603 */ 4604 void reclaimScrapViews(List<View> views) { 4605 if (mViewTypeCount == 1) { 4606 views.addAll(mCurrentScrap); 4607 } else { 4608 final int viewTypeCount = mViewTypeCount; 4609 final ArrayList<View>[] scrapViews = mScrapViews; 4610 for (int i = 0; i < viewTypeCount; ++i) { 4611 final ArrayList<View> scrapPile = scrapViews[i]; 4612 views.addAll(scrapPile); 4613 } 4614 } 4615 } 4616 4617 /** 4618 * Updates the cache color hint of all known views. 4619 * 4620 * @param color The new cache color hint. 4621 */ 4622 void setCacheColorHint(int color) { 4623 if (mViewTypeCount == 1) { 4624 final ArrayList<View> scrap = mCurrentScrap; 4625 final int scrapCount = scrap.size(); 4626 for (int i = 0; i < scrapCount; i++) { 4627 scrap.get(i).setDrawingCacheBackgroundColor(color); 4628 } 4629 } else { 4630 final int typeCount = mViewTypeCount; 4631 for (int i = 0; i < typeCount; i++) { 4632 final ArrayList<View> scrap = mScrapViews[i]; 4633 final int scrapCount = scrap.size(); 4634 for (int j = 0; j < scrapCount; j++) { 4635 scrap.get(i).setDrawingCacheBackgroundColor(color); 4636 } 4637 } 4638 } 4639 // Just in case this is called during a layout pass 4640 final View[] activeViews = mActiveViews; 4641 final int count = activeViews.length; 4642 for (int i = 0; i < count; ++i) { 4643 final View victim = activeViews[i]; 4644 if (victim != null) { 4645 victim.setDrawingCacheBackgroundColor(color); 4646 } 4647 } 4648 } 4649 } 4650 } 4651