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