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