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