AdapterView.java revision 98348517fde86f993b5720a862bce5c2a5f1a06f
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.database.DataSetObserver; 21import android.os.Parcelable; 22import android.os.SystemClock; 23import android.util.AttributeSet; 24import android.util.SparseArray; 25import android.view.ContextMenu; 26import android.view.ContextMenu.ContextMenuInfo; 27import android.view.SoundEffectConstants; 28import android.view.View; 29import android.view.ViewDebug; 30import android.view.ViewGroup; 31import android.view.accessibility.AccessibilityEvent; 32import android.view.accessibility.AccessibilityNodeInfo; 33 34 35/** 36 * An AdapterView is a view whose children are determined by an {@link Adapter}. 37 * 38 * <p> 39 * See {@link ListView}, {@link GridView}, {@link Spinner} and 40 * {@link Gallery} for commonly used subclasses of AdapterView. 41 */ 42public abstract class AdapterView<T extends Adapter> extends ViewGroup { 43 44 /** 45 * The item view type returned by {@link Adapter#getItemViewType(int)} when 46 * the adapter does not want the item's view recycled. 47 */ 48 public static final int ITEM_VIEW_TYPE_IGNORE = -1; 49 50 /** 51 * The item view type returned by {@link Adapter#getItemViewType(int)} when 52 * the item is a header or footer. 53 */ 54 public static final int ITEM_VIEW_TYPE_HEADER_OR_FOOTER = -2; 55 56 /** 57 * The position of the first child displayed 58 */ 59 @ViewDebug.ExportedProperty(category = "scrolling") 60 int mFirstPosition = 0; 61 62 /** 63 * The offset in pixels from the top of the AdapterView to the top 64 * of the view to select during the next layout. 65 */ 66 int mSpecificTop; 67 68 /** 69 * Position from which to start looking for mSyncRowId 70 */ 71 int mSyncPosition; 72 73 /** 74 * Row id to look for when data has changed 75 */ 76 long mSyncRowId = INVALID_ROW_ID; 77 78 /** 79 * Height of the view when mSyncPosition and mSyncRowId where set 80 */ 81 long mSyncHeight; 82 83 /** 84 * True if we need to sync to mSyncRowId 85 */ 86 boolean mNeedSync = false; 87 88 /** 89 * Indicates whether to sync based on the selection or position. Possible 90 * values are {@link #SYNC_SELECTED_POSITION} or 91 * {@link #SYNC_FIRST_POSITION}. 92 */ 93 int mSyncMode; 94 95 /** 96 * Our height after the last layout 97 */ 98 private int mLayoutHeight; 99 100 /** 101 * Sync based on the selected child 102 */ 103 static final int SYNC_SELECTED_POSITION = 0; 104 105 /** 106 * Sync based on the first child displayed 107 */ 108 static final int SYNC_FIRST_POSITION = 1; 109 110 /** 111 * Maximum amount of time to spend in {@link #findSyncPosition()} 112 */ 113 static final int SYNC_MAX_DURATION_MILLIS = 100; 114 115 /** 116 * Indicates that this view is currently being laid out. 117 */ 118 boolean mInLayout = false; 119 120 /** 121 * The listener that receives notifications when an item is selected. 122 */ 123 OnItemSelectedListener mOnItemSelectedListener; 124 125 /** 126 * The listener that receives notifications when an item is clicked. 127 */ 128 OnItemClickListener mOnItemClickListener; 129 130 /** 131 * The listener that receives notifications when an item is long clicked. 132 */ 133 OnItemLongClickListener mOnItemLongClickListener; 134 135 /** 136 * True if the data has changed since the last layout 137 */ 138 boolean mDataChanged; 139 140 /** 141 * The position within the adapter's data set of the item to select 142 * during the next layout. 143 */ 144 @ViewDebug.ExportedProperty(category = "list") 145 int mNextSelectedPosition = INVALID_POSITION; 146 147 /** 148 * The item id of the item to select during the next layout. 149 */ 150 long mNextSelectedRowId = INVALID_ROW_ID; 151 152 /** 153 * The position within the adapter's data set of the currently selected item. 154 */ 155 @ViewDebug.ExportedProperty(category = "list") 156 int mSelectedPosition = INVALID_POSITION; 157 158 /** 159 * The item id of the currently selected item. 160 */ 161 long mSelectedRowId = INVALID_ROW_ID; 162 163 /** 164 * View to show if there are no items to show. 165 */ 166 private View mEmptyView; 167 168 /** 169 * The number of items in the current adapter. 170 */ 171 @ViewDebug.ExportedProperty(category = "list") 172 int mItemCount; 173 174 /** 175 * The number of items in the adapter before a data changed event occurred. 176 */ 177 int mOldItemCount; 178 179 /** 180 * Represents an invalid position. All valid positions are in the range 0 to 1 less than the 181 * number of items in the current adapter. 182 */ 183 public static final int INVALID_POSITION = -1; 184 185 /** 186 * Represents an empty or invalid row id 187 */ 188 public static final long INVALID_ROW_ID = Long.MIN_VALUE; 189 190 /** 191 * The last selected position we used when notifying 192 */ 193 int mOldSelectedPosition = INVALID_POSITION; 194 195 /** 196 * The id of the last selected position we used when notifying 197 */ 198 long mOldSelectedRowId = INVALID_ROW_ID; 199 200 /** 201 * Indicates what focusable state is requested when calling setFocusable(). 202 * In addition to this, this view has other criteria for actually 203 * determining the focusable state (such as whether its empty or the text 204 * filter is shown). 205 * 206 * @see #setFocusable(boolean) 207 * @see #checkFocus() 208 */ 209 private boolean mDesiredFocusableState; 210 private boolean mDesiredFocusableInTouchModeState; 211 212 private SelectionNotifier mSelectionNotifier; 213 /** 214 * When set to true, calls to requestLayout() will not propagate up the parent hierarchy. 215 * This is used to layout the children during a layout pass. 216 */ 217 boolean mBlockLayoutRequests = false; 218 219 public AdapterView(Context context) { 220 super(context); 221 } 222 223 public AdapterView(Context context, AttributeSet attrs) { 224 super(context, attrs); 225 } 226 227 public AdapterView(Context context, AttributeSet attrs, int defStyle) { 228 super(context, attrs, defStyle); 229 } 230 231 /** 232 * Interface definition for a callback to be invoked when an item in this 233 * AdapterView has been clicked. 234 */ 235 public interface OnItemClickListener { 236 237 /** 238 * Callback method to be invoked when an item in this AdapterView has 239 * been clicked. 240 * <p> 241 * Implementers can call getItemAtPosition(position) if they need 242 * to access the data associated with the selected item. 243 * 244 * @param parent The AdapterView where the click happened. 245 * @param view The view within the AdapterView that was clicked (this 246 * will be a view provided by the adapter) 247 * @param position The position of the view in the adapter. 248 * @param id The row id of the item that was clicked. 249 */ 250 void onItemClick(AdapterView<?> parent, View view, int position, long id); 251 } 252 253 /** 254 * Register a callback to be invoked when an item in this AdapterView has 255 * been clicked. 256 * 257 * @param listener The callback that will be invoked. 258 */ 259 public void setOnItemClickListener(OnItemClickListener listener) { 260 mOnItemClickListener = listener; 261 } 262 263 /** 264 * @return The callback to be invoked with an item in this AdapterView has 265 * been clicked, or null id no callback has been set. 266 */ 267 public final OnItemClickListener getOnItemClickListener() { 268 return mOnItemClickListener; 269 } 270 271 /** 272 * Call the OnItemClickListener, if it is defined. 273 * 274 * @param view The view within the AdapterView that was clicked. 275 * @param position The position of the view in the adapter. 276 * @param id The row id of the item that was clicked. 277 * @return True if there was an assigned OnItemClickListener that was 278 * called, false otherwise is returned. 279 */ 280 public boolean performItemClick(View view, int position, long id) { 281 if (mOnItemClickListener != null) { 282 playSoundEffect(SoundEffectConstants.CLICK); 283 if (view != null) { 284 view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); 285 } 286 mOnItemClickListener.onItemClick(this, view, position, id); 287 return true; 288 } 289 290 return false; 291 } 292 293 /** 294 * Interface definition for a callback to be invoked when an item in this 295 * view has been clicked and held. 296 */ 297 public interface OnItemLongClickListener { 298 /** 299 * Callback method to be invoked when an item in this view has been 300 * clicked and held. 301 * 302 * Implementers can call getItemAtPosition(position) if they need to access 303 * the data associated with the selected item. 304 * 305 * @param parent The AbsListView where the click happened 306 * @param view The view within the AbsListView that was clicked 307 * @param position The position of the view in the list 308 * @param id The row id of the item that was clicked 309 * 310 * @return true if the callback consumed the long click, false otherwise 311 */ 312 boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id); 313 } 314 315 316 /** 317 * Register a callback to be invoked when an item in this AdapterView has 318 * been clicked and held 319 * 320 * @param listener The callback that will run 321 */ 322 public void setOnItemLongClickListener(OnItemLongClickListener listener) { 323 if (!isLongClickable()) { 324 setLongClickable(true); 325 } 326 mOnItemLongClickListener = listener; 327 } 328 329 /** 330 * @return The callback to be invoked with an item in this AdapterView has 331 * been clicked and held, or null id no callback as been set. 332 */ 333 public final OnItemLongClickListener getOnItemLongClickListener() { 334 return mOnItemLongClickListener; 335 } 336 337 /** 338 * Interface definition for a callback to be invoked when 339 * an item in this view has been selected. 340 */ 341 public interface OnItemSelectedListener { 342 /** 343 * <p>Callback method to be invoked when an item in this view has been 344 * selected. This callback is invoked only when the newly selected 345 * position is different from the previously selected position or if 346 * there was no selected item.</p> 347 * 348 * Impelmenters can call getItemAtPosition(position) if they need to access the 349 * data associated with the selected item. 350 * 351 * @param parent The AdapterView where the selection happened 352 * @param view The view within the AdapterView that was clicked 353 * @param position The position of the view in the adapter 354 * @param id The row id of the item that is selected 355 */ 356 void onItemSelected(AdapterView<?> parent, View view, int position, long id); 357 358 /** 359 * Callback method to be invoked when the selection disappears from this 360 * view. The selection can disappear for instance when touch is activated 361 * or when the adapter becomes empty. 362 * 363 * @param parent The AdapterView that now contains no selected item. 364 */ 365 void onNothingSelected(AdapterView<?> parent); 366 } 367 368 369 /** 370 * Register a callback to be invoked when an item in this AdapterView has 371 * been selected. 372 * 373 * @param listener The callback that will run 374 */ 375 public void setOnItemSelectedListener(OnItemSelectedListener listener) { 376 mOnItemSelectedListener = listener; 377 } 378 379 public final OnItemSelectedListener getOnItemSelectedListener() { 380 return mOnItemSelectedListener; 381 } 382 383 /** 384 * Extra menu information provided to the 385 * {@link android.view.View.OnCreateContextMenuListener#onCreateContextMenu(ContextMenu, View, ContextMenuInfo) } 386 * callback when a context menu is brought up for this AdapterView. 387 * 388 */ 389 public static class AdapterContextMenuInfo implements ContextMenu.ContextMenuInfo { 390 391 public AdapterContextMenuInfo(View targetView, int position, long id) { 392 this.targetView = targetView; 393 this.position = position; 394 this.id = id; 395 } 396 397 /** 398 * The child view for which the context menu is being displayed. This 399 * will be one of the children of this AdapterView. 400 */ 401 public View targetView; 402 403 /** 404 * The position in the adapter for which the context menu is being 405 * displayed. 406 */ 407 public int position; 408 409 /** 410 * The row id of the item for which the context menu is being displayed. 411 */ 412 public long id; 413 } 414 415 /** 416 * Returns the adapter currently associated with this widget. 417 * 418 * @return The adapter used to provide this view's content. 419 */ 420 public abstract T getAdapter(); 421 422 /** 423 * Sets the adapter that provides the data and the views to represent the data 424 * in this widget. 425 * 426 * @param adapter The adapter to use to create this view's content. 427 */ 428 public abstract void setAdapter(T adapter); 429 430 /** 431 * This method is not supported and throws an UnsupportedOperationException when called. 432 * 433 * @param child Ignored. 434 * 435 * @throws UnsupportedOperationException Every time this method is invoked. 436 */ 437 @Override 438 public void addView(View child) { 439 throw new UnsupportedOperationException("addView(View) is not supported in AdapterView"); 440 } 441 442 /** 443 * This method is not supported and throws an UnsupportedOperationException when called. 444 * 445 * @param child Ignored. 446 * @param index Ignored. 447 * 448 * @throws UnsupportedOperationException Every time this method is invoked. 449 */ 450 @Override 451 public void addView(View child, int index) { 452 throw new UnsupportedOperationException("addView(View, int) is not supported in AdapterView"); 453 } 454 455 /** 456 * This method is not supported and throws an UnsupportedOperationException when called. 457 * 458 * @param child Ignored. 459 * @param params Ignored. 460 * 461 * @throws UnsupportedOperationException Every time this method is invoked. 462 */ 463 @Override 464 public void addView(View child, LayoutParams params) { 465 throw new UnsupportedOperationException("addView(View, LayoutParams) " 466 + "is not supported in AdapterView"); 467 } 468 469 /** 470 * This method is not supported and throws an UnsupportedOperationException when called. 471 * 472 * @param child Ignored. 473 * @param index Ignored. 474 * @param params Ignored. 475 * 476 * @throws UnsupportedOperationException Every time this method is invoked. 477 */ 478 @Override 479 public void addView(View child, int index, LayoutParams params) { 480 throw new UnsupportedOperationException("addView(View, int, LayoutParams) " 481 + "is not supported in AdapterView"); 482 } 483 484 /** 485 * This method is not supported and throws an UnsupportedOperationException when called. 486 * 487 * @param child Ignored. 488 * 489 * @throws UnsupportedOperationException Every time this method is invoked. 490 */ 491 @Override 492 public void removeView(View child) { 493 throw new UnsupportedOperationException("removeView(View) is not supported in AdapterView"); 494 } 495 496 /** 497 * This method is not supported and throws an UnsupportedOperationException when called. 498 * 499 * @param index Ignored. 500 * 501 * @throws UnsupportedOperationException Every time this method is invoked. 502 */ 503 @Override 504 public void removeViewAt(int index) { 505 throw new UnsupportedOperationException("removeViewAt(int) is not supported in AdapterView"); 506 } 507 508 /** 509 * This method is not supported and throws an UnsupportedOperationException when called. 510 * 511 * @throws UnsupportedOperationException Every time this method is invoked. 512 */ 513 @Override 514 public void removeAllViews() { 515 throw new UnsupportedOperationException("removeAllViews() is not supported in AdapterView"); 516 } 517 518 @Override 519 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 520 mLayoutHeight = getHeight(); 521 } 522 523 /** 524 * Return the position of the currently selected item within the adapter's data set 525 * 526 * @return int Position (starting at 0), or {@link #INVALID_POSITION} if there is nothing selected. 527 */ 528 @ViewDebug.CapturedViewProperty 529 public int getSelectedItemPosition() { 530 return mNextSelectedPosition; 531 } 532 533 /** 534 * @return The id corresponding to the currently selected item, or {@link #INVALID_ROW_ID} 535 * if nothing is selected. 536 */ 537 @ViewDebug.CapturedViewProperty 538 public long getSelectedItemId() { 539 return mNextSelectedRowId; 540 } 541 542 /** 543 * @return The view corresponding to the currently selected item, or null 544 * if nothing is selected 545 */ 546 public abstract View getSelectedView(); 547 548 /** 549 * @return The data corresponding to the currently selected item, or 550 * null if there is nothing selected. 551 */ 552 public Object getSelectedItem() { 553 T adapter = getAdapter(); 554 int selection = getSelectedItemPosition(); 555 if (adapter != null && adapter.getCount() > 0 && selection >= 0) { 556 return adapter.getItem(selection); 557 } else { 558 return null; 559 } 560 } 561 562 /** 563 * @return The number of items owned by the Adapter associated with this 564 * AdapterView. (This is the number of data items, which may be 565 * larger than the number of visible views.) 566 */ 567 @ViewDebug.CapturedViewProperty 568 public int getCount() { 569 return mItemCount; 570 } 571 572 /** 573 * Get the position within the adapter's data set for the view, where view is a an adapter item 574 * or a descendant of an adapter item. 575 * 576 * @param view an adapter item, or a descendant of an adapter item. This must be visible in this 577 * AdapterView at the time of the call. 578 * @return the position within the adapter's data set of the view, or {@link #INVALID_POSITION} 579 * if the view does not correspond to a list item (or it is not currently visible). 580 */ 581 public int getPositionForView(View view) { 582 View listItem = view; 583 try { 584 View v; 585 while (!(v = (View) listItem.getParent()).equals(this)) { 586 listItem = v; 587 } 588 } catch (ClassCastException e) { 589 // We made it up to the window without find this list view 590 return INVALID_POSITION; 591 } 592 593 // Search the children for the list item 594 final int childCount = getChildCount(); 595 for (int i = 0; i < childCount; i++) { 596 if (getChildAt(i).equals(listItem)) { 597 return mFirstPosition + i; 598 } 599 } 600 601 // Child not found! 602 return INVALID_POSITION; 603 } 604 605 /** 606 * Returns the position within the adapter's data set for the first item 607 * displayed on screen. 608 * 609 * @return The position within the adapter's data set 610 */ 611 public int getFirstVisiblePosition() { 612 return mFirstPosition; 613 } 614 615 /** 616 * Returns the position within the adapter's data set for the last item 617 * displayed on screen. 618 * 619 * @return The position within the adapter's data set 620 */ 621 public int getLastVisiblePosition() { 622 return mFirstPosition + getChildCount() - 1; 623 } 624 625 /** 626 * Sets the currently selected item. To support accessibility subclasses that 627 * override this method must invoke the overriden super method first. 628 * 629 * @param position Index (starting at 0) of the data item to be selected. 630 */ 631 public abstract void setSelection(int position); 632 633 /** 634 * Sets the view to show if the adapter is empty 635 */ 636 @android.view.RemotableViewMethod 637 public void setEmptyView(View emptyView) { 638 mEmptyView = emptyView; 639 640 final T adapter = getAdapter(); 641 final boolean empty = ((adapter == null) || adapter.isEmpty()); 642 updateEmptyStatus(empty); 643 } 644 645 /** 646 * When the current adapter is empty, the AdapterView can display a special view 647 * call the empty view. The empty view is used to provide feedback to the user 648 * that no data is available in this AdapterView. 649 * 650 * @return The view to show if the adapter is empty. 651 */ 652 public View getEmptyView() { 653 return mEmptyView; 654 } 655 656 /** 657 * Indicates whether this view is in filter mode. Filter mode can for instance 658 * be enabled by a user when typing on the keyboard. 659 * 660 * @return True if the view is in filter mode, false otherwise. 661 */ 662 boolean isInFilterMode() { 663 return false; 664 } 665 666 @Override 667 public void setFocusable(boolean focusable) { 668 final T adapter = getAdapter(); 669 final boolean empty = adapter == null || adapter.getCount() == 0; 670 671 mDesiredFocusableState = focusable; 672 if (!focusable) { 673 mDesiredFocusableInTouchModeState = false; 674 } 675 676 super.setFocusable(focusable && (!empty || isInFilterMode())); 677 } 678 679 @Override 680 public void setFocusableInTouchMode(boolean focusable) { 681 final T adapter = getAdapter(); 682 final boolean empty = adapter == null || adapter.getCount() == 0; 683 684 mDesiredFocusableInTouchModeState = focusable; 685 if (focusable) { 686 mDesiredFocusableState = true; 687 } 688 689 super.setFocusableInTouchMode(focusable && (!empty || isInFilterMode())); 690 } 691 692 void checkFocus() { 693 final T adapter = getAdapter(); 694 final boolean empty = adapter == null || adapter.getCount() == 0; 695 final boolean focusable = !empty || isInFilterMode(); 696 // The order in which we set focusable in touch mode/focusable may matter 697 // for the client, see View.setFocusableInTouchMode() comments for more 698 // details 699 super.setFocusableInTouchMode(focusable && mDesiredFocusableInTouchModeState); 700 super.setFocusable(focusable && mDesiredFocusableState); 701 if (mEmptyView != null) { 702 updateEmptyStatus((adapter == null) || adapter.isEmpty()); 703 } 704 } 705 706 /** 707 * Update the status of the list based on the empty parameter. If empty is true and 708 * we have an empty view, display it. In all the other cases, make sure that the listview 709 * is VISIBLE and that the empty view is GONE (if it's not null). 710 */ 711 private void updateEmptyStatus(boolean empty) { 712 if (isInFilterMode()) { 713 empty = false; 714 } 715 716 if (empty) { 717 if (mEmptyView != null) { 718 mEmptyView.setVisibility(View.VISIBLE); 719 setVisibility(View.GONE); 720 } else { 721 // If the caller just removed our empty view, make sure the list view is visible 722 setVisibility(View.VISIBLE); 723 } 724 725 // We are now GONE, so pending layouts will not be dispatched. 726 // Force one here to make sure that the state of the list matches 727 // the state of the adapter. 728 if (mDataChanged) { 729 this.onLayout(false, mLeft, mTop, mRight, mBottom); 730 } 731 } else { 732 if (mEmptyView != null) mEmptyView.setVisibility(View.GONE); 733 setVisibility(View.VISIBLE); 734 } 735 } 736 737 /** 738 * Gets the data associated with the specified position in the list. 739 * 740 * @param position Which data to get 741 * @return The data associated with the specified position in the list 742 */ 743 public Object getItemAtPosition(int position) { 744 T adapter = getAdapter(); 745 return (adapter == null || position < 0) ? null : adapter.getItem(position); 746 } 747 748 public long getItemIdAtPosition(int position) { 749 T adapter = getAdapter(); 750 return (adapter == null || position < 0) ? INVALID_ROW_ID : adapter.getItemId(position); 751 } 752 753 @Override 754 public void setOnClickListener(OnClickListener l) { 755 throw new RuntimeException("Don't call setOnClickListener for an AdapterView. " 756 + "You probably want setOnItemClickListener instead"); 757 } 758 759 /** 760 * Override to prevent freezing of any views created by the adapter. 761 */ 762 @Override 763 protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) { 764 dispatchFreezeSelfOnly(container); 765 } 766 767 /** 768 * Override to prevent thawing of any views created by the adapter. 769 */ 770 @Override 771 protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) { 772 dispatchThawSelfOnly(container); 773 } 774 775 class AdapterDataSetObserver extends DataSetObserver { 776 777 private Parcelable mInstanceState = null; 778 779 @Override 780 public void onChanged() { 781 mDataChanged = true; 782 mOldItemCount = mItemCount; 783 mItemCount = getAdapter().getCount(); 784 785 // Detect the case where a cursor that was previously invalidated has 786 // been repopulated with new data. 787 if (AdapterView.this.getAdapter().hasStableIds() && mInstanceState != null 788 && mOldItemCount == 0 && mItemCount > 0) { 789 AdapterView.this.onRestoreInstanceState(mInstanceState); 790 mInstanceState = null; 791 } else { 792 rememberSyncState(); 793 } 794 checkFocus(); 795 requestLayout(); 796 } 797 798 @Override 799 public void onInvalidated() { 800 mDataChanged = true; 801 802 if (AdapterView.this.getAdapter().hasStableIds()) { 803 // Remember the current state for the case where our hosting activity is being 804 // stopped and later restarted 805 mInstanceState = AdapterView.this.onSaveInstanceState(); 806 } 807 808 // Data is invalid so we should reset our state 809 mOldItemCount = mItemCount; 810 mItemCount = 0; 811 mSelectedPosition = INVALID_POSITION; 812 mSelectedRowId = INVALID_ROW_ID; 813 mNextSelectedPosition = INVALID_POSITION; 814 mNextSelectedRowId = INVALID_ROW_ID; 815 mNeedSync = false; 816 817 checkFocus(); 818 requestLayout(); 819 } 820 821 public void clearSavedState() { 822 mInstanceState = null; 823 } 824 } 825 826 @Override 827 protected void onDetachedFromWindow() { 828 super.onDetachedFromWindow(); 829 removeCallbacks(mSelectionNotifier); 830 } 831 832 private class SelectionNotifier implements Runnable { 833 public void run() { 834 if (mDataChanged) { 835 // Data has changed between when this SelectionNotifier 836 // was posted and now. We need to wait until the AdapterView 837 // has been synched to the new data. 838 if (getAdapter() != null) { 839 post(this); 840 } 841 } else { 842 fireOnSelected(); 843 } 844 } 845 } 846 847 void selectionChanged() { 848 if (mOnItemSelectedListener != null) { 849 if (mInLayout || mBlockLayoutRequests) { 850 // If we are in a layout traversal, defer notification 851 // by posting. This ensures that the view tree is 852 // in a consistent state and is able to accomodate 853 // new layout or invalidate requests. 854 if (mSelectionNotifier == null) { 855 mSelectionNotifier = new SelectionNotifier(); 856 } 857 post(mSelectionNotifier); 858 } else { 859 fireOnSelected(); 860 } 861 } 862 863 // we fire selection events here not in View 864 if (mSelectedPosition != ListView.INVALID_POSITION && isShown() && !isInTouchMode()) { 865 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); 866 } 867 } 868 869 private void fireOnSelected() { 870 if (mOnItemSelectedListener == null) 871 return; 872 873 int selection = this.getSelectedItemPosition(); 874 if (selection >= 0) { 875 View v = getSelectedView(); 876 mOnItemSelectedListener.onItemSelected(this, v, selection, 877 getAdapter().getItemId(selection)); 878 } else { 879 mOnItemSelectedListener.onNothingSelected(this); 880 } 881 } 882 883 @Override 884 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 885 View selectedView = getSelectedView(); 886 if (selectedView != null && selectedView.getVisibility() == VISIBLE 887 && selectedView.dispatchPopulateAccessibilityEvent(event)) { 888 return true; 889 } 890 return false; 891 } 892 893 @Override 894 public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) { 895 if (super.onRequestSendAccessibilityEvent(child, event)) { 896 // Add a record for ourselves as well. 897 AccessibilityEvent record = AccessibilityEvent.obtain(); 898 onInitializeAccessibilityEvent(record); 899 // Populate with the text of the requesting child. 900 child.dispatchPopulateAccessibilityEvent(record); 901 event.appendRecord(record); 902 return true; 903 } 904 return false; 905 } 906 907 @Override 908 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 909 super.onInitializeAccessibilityNodeInfo(info); 910 info.setScrollable(isScrollableForAccessibility()); 911 View selectedView = getSelectedView(); 912 if (selectedView != null) { 913 info.setEnabled(selectedView.isEnabled()); 914 } 915 } 916 917 @Override 918 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 919 super.onInitializeAccessibilityEvent(event); 920 event.setScrollable(isScrollableForAccessibility()); 921 View selectedView = getSelectedView(); 922 if (selectedView != null) { 923 event.setEnabled(selectedView.isEnabled()); 924 } 925 event.setFromIndex(getFirstVisiblePosition()); 926 event.setToIndex(getLastVisiblePosition()); 927 event.setItemCount(getAdapter().getCount()); 928 } 929 930 private boolean isScrollableForAccessibility() { 931 T adapter = getAdapter(); 932 if (adapter != null) { 933 final int itemCount = adapter.getCount(); 934 return itemCount > 0 935 && (getFirstVisiblePosition() > 0 || getLastVisiblePosition() < itemCount - 1); 936 } 937 return false; 938 } 939 940 @Override 941 protected boolean canAnimate() { 942 return super.canAnimate() && mItemCount > 0; 943 } 944 945 void handleDataChanged() { 946 final int count = mItemCount; 947 boolean found = false; 948 949 if (count > 0) { 950 951 int newPos; 952 953 // Find the row we are supposed to sync to 954 if (mNeedSync) { 955 // Update this first, since setNextSelectedPositionInt inspects 956 // it 957 mNeedSync = false; 958 959 // See if we can find a position in the new data with the same 960 // id as the old selection 961 newPos = findSyncPosition(); 962 if (newPos >= 0) { 963 // Verify that new selection is selectable 964 int selectablePos = lookForSelectablePosition(newPos, true); 965 if (selectablePos == newPos) { 966 // Same row id is selected 967 setNextSelectedPositionInt(newPos); 968 found = true; 969 } 970 } 971 } 972 if (!found) { 973 // Try to use the same position if we can't find matching data 974 newPos = getSelectedItemPosition(); 975 976 // Pin position to the available range 977 if (newPos >= count) { 978 newPos = count - 1; 979 } 980 if (newPos < 0) { 981 newPos = 0; 982 } 983 984 // Make sure we select something selectable -- first look down 985 int selectablePos = lookForSelectablePosition(newPos, true); 986 if (selectablePos < 0) { 987 // Looking down didn't work -- try looking up 988 selectablePos = lookForSelectablePosition(newPos, false); 989 } 990 if (selectablePos >= 0) { 991 setNextSelectedPositionInt(selectablePos); 992 checkSelectionChanged(); 993 found = true; 994 } 995 } 996 } 997 if (!found) { 998 // Nothing is selected 999 mSelectedPosition = INVALID_POSITION; 1000 mSelectedRowId = INVALID_ROW_ID; 1001 mNextSelectedPosition = INVALID_POSITION; 1002 mNextSelectedRowId = INVALID_ROW_ID; 1003 mNeedSync = false; 1004 checkSelectionChanged(); 1005 } 1006 } 1007 1008 void checkSelectionChanged() { 1009 if ((mSelectedPosition != mOldSelectedPosition) || (mSelectedRowId != mOldSelectedRowId)) { 1010 selectionChanged(); 1011 mOldSelectedPosition = mSelectedPosition; 1012 mOldSelectedRowId = mSelectedRowId; 1013 } 1014 } 1015 1016 /** 1017 * Searches the adapter for a position matching mSyncRowId. The search starts at mSyncPosition 1018 * and then alternates between moving up and moving down until 1) we find the right position, or 1019 * 2) we run out of time, or 3) we have looked at every position 1020 * 1021 * @return Position of the row that matches mSyncRowId, or {@link #INVALID_POSITION} if it can't 1022 * be found 1023 */ 1024 int findSyncPosition() { 1025 int count = mItemCount; 1026 1027 if (count == 0) { 1028 return INVALID_POSITION; 1029 } 1030 1031 long idToMatch = mSyncRowId; 1032 int seed = mSyncPosition; 1033 1034 // If there isn't a selection don't hunt for it 1035 if (idToMatch == INVALID_ROW_ID) { 1036 return INVALID_POSITION; 1037 } 1038 1039 // Pin seed to reasonable values 1040 seed = Math.max(0, seed); 1041 seed = Math.min(count - 1, seed); 1042 1043 long endTime = SystemClock.uptimeMillis() + SYNC_MAX_DURATION_MILLIS; 1044 1045 long rowId; 1046 1047 // first position scanned so far 1048 int first = seed; 1049 1050 // last position scanned so far 1051 int last = seed; 1052 1053 // True if we should move down on the next iteration 1054 boolean next = false; 1055 1056 // True when we have looked at the first item in the data 1057 boolean hitFirst; 1058 1059 // True when we have looked at the last item in the data 1060 boolean hitLast; 1061 1062 // Get the item ID locally (instead of getItemIdAtPosition), so 1063 // we need the adapter 1064 T adapter = getAdapter(); 1065 if (adapter == null) { 1066 return INVALID_POSITION; 1067 } 1068 1069 while (SystemClock.uptimeMillis() <= endTime) { 1070 rowId = adapter.getItemId(seed); 1071 if (rowId == idToMatch) { 1072 // Found it! 1073 return seed; 1074 } 1075 1076 hitLast = last == count - 1; 1077 hitFirst = first == 0; 1078 1079 if (hitLast && hitFirst) { 1080 // Looked at everything 1081 break; 1082 } 1083 1084 if (hitFirst || (next && !hitLast)) { 1085 // Either we hit the top, or we are trying to move down 1086 last++; 1087 seed = last; 1088 // Try going up next time 1089 next = false; 1090 } else if (hitLast || (!next && !hitFirst)) { 1091 // Either we hit the bottom, or we are trying to move up 1092 first--; 1093 seed = first; 1094 // Try going down next time 1095 next = true; 1096 } 1097 1098 } 1099 1100 return INVALID_POSITION; 1101 } 1102 1103 /** 1104 * Find a position that can be selected (i.e., is not a separator). 1105 * 1106 * @param position The starting position to look at. 1107 * @param lookDown Whether to look down for other positions. 1108 * @return The next selectable position starting at position and then searching either up or 1109 * down. Returns {@link #INVALID_POSITION} if nothing can be found. 1110 */ 1111 int lookForSelectablePosition(int position, boolean lookDown) { 1112 return position; 1113 } 1114 1115 /** 1116 * Utility to keep mSelectedPosition and mSelectedRowId in sync 1117 * @param position Our current position 1118 */ 1119 void setSelectedPositionInt(int position) { 1120 mSelectedPosition = position; 1121 mSelectedRowId = getItemIdAtPosition(position); 1122 } 1123 1124 /** 1125 * Utility to keep mNextSelectedPosition and mNextSelectedRowId in sync 1126 * @param position Intended value for mSelectedPosition the next time we go 1127 * through layout 1128 */ 1129 void setNextSelectedPositionInt(int position) { 1130 mNextSelectedPosition = position; 1131 mNextSelectedRowId = getItemIdAtPosition(position); 1132 // If we are trying to sync to the selection, update that too 1133 if (mNeedSync && mSyncMode == SYNC_SELECTED_POSITION && position >= 0) { 1134 mSyncPosition = position; 1135 mSyncRowId = mNextSelectedRowId; 1136 } 1137 } 1138 1139 /** 1140 * Remember enough information to restore the screen state when the data has 1141 * changed. 1142 * 1143 */ 1144 void rememberSyncState() { 1145 if (getChildCount() > 0) { 1146 mNeedSync = true; 1147 mSyncHeight = mLayoutHeight; 1148 if (mSelectedPosition >= 0) { 1149 // Sync the selection state 1150 View v = getChildAt(mSelectedPosition - mFirstPosition); 1151 mSyncRowId = mNextSelectedRowId; 1152 mSyncPosition = mNextSelectedPosition; 1153 if (v != null) { 1154 mSpecificTop = v.getTop(); 1155 } 1156 mSyncMode = SYNC_SELECTED_POSITION; 1157 } else { 1158 // Sync the based on the offset of the first view 1159 View v = getChildAt(0); 1160 T adapter = getAdapter(); 1161 if (mFirstPosition >= 0 && mFirstPosition < adapter.getCount()) { 1162 mSyncRowId = adapter.getItemId(mFirstPosition); 1163 } else { 1164 mSyncRowId = NO_ID; 1165 } 1166 mSyncPosition = mFirstPosition; 1167 if (v != null) { 1168 mSpecificTop = v.getTop(); 1169 } 1170 mSyncMode = SYNC_FIRST_POSITION; 1171 } 1172 } 1173 } 1174} 1175