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 && getViewRootImpl() != null 865 && getViewRootImpl().isLayoutRequested()) { 866 // Data has changed between when this SelectionNotifier was 867 // posted and now. Postpone the notification until the next 868 // layout is complete and we run checkSelectionChanged(). 869 if (getAdapter() != null) { 870 mPendingSelectionNotifier = this; 871 } 872 } else { 873 dispatchOnItemSelected(); 874 } 875 } 876 } 877 878 void selectionChanged() { 879 // We're about to post or run the selection notifier, so we don't need 880 // a pending notifier. 881 mPendingSelectionNotifier = null; 882 883 if (mOnItemSelectedListener != null 884 || AccessibilityManager.getInstance(mContext).isEnabled()) { 885 if (mInLayout || mBlockLayoutRequests) { 886 // If we are in a layout traversal, defer notification 887 // by posting. This ensures that the view tree is 888 // in a consistent state and is able to accommodate 889 // new layout or invalidate requests. 890 if (mSelectionNotifier == null) { 891 mSelectionNotifier = new SelectionNotifier(); 892 } else { 893 removeCallbacks(mSelectionNotifier); 894 } 895 post(mSelectionNotifier); 896 } else { 897 dispatchOnItemSelected(); 898 } 899 } 900 } 901 902 private void dispatchOnItemSelected() { 903 fireOnSelected(); 904 performAccessibilityActionsOnSelected(); 905 } 906 907 private void fireOnSelected() { 908 if (mOnItemSelectedListener == null) { 909 return; 910 } 911 final int selection = getSelectedItemPosition(); 912 if (selection >= 0) { 913 View v = getSelectedView(); 914 mOnItemSelectedListener.onItemSelected(this, v, selection, 915 getAdapter().getItemId(selection)); 916 } else { 917 mOnItemSelectedListener.onNothingSelected(this); 918 } 919 } 920 921 private void performAccessibilityActionsOnSelected() { 922 if (!AccessibilityManager.getInstance(mContext).isEnabled()) { 923 return; 924 } 925 final int position = getSelectedItemPosition(); 926 if (position >= 0) { 927 // we fire selection events here not in View 928 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); 929 } 930 } 931 932 @Override 933 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 934 View selectedView = getSelectedView(); 935 if (selectedView != null && selectedView.getVisibility() == VISIBLE 936 && selectedView.dispatchPopulateAccessibilityEvent(event)) { 937 return true; 938 } 939 return false; 940 } 941 942 @Override 943 public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) { 944 if (super.onRequestSendAccessibilityEvent(child, event)) { 945 // Add a record for ourselves as well. 946 AccessibilityEvent record = AccessibilityEvent.obtain(); 947 onInitializeAccessibilityEvent(record); 948 // Populate with the text of the requesting child. 949 child.dispatchPopulateAccessibilityEvent(record); 950 event.appendRecord(record); 951 return true; 952 } 953 return false; 954 } 955 956 @Override 957 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 958 super.onInitializeAccessibilityNodeInfo(info); 959 info.setClassName(AdapterView.class.getName()); 960 info.setScrollable(isScrollableForAccessibility()); 961 View selectedView = getSelectedView(); 962 if (selectedView != null) { 963 info.setEnabled(selectedView.isEnabled()); 964 } 965 } 966 967 @Override 968 public void onInitializeAccessibilityEvent(AccessibilityEvent event) { 969 super.onInitializeAccessibilityEvent(event); 970 event.setClassName(AdapterView.class.getName()); 971 event.setScrollable(isScrollableForAccessibility()); 972 View selectedView = getSelectedView(); 973 if (selectedView != null) { 974 event.setEnabled(selectedView.isEnabled()); 975 } 976 event.setCurrentItemIndex(getSelectedItemPosition()); 977 event.setFromIndex(getFirstVisiblePosition()); 978 event.setToIndex(getLastVisiblePosition()); 979 event.setItemCount(getCount()); 980 } 981 982 private boolean isScrollableForAccessibility() { 983 T adapter = getAdapter(); 984 if (adapter != null) { 985 final int itemCount = adapter.getCount(); 986 return itemCount > 0 987 && (getFirstVisiblePosition() > 0 || getLastVisiblePosition() < itemCount - 1); 988 } 989 return false; 990 } 991 992 @Override 993 protected boolean canAnimate() { 994 return super.canAnimate() && mItemCount > 0; 995 } 996 997 void handleDataChanged() { 998 final int count = mItemCount; 999 boolean found = false; 1000 1001 if (count > 0) { 1002 1003 int newPos; 1004 1005 // Find the row we are supposed to sync to 1006 if (mNeedSync) { 1007 // Update this first, since setNextSelectedPositionInt inspects 1008 // it 1009 mNeedSync = false; 1010 1011 // See if we can find a position in the new data with the same 1012 // id as the old selection 1013 newPos = findSyncPosition(); 1014 if (newPos >= 0) { 1015 // Verify that new selection is selectable 1016 int selectablePos = lookForSelectablePosition(newPos, true); 1017 if (selectablePos == newPos) { 1018 // Same row id is selected 1019 setNextSelectedPositionInt(newPos); 1020 found = true; 1021 } 1022 } 1023 } 1024 if (!found) { 1025 // Try to use the same position if we can't find matching data 1026 newPos = getSelectedItemPosition(); 1027 1028 // Pin position to the available range 1029 if (newPos >= count) { 1030 newPos = count - 1; 1031 } 1032 if (newPos < 0) { 1033 newPos = 0; 1034 } 1035 1036 // Make sure we select something selectable -- first look down 1037 int selectablePos = lookForSelectablePosition(newPos, true); 1038 if (selectablePos < 0) { 1039 // Looking down didn't work -- try looking up 1040 selectablePos = lookForSelectablePosition(newPos, false); 1041 } 1042 if (selectablePos >= 0) { 1043 setNextSelectedPositionInt(selectablePos); 1044 checkSelectionChanged(); 1045 found = true; 1046 } 1047 } 1048 } 1049 if (!found) { 1050 // Nothing is selected 1051 mSelectedPosition = INVALID_POSITION; 1052 mSelectedRowId = INVALID_ROW_ID; 1053 mNextSelectedPosition = INVALID_POSITION; 1054 mNextSelectedRowId = INVALID_ROW_ID; 1055 mNeedSync = false; 1056 checkSelectionChanged(); 1057 } 1058 1059 notifySubtreeAccessibilityStateChangedIfNeeded(); 1060 } 1061 1062 /** 1063 * Called after layout to determine whether the selection position needs to 1064 * be updated. Also used to fire any pending selection events. 1065 */ 1066 void checkSelectionChanged() { 1067 if ((mSelectedPosition != mOldSelectedPosition) || (mSelectedRowId != mOldSelectedRowId)) { 1068 selectionChanged(); 1069 mOldSelectedPosition = mSelectedPosition; 1070 mOldSelectedRowId = mSelectedRowId; 1071 } 1072 1073 // If we have a pending selection notification -- and we won't if we 1074 // just fired one in selectionChanged() -- run it now. 1075 if (mPendingSelectionNotifier != null) { 1076 mPendingSelectionNotifier.run(); 1077 } 1078 } 1079 1080 /** 1081 * Searches the adapter for a position matching mSyncRowId. The search starts at mSyncPosition 1082 * and then alternates between moving up and moving down until 1) we find the right position, or 1083 * 2) we run out of time, or 3) we have looked at every position 1084 * 1085 * @return Position of the row that matches mSyncRowId, or {@link #INVALID_POSITION} if it can't 1086 * be found 1087 */ 1088 int findSyncPosition() { 1089 int count = mItemCount; 1090 1091 if (count == 0) { 1092 return INVALID_POSITION; 1093 } 1094 1095 long idToMatch = mSyncRowId; 1096 int seed = mSyncPosition; 1097 1098 // If there isn't a selection don't hunt for it 1099 if (idToMatch == INVALID_ROW_ID) { 1100 return INVALID_POSITION; 1101 } 1102 1103 // Pin seed to reasonable values 1104 seed = Math.max(0, seed); 1105 seed = Math.min(count - 1, seed); 1106 1107 long endTime = SystemClock.uptimeMillis() + SYNC_MAX_DURATION_MILLIS; 1108 1109 long rowId; 1110 1111 // first position scanned so far 1112 int first = seed; 1113 1114 // last position scanned so far 1115 int last = seed; 1116 1117 // True if we should move down on the next iteration 1118 boolean next = false; 1119 1120 // True when we have looked at the first item in the data 1121 boolean hitFirst; 1122 1123 // True when we have looked at the last item in the data 1124 boolean hitLast; 1125 1126 // Get the item ID locally (instead of getItemIdAtPosition), so 1127 // we need the adapter 1128 T adapter = getAdapter(); 1129 if (adapter == null) { 1130 return INVALID_POSITION; 1131 } 1132 1133 while (SystemClock.uptimeMillis() <= endTime) { 1134 rowId = adapter.getItemId(seed); 1135 if (rowId == idToMatch) { 1136 // Found it! 1137 return seed; 1138 } 1139 1140 hitLast = last == count - 1; 1141 hitFirst = first == 0; 1142 1143 if (hitLast && hitFirst) { 1144 // Looked at everything 1145 break; 1146 } 1147 1148 if (hitFirst || (next && !hitLast)) { 1149 // Either we hit the top, or we are trying to move down 1150 last++; 1151 seed = last; 1152 // Try going up next time 1153 next = false; 1154 } else if (hitLast || (!next && !hitFirst)) { 1155 // Either we hit the bottom, or we are trying to move up 1156 first--; 1157 seed = first; 1158 // Try going down next time 1159 next = true; 1160 } 1161 1162 } 1163 1164 return INVALID_POSITION; 1165 } 1166 1167 /** 1168 * Find a position that can be selected (i.e., is not a separator). 1169 * 1170 * @param position The starting position to look at. 1171 * @param lookDown Whether to look down for other positions. 1172 * @return The next selectable position starting at position and then searching either up or 1173 * down. Returns {@link #INVALID_POSITION} if nothing can be found. 1174 */ 1175 int lookForSelectablePosition(int position, boolean lookDown) { 1176 return position; 1177 } 1178 1179 /** 1180 * Utility to keep mSelectedPosition and mSelectedRowId in sync 1181 * @param position Our current position 1182 */ 1183 void setSelectedPositionInt(int position) { 1184 mSelectedPosition = position; 1185 mSelectedRowId = getItemIdAtPosition(position); 1186 } 1187 1188 /** 1189 * Utility to keep mNextSelectedPosition and mNextSelectedRowId in sync 1190 * @param position Intended value for mSelectedPosition the next time we go 1191 * through layout 1192 */ 1193 void setNextSelectedPositionInt(int position) { 1194 mNextSelectedPosition = position; 1195 mNextSelectedRowId = getItemIdAtPosition(position); 1196 // If we are trying to sync to the selection, update that too 1197 if (mNeedSync && mSyncMode == SYNC_SELECTED_POSITION && position >= 0) { 1198 mSyncPosition = position; 1199 mSyncRowId = mNextSelectedRowId; 1200 } 1201 } 1202 1203 /** 1204 * Remember enough information to restore the screen state when the data has 1205 * changed. 1206 * 1207 */ 1208 void rememberSyncState() { 1209 if (getChildCount() > 0) { 1210 mNeedSync = true; 1211 mSyncHeight = mLayoutHeight; 1212 if (mSelectedPosition >= 0) { 1213 // Sync the selection state 1214 View v = getChildAt(mSelectedPosition - mFirstPosition); 1215 mSyncRowId = mNextSelectedRowId; 1216 mSyncPosition = mNextSelectedPosition; 1217 if (v != null) { 1218 mSpecificTop = v.getTop(); 1219 } 1220 mSyncMode = SYNC_SELECTED_POSITION; 1221 } else { 1222 // Sync the based on the offset of the first view 1223 View v = getChildAt(0); 1224 T adapter = getAdapter(); 1225 if (mFirstPosition >= 0 && mFirstPosition < adapter.getCount()) { 1226 mSyncRowId = adapter.getItemId(mFirstPosition); 1227 } else { 1228 mSyncRowId = NO_ID; 1229 } 1230 mSyncPosition = mFirstPosition; 1231 if (v != null) { 1232 mSpecificTop = v.getTop(); 1233 } 1234 mSyncMode = SYNC_FIRST_POSITION; 1235 } 1236 } 1237 } 1238} 1239