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