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