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