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