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