AbsListView.java revision 52e2ef8cac0cd6d78c2c2f5783846e03c636a54c
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 /** 2856 * Removes the filter window 2857 */ 2858 private void dismissPopup() { 2859 if (mPopup != null) { 2860 mPopup.dismiss(); 2861 } 2862 } 2863 2864 /** 2865 * Shows the filter window 2866 */ 2867 private void showPopup() { 2868 // Make sure we have a window before showing the popup 2869 if (getWindowVisibility() == View.VISIBLE) { 2870 createTextFilter(true); 2871 positionPopup(); 2872 // Make sure we get focus if we are showing the popup 2873 checkFocus(); 2874 } 2875 } 2876 2877 private void positionPopup() { 2878 int screenHeight = getResources().getDisplayMetrics().heightPixels; 2879 final int[] xy = new int[2]; 2880 getLocationOnScreen(xy); 2881 // TODO: The 20 below should come from the theme 2882 // TODO: And the gravity should be defined in the theme as well 2883 final int bottomGap = screenHeight - xy[1] - getHeight() + (int) (mDensityScale * 20); 2884 if (!mPopup.isShowing()) { 2885 mPopup.showAtLocation(this, Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 2886 xy[0], bottomGap); 2887 } else { 2888 mPopup.update(xy[0], bottomGap, -1, -1); 2889 } 2890 } 2891 2892 /** 2893 * What is the distance between the source and destination rectangles given the direction of 2894 * focus navigation between them? The direction basically helps figure out more quickly what is 2895 * self evident by the relationship between the rects... 2896 * 2897 * @param source the source rectangle 2898 * @param dest the destination rectangle 2899 * @param direction the direction 2900 * @return the distance between the rectangles 2901 */ 2902 static int getDistance(Rect source, Rect dest, int direction) { 2903 int sX, sY; // source x, y 2904 int dX, dY; // dest x, y 2905 switch (direction) { 2906 case View.FOCUS_RIGHT: 2907 sX = source.right; 2908 sY = source.top + source.height() / 2; 2909 dX = dest.left; 2910 dY = dest.top + dest.height() / 2; 2911 break; 2912 case View.FOCUS_DOWN: 2913 sX = source.left + source.width() / 2; 2914 sY = source.bottom; 2915 dX = dest.left + dest.width() / 2; 2916 dY = dest.top; 2917 break; 2918 case View.FOCUS_LEFT: 2919 sX = source.left; 2920 sY = source.top + source.height() / 2; 2921 dX = dest.right; 2922 dY = dest.top + dest.height() / 2; 2923 break; 2924 case View.FOCUS_UP: 2925 sX = source.left + source.width() / 2; 2926 sY = source.top; 2927 dX = dest.left + dest.width() / 2; 2928 dY = dest.bottom; 2929 break; 2930 default: 2931 throw new IllegalArgumentException("direction must be one of " 2932 + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}."); 2933 } 2934 int deltaX = dX - sX; 2935 int deltaY = dY - sY; 2936 return deltaY * deltaY + deltaX * deltaX; 2937 } 2938 2939 @Override 2940 protected boolean isInFilterMode() { 2941 return mFiltered; 2942 } 2943 2944 /** 2945 * Sends a key to the text filter window 2946 * 2947 * @param keyCode The keycode for the event 2948 * @param event The actual key event 2949 * 2950 * @return True if the text filter handled the event, false otherwise. 2951 */ 2952 boolean sendToTextFilter(int keyCode, int count, KeyEvent event) { 2953 if (!acceptFilter()) { 2954 return false; 2955 } 2956 2957 boolean handled = false; 2958 boolean okToSend = true; 2959 switch (keyCode) { 2960 case KeyEvent.KEYCODE_DPAD_UP: 2961 case KeyEvent.KEYCODE_DPAD_DOWN: 2962 case KeyEvent.KEYCODE_DPAD_LEFT: 2963 case KeyEvent.KEYCODE_DPAD_RIGHT: 2964 case KeyEvent.KEYCODE_DPAD_CENTER: 2965 case KeyEvent.KEYCODE_ENTER: 2966 okToSend = false; 2967 break; 2968 case KeyEvent.KEYCODE_BACK: 2969 if (mFiltered && mPopup != null && mPopup.isShowing()) { 2970 if (event.getAction() == KeyEvent.ACTION_DOWN 2971 && event.getRepeatCount() == 0) { 2972 getKeyDispatcherState().startTracking(event, this); 2973 handled = true; 2974 } else if (event.getAction() == KeyEvent.ACTION_UP 2975 && event.isTracking() && !event.isCanceled()) { 2976 handled = true; 2977 mTextFilter.setText(""); 2978 } 2979 } 2980 okToSend = false; 2981 break; 2982 case KeyEvent.KEYCODE_SPACE: 2983 // Only send spaces once we are filtered 2984 okToSend = mFiltered; 2985 break; 2986 } 2987 2988 if (okToSend) { 2989 createTextFilter(true); 2990 2991 KeyEvent forwardEvent = event; 2992 if (forwardEvent.getRepeatCount() > 0) { 2993 forwardEvent = KeyEvent.changeTimeRepeat(event, event.getEventTime(), 0); 2994 } 2995 2996 int action = event.getAction(); 2997 switch (action) { 2998 case KeyEvent.ACTION_DOWN: 2999 handled = mTextFilter.onKeyDown(keyCode, forwardEvent); 3000 break; 3001 3002 case KeyEvent.ACTION_UP: 3003 handled = mTextFilter.onKeyUp(keyCode, forwardEvent); 3004 break; 3005 3006 case KeyEvent.ACTION_MULTIPLE: 3007 handled = mTextFilter.onKeyMultiple(keyCode, count, event); 3008 break; 3009 } 3010 } 3011 return handled; 3012 } 3013 3014 /** 3015 * Return an InputConnection for editing of the filter text. 3016 */ 3017 @Override 3018 public InputConnection onCreateInputConnection(EditorInfo outAttrs) { 3019 if (isTextFilterEnabled()) { 3020 // XXX we need to have the text filter created, so we can get an 3021 // InputConnection to proxy to. Unfortunately this means we pretty 3022 // much need to make it as soon as a list view gets focus. 3023 createTextFilter(false); 3024 if (mPublicInputConnection == null) { 3025 mDefInputConnection = new BaseInputConnection(this, false); 3026 mPublicInputConnection = new InputConnectionWrapper( 3027 mTextFilter.onCreateInputConnection(outAttrs), true) { 3028 @Override 3029 public boolean reportFullscreenMode(boolean enabled) { 3030 // Use our own input connection, since it is 3031 // the "real" one the IME is talking with. 3032 return mDefInputConnection.reportFullscreenMode(enabled); 3033 } 3034 3035 @Override 3036 public boolean performEditorAction(int editorAction) { 3037 // The editor is off in its own window; we need to be 3038 // the one that does this. 3039 if (editorAction == EditorInfo.IME_ACTION_DONE) { 3040 InputMethodManager imm = (InputMethodManager) 3041 getContext().getSystemService( 3042 Context.INPUT_METHOD_SERVICE); 3043 if (imm != null) { 3044 imm.hideSoftInputFromWindow(getWindowToken(), 0); 3045 } 3046 return true; 3047 } 3048 return false; 3049 } 3050 3051 @Override 3052 public boolean sendKeyEvent(KeyEvent event) { 3053 // Use our own input connection, since the filter 3054 // text view may not be shown in a window so has 3055 // no ViewRoot to dispatch events with. 3056 return mDefInputConnection.sendKeyEvent(event); 3057 } 3058 }; 3059 } 3060 outAttrs.inputType = EditorInfo.TYPE_CLASS_TEXT 3061 | EditorInfo.TYPE_TEXT_VARIATION_FILTER; 3062 outAttrs.imeOptions = EditorInfo.IME_ACTION_DONE; 3063 return mPublicInputConnection; 3064 } 3065 return null; 3066 } 3067 3068 /** 3069 * For filtering we proxy an input connection to an internal text editor, 3070 * and this allows the proxying to happen. 3071 */ 3072 @Override 3073 public boolean checkInputConnectionProxy(View view) { 3074 return view == mTextFilter; 3075 } 3076 3077 /** 3078 * Creates the window for the text filter and populates it with an EditText field; 3079 * 3080 * @param animateEntrance true if the window should appear with an animation 3081 */ 3082 private void createTextFilter(boolean animateEntrance) { 3083 if (mPopup == null) { 3084 Context c = getContext(); 3085 PopupWindow p = new PopupWindow(c); 3086 LayoutInflater layoutInflater = (LayoutInflater) 3087 c.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 3088 mTextFilter = (EditText) layoutInflater.inflate( 3089 com.android.internal.R.layout.typing_filter, null); 3090 // For some reason setting this as the "real" input type changes 3091 // the text view in some way that it doesn't work, and I don't 3092 // want to figure out why this is. 3093 mTextFilter.setRawInputType(EditorInfo.TYPE_CLASS_TEXT 3094 | EditorInfo.TYPE_TEXT_VARIATION_FILTER); 3095 mTextFilter.setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI); 3096 mTextFilter.addTextChangedListener(this); 3097 p.setFocusable(false); 3098 p.setTouchable(false); 3099 p.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); 3100 p.setContentView(mTextFilter); 3101 p.setWidth(LayoutParams.WRAP_CONTENT); 3102 p.setHeight(LayoutParams.WRAP_CONTENT); 3103 p.setBackgroundDrawable(null); 3104 mPopup = p; 3105 getViewTreeObserver().addOnGlobalLayoutListener(this); 3106 mGlobalLayoutListenerAddedFilter = true; 3107 } 3108 if (animateEntrance) { 3109 mPopup.setAnimationStyle(com.android.internal.R.style.Animation_TypingFilter); 3110 } else { 3111 mPopup.setAnimationStyle(com.android.internal.R.style.Animation_TypingFilterRestore); 3112 } 3113 } 3114 3115 /** 3116 * Clear the text filter. 3117 */ 3118 public void clearTextFilter() { 3119 if (mFiltered) { 3120 mTextFilter.setText(""); 3121 mFiltered = false; 3122 if (mPopup != null && mPopup.isShowing()) { 3123 dismissPopup(); 3124 } 3125 } 3126 } 3127 3128 /** 3129 * Returns if the ListView currently has a text filter. 3130 */ 3131 public boolean hasTextFilter() { 3132 return mFiltered; 3133 } 3134 3135 public void onGlobalLayout() { 3136 if (isShown()) { 3137 // Show the popup if we are filtered 3138 if (mFiltered && mPopup != null && !mPopup.isShowing()) { 3139 showPopup(); 3140 } 3141 } else { 3142 // Hide the popup when we are no longer visible 3143 if (mPopup.isShowing()) { 3144 dismissPopup(); 3145 } 3146 } 3147 3148 } 3149 3150 /** 3151 * For our text watcher that is associated with the text filter. Does 3152 * nothing. 3153 */ 3154 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 3155 } 3156 3157 /** 3158 * For our text watcher that is associated with the text filter. Performs 3159 * the actual filtering as the text changes, and takes care of hiding and 3160 * showing the popup displaying the currently entered filter text. 3161 */ 3162 public void onTextChanged(CharSequence s, int start, int before, int count) { 3163 if (mPopup != null && isTextFilterEnabled()) { 3164 int length = s.length(); 3165 boolean showing = mPopup.isShowing(); 3166 if (!showing && length > 0) { 3167 // Show the filter popup if necessary 3168 showPopup(); 3169 mFiltered = true; 3170 } else if (showing && length == 0) { 3171 // Remove the filter popup if the user has cleared all text 3172 dismissPopup(); 3173 mFiltered = false; 3174 } 3175 if (mAdapter instanceof Filterable) { 3176 Filter f = ((Filterable) mAdapter).getFilter(); 3177 // Filter should not be null when we reach this part 3178 if (f != null) { 3179 f.filter(s, this); 3180 } else { 3181 throw new IllegalStateException("You cannot call onTextChanged with a non " 3182 + "filterable adapter"); 3183 } 3184 } 3185 } 3186 } 3187 3188 /** 3189 * For our text watcher that is associated with the text filter. Does 3190 * nothing. 3191 */ 3192 public void afterTextChanged(Editable s) { 3193 } 3194 3195 public void onFilterComplete(int count) { 3196 if (mSelectedPosition < 0 && count > 0) { 3197 mResurrectToPosition = INVALID_POSITION; 3198 resurrectSelection(); 3199 } 3200 } 3201 3202 @Override 3203 protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { 3204 return new LayoutParams(p); 3205 } 3206 3207 @Override 3208 public LayoutParams generateLayoutParams(AttributeSet attrs) { 3209 return new AbsListView.LayoutParams(getContext(), attrs); 3210 } 3211 3212 @Override 3213 protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 3214 return p instanceof AbsListView.LayoutParams; 3215 } 3216 3217 /** 3218 * Puts the list or grid into transcript mode. In this mode the list or grid will always scroll 3219 * to the bottom to show new items. 3220 * 3221 * @param mode the transcript mode to set 3222 * 3223 * @see #TRANSCRIPT_MODE_DISABLED 3224 * @see #TRANSCRIPT_MODE_NORMAL 3225 * @see #TRANSCRIPT_MODE_ALWAYS_SCROLL 3226 */ 3227 public void setTranscriptMode(int mode) { 3228 mTranscriptMode = mode; 3229 } 3230 3231 /** 3232 * Returns the current transcript mode. 3233 * 3234 * @return {@link #TRANSCRIPT_MODE_DISABLED}, {@link #TRANSCRIPT_MODE_NORMAL} or 3235 * {@link #TRANSCRIPT_MODE_ALWAYS_SCROLL} 3236 */ 3237 public int getTranscriptMode() { 3238 return mTranscriptMode; 3239 } 3240 3241 @Override 3242 public int getSolidColor() { 3243 return mCacheColorHint; 3244 } 3245 3246 /** 3247 * When set to a non-zero value, the cache color hint indicates that this list is always drawn 3248 * on top of a solid, single-color, opaque background 3249 * 3250 * @param color The background color 3251 */ 3252 public void setCacheColorHint(int color) { 3253 if (color != mCacheColorHint) { 3254 mCacheColorHint = color; 3255 int count = getChildCount(); 3256 for (int i = 0; i < count; i++) { 3257 getChildAt(i).setDrawingCacheBackgroundColor(color); 3258 } 3259 mRecycler.setCacheColorHint(color); 3260 } 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 * @return The cache color hint 3268 */ 3269 public int getCacheColorHint() { 3270 return mCacheColorHint; 3271 } 3272 3273 /** 3274 * Move all views (excluding headers and footers) held by this AbsListView into the supplied 3275 * List. This includes views displayed on the screen as well as views stored in AbsListView's 3276 * internal view recycler. 3277 * 3278 * @param views A list into which to put the reclaimed views 3279 */ 3280 public void reclaimViews(List<View> views) { 3281 int childCount = getChildCount(); 3282 RecyclerListener listener = mRecycler.mRecyclerListener; 3283 3284 // Reclaim views on screen 3285 for (int i = 0; i < childCount; i++) { 3286 View child = getChildAt(i); 3287 AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams(); 3288 // Don't reclaim header or footer views, or views that should be ignored 3289 if (lp != null && mRecycler.shouldRecycleViewType(lp.viewType)) { 3290 views.add(child); 3291 if (listener != null) { 3292 // Pretend they went through the scrap heap 3293 listener.onMovedToScrapHeap(child); 3294 } 3295 } 3296 } 3297 mRecycler.reclaimScrapViews(views); 3298 removeAllViewsInLayout(); 3299 } 3300 3301 /** 3302 * @hide 3303 */ 3304 @Override 3305 protected boolean onConsistencyCheck(int consistency) { 3306 boolean result = super.onConsistencyCheck(consistency); 3307 3308 final boolean checkLayout = (consistency & ViewDebug.CONSISTENCY_LAYOUT) != 0; 3309 3310 if (checkLayout) { 3311 // The active recycler must be empty 3312 final View[] activeViews = mRecycler.mActiveViews; 3313 int count = activeViews.length; 3314 for (int i = 0; i < count; i++) { 3315 if (activeViews[i] != null) { 3316 result = false; 3317 android.util.Log.d(ViewDebug.CONSISTENCY_LOG_TAG, 3318 "AbsListView " + this + " has a view in its active recycler: " + 3319 activeViews[i]); 3320 } 3321 } 3322 3323 // All views in the recycler must NOT be on screen and must NOT have a parent 3324 final ArrayList<View> scrap = mRecycler.mCurrentScrap; 3325 if (!checkScrap(scrap)) result = false; 3326 final ArrayList<View>[] scraps = mRecycler.mScrapViews; 3327 count = scraps.length; 3328 for (int i = 0; i < count; i++) { 3329 if (!checkScrap(scraps[i])) result = false; 3330 } 3331 } 3332 3333 return result; 3334 } 3335 3336 private boolean checkScrap(ArrayList<View> scrap) { 3337 if (scrap == null) return true; 3338 boolean result = true; 3339 3340 final int count = scrap.size(); 3341 for (int i = 0; i < count; i++) { 3342 final View view = scrap.get(i); 3343 if (view.getParent() != null) { 3344 result = false; 3345 android.util.Log.d(ViewDebug.CONSISTENCY_LOG_TAG, "AbsListView " + this + 3346 " has a view in its scrap heap still attached to a parent: " + view); 3347 } 3348 if (indexOfChild(view) >= 0) { 3349 result = false; 3350 android.util.Log.d(ViewDebug.CONSISTENCY_LOG_TAG, "AbsListView " + this + 3351 " has a view in its scrap heap that is also a direct child: " + view); 3352 } 3353 } 3354 3355 return result; 3356 } 3357 3358 /** 3359 * Sets the recycler listener to be notified whenever a View is set aside in 3360 * the recycler for later reuse. This listener can be used to free resources 3361 * associated to the View. 3362 * 3363 * @param listener The recycler listener to be notified of views set aside 3364 * in the recycler. 3365 * 3366 * @see android.widget.AbsListView.RecycleBin 3367 * @see android.widget.AbsListView.RecyclerListener 3368 */ 3369 public void setRecyclerListener(RecyclerListener listener) { 3370 mRecycler.mRecyclerListener = listener; 3371 } 3372 3373 /** 3374 * AbsListView extends LayoutParams to provide a place to hold the view type. 3375 */ 3376 public static class LayoutParams extends ViewGroup.LayoutParams { 3377 /** 3378 * View type for this view, as returned by 3379 * {@link android.widget.Adapter#getItemViewType(int) } 3380 */ 3381 int viewType; 3382 3383 /** 3384 * When this boolean is set, the view has been added to the AbsListView 3385 * at least once. It is used to know whether headers/footers have already 3386 * been added to the list view and whether they should be treated as 3387 * recycled views or not. 3388 */ 3389 boolean recycledHeaderFooter; 3390 3391 public LayoutParams(Context c, AttributeSet attrs) { 3392 super(c, attrs); 3393 } 3394 3395 public LayoutParams(int w, int h) { 3396 super(w, h); 3397 } 3398 3399 public LayoutParams(int w, int h, int viewType) { 3400 super(w, h); 3401 this.viewType = viewType; 3402 } 3403 3404 public LayoutParams(ViewGroup.LayoutParams source) { 3405 super(source); 3406 } 3407 } 3408 3409 /** 3410 * A RecyclerListener is used to receive a notification whenever a View is placed 3411 * inside the RecycleBin's scrap heap. This listener is used to free resources 3412 * associated to Views placed in the RecycleBin. 3413 * 3414 * @see android.widget.AbsListView.RecycleBin 3415 * @see android.widget.AbsListView#setRecyclerListener(android.widget.AbsListView.RecyclerListener) 3416 */ 3417 public static interface RecyclerListener { 3418 /** 3419 * Indicates that the specified View was moved into the recycler's scrap heap. 3420 * The view is not displayed on screen any more and any expensive resource 3421 * associated with the view should be discarded. 3422 * 3423 * @param view 3424 */ 3425 void onMovedToScrapHeap(View view); 3426 } 3427 3428 /** 3429 * The RecycleBin facilitates reuse of views across layouts. The RecycleBin has two levels of 3430 * storage: ActiveViews and ScrapViews. ActiveViews are those views which were onscreen at the 3431 * start of a layout. By construction, they are displaying current information. At the end of 3432 * layout, all views in ActiveViews are demoted to ScrapViews. ScrapViews are old views that 3433 * could potentially be used by the adapter to avoid allocating views unnecessarily. 3434 * 3435 * @see android.widget.AbsListView#setRecyclerListener(android.widget.AbsListView.RecyclerListener) 3436 * @see android.widget.AbsListView.RecyclerListener 3437 */ 3438 class RecycleBin { 3439 private RecyclerListener mRecyclerListener; 3440 3441 /** 3442 * The position of the first view stored in mActiveViews. 3443 */ 3444 private int mFirstActivePosition; 3445 3446 /** 3447 * Views that were on screen at the start of layout. This array is populated at the start of 3448 * layout, and at the end of layout all view in mActiveViews are moved to mScrapViews. 3449 * Views in mActiveViews represent a contiguous range of Views, with position of the first 3450 * view store in mFirstActivePosition. 3451 */ 3452 private View[] mActiveViews = new View[0]; 3453 3454 /** 3455 * Unsorted views that can be used by the adapter as a convert view. 3456 */ 3457 private ArrayList<View>[] mScrapViews; 3458 3459 private int mViewTypeCount; 3460 3461 private ArrayList<View> mCurrentScrap; 3462 3463 public void setViewTypeCount(int viewTypeCount) { 3464 if (viewTypeCount < 1) { 3465 throw new IllegalArgumentException("Can't have a viewTypeCount < 1"); 3466 } 3467 //noinspection unchecked 3468 ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount]; 3469 for (int i = 0; i < viewTypeCount; i++) { 3470 scrapViews[i] = new ArrayList<View>(); 3471 } 3472 mViewTypeCount = viewTypeCount; 3473 mCurrentScrap = scrapViews[0]; 3474 mScrapViews = scrapViews; 3475 } 3476 3477 public boolean shouldRecycleViewType(int viewType) { 3478 return viewType >= 0; 3479 } 3480 3481 /** 3482 * Clears the scrap heap. 3483 */ 3484 void clear() { 3485 if (mViewTypeCount == 1) { 3486 final ArrayList<View> scrap = mCurrentScrap; 3487 final int scrapCount = scrap.size(); 3488 for (int i = 0; i < scrapCount; i++) { 3489 removeDetachedView(scrap.remove(scrapCount - 1 - i), false); 3490 } 3491 } else { 3492 final int typeCount = mViewTypeCount; 3493 for (int i = 0; i < typeCount; i++) { 3494 final ArrayList<View> scrap = mScrapViews[i]; 3495 final int scrapCount = scrap.size(); 3496 for (int j = 0; j < scrapCount; j++) { 3497 removeDetachedView(scrap.remove(scrapCount - 1 - j), false); 3498 } 3499 } 3500 } 3501 } 3502 3503 /** 3504 * Fill ActiveViews with all of the children of the AbsListView. 3505 * 3506 * @param childCount The minimum number of views mActiveViews should hold 3507 * @param firstActivePosition The position of the first view that will be stored in 3508 * mActiveViews 3509 */ 3510 void fillActiveViews(int childCount, int firstActivePosition) { 3511 if (mActiveViews.length < childCount) { 3512 mActiveViews = new View[childCount]; 3513 } 3514 mFirstActivePosition = firstActivePosition; 3515 3516 final View[] activeViews = mActiveViews; 3517 for (int i = 0; i < childCount; i++) { 3518 View child = getChildAt(i); 3519 AbsListView.LayoutParams lp = (AbsListView.LayoutParams)child.getLayoutParams(); 3520 // Don't put header or footer views into the scrap heap 3521 if (lp != null && lp.viewType != AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) { 3522 // Note: We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in active views. 3523 // However, we will NOT place them into scrap views. 3524 activeViews[i] = child; 3525 } 3526 } 3527 } 3528 3529 /** 3530 * Get the view corresponding to the specified position. The view will be removed from 3531 * mActiveViews if it is found. 3532 * 3533 * @param position The position to look up in mActiveViews 3534 * @return The view if it is found, null otherwise 3535 */ 3536 View getActiveView(int position) { 3537 int index = position - mFirstActivePosition; 3538 final View[] activeViews = mActiveViews; 3539 if (index >=0 && index < activeViews.length) { 3540 final View match = activeViews[index]; 3541 activeViews[index] = null; 3542 return match; 3543 } 3544 return null; 3545 } 3546 3547 /** 3548 * @return A view from the ScrapViews collection. These are unordered. 3549 */ 3550 View getScrapView(int position) { 3551 ArrayList<View> scrapViews; 3552 if (mViewTypeCount == 1) { 3553 scrapViews = mCurrentScrap; 3554 int size = scrapViews.size(); 3555 if (size > 0) { 3556 return scrapViews.remove(size - 1); 3557 } else { 3558 return null; 3559 } 3560 } else { 3561 int whichScrap = mAdapter.getItemViewType(position); 3562 if (whichScrap >= 0 && whichScrap < mScrapViews.length) { 3563 scrapViews = mScrapViews[whichScrap]; 3564 int size = scrapViews.size(); 3565 if (size > 0) { 3566 return scrapViews.remove(size - 1); 3567 } 3568 } 3569 } 3570 return null; 3571 } 3572 3573 /** 3574 * Put a view into the ScapViews list. These views are unordered. 3575 * 3576 * @param scrap The view to add 3577 */ 3578 void addScrapView(View scrap) { 3579 AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams(); 3580 if (lp == null) { 3581 return; 3582 } 3583 3584 // Don't put header or footer views or views that should be ignored 3585 // into the scrap heap 3586 int viewType = lp.viewType; 3587 if (!shouldRecycleViewType(viewType)) { 3588 removeDetachedView(scrap, false); 3589 return; 3590 } 3591 3592 if (mViewTypeCount == 1) { 3593 mCurrentScrap.add(scrap); 3594 } else { 3595 mScrapViews[viewType].add(scrap); 3596 } 3597 3598 if (mRecyclerListener != null) { 3599 mRecyclerListener.onMovedToScrapHeap(scrap); 3600 } 3601 } 3602 3603 /** 3604 * Move all views remaining in mActiveViews to mScrapViews. 3605 */ 3606 void scrapActiveViews() { 3607 final View[] activeViews = mActiveViews; 3608 final boolean hasListener = mRecyclerListener != null; 3609 final boolean multipleScraps = mViewTypeCount > 1; 3610 3611 ArrayList<View> scrapViews = mCurrentScrap; 3612 final int count = activeViews.length; 3613 for (int i = 0; i < count; ++i) { 3614 final View victim = activeViews[i]; 3615 if (victim != null) { 3616 int whichScrap = ((AbsListView.LayoutParams) victim.getLayoutParams()).viewType; 3617 3618 activeViews[i] = null; 3619 3620 if (whichScrap == AdapterView.ITEM_VIEW_TYPE_IGNORE) { 3621 removeDetachedView(victim, false); 3622 // Do not move views that should be ignored 3623 continue; 3624 } 3625 3626 if (multipleScraps) { 3627 scrapViews = mScrapViews[whichScrap]; 3628 } 3629 scrapViews.add(victim); 3630 3631 if (hasListener) { 3632 mRecyclerListener.onMovedToScrapHeap(victim); 3633 } 3634 3635 if (ViewDebug.TRACE_RECYCLER) { 3636 ViewDebug.trace(victim, 3637 ViewDebug.RecyclerTraceType.MOVE_FROM_ACTIVE_TO_SCRAP_HEAP, 3638 mFirstActivePosition + i, -1); 3639 } 3640 } 3641 } 3642 3643 pruneScrapViews(); 3644 } 3645 3646 /** 3647 * Makes sure that the size of mScrapViews does not exceed the size of mActiveViews. 3648 * (This can happen if an adapter does not recycle its views). 3649 */ 3650 private void pruneScrapViews() { 3651 final int maxViews = mActiveViews.length; 3652 final int viewTypeCount = mViewTypeCount; 3653 final ArrayList<View>[] scrapViews = mScrapViews; 3654 for (int i = 0; i < viewTypeCount; ++i) { 3655 final ArrayList<View> scrapPile = scrapViews[i]; 3656 int size = scrapPile.size(); 3657 final int extras = size - maxViews; 3658 size--; 3659 for (int j = 0; j < extras; j++) { 3660 removeDetachedView(scrapPile.remove(size--), false); 3661 } 3662 } 3663 } 3664 3665 /** 3666 * Puts all views in the scrap heap into the supplied list. 3667 */ 3668 void reclaimScrapViews(List<View> views) { 3669 if (mViewTypeCount == 1) { 3670 views.addAll(mCurrentScrap); 3671 } else { 3672 final int viewTypeCount = mViewTypeCount; 3673 final ArrayList<View>[] scrapViews = mScrapViews; 3674 for (int i = 0; i < viewTypeCount; ++i) { 3675 final ArrayList<View> scrapPile = scrapViews[i]; 3676 views.addAll(scrapPile); 3677 } 3678 } 3679 } 3680 3681 /** 3682 * Updates the cache color hint of all known views. 3683 * 3684 * @param color The new cache color hint. 3685 */ 3686 void setCacheColorHint(int color) { 3687 if (mViewTypeCount == 1) { 3688 final ArrayList<View> scrap = mCurrentScrap; 3689 final int scrapCount = scrap.size(); 3690 for (int i = 0; i < scrapCount; i++) { 3691 scrap.get(i).setDrawingCacheBackgroundColor(color); 3692 } 3693 } else { 3694 final int typeCount = mViewTypeCount; 3695 for (int i = 0; i < typeCount; i++) { 3696 final ArrayList<View> scrap = mScrapViews[i]; 3697 final int scrapCount = scrap.size(); 3698 for (int j = 0; j < scrapCount; j++) { 3699 scrap.get(i).setDrawingCacheBackgroundColor(color); 3700 } 3701 } 3702 } 3703 // Just in case this is called during a layout pass 3704 final View[] activeViews = mActiveViews; 3705 final int count = activeViews.length; 3706 for (int i = 0; i < count; ++i) { 3707 final View victim = activeViews[i]; 3708 if (victim != null) { 3709 victim.setDrawingCacheBackgroundColor(color); 3710 } 3711 } 3712 } 3713 } 3714} 3715