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