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