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