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