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