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