AdapterViewICS.java revision da10fdd1400ecfd8d7f2e55651dd528d0614dfc5
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 */ 50abstract class AdapterViewICS<T extends Adapter> extends ViewGroup { 51 52 /** 53 * The item view type returned by {@link Adapter#getItemViewType(int)} when 54 * the adapter does not want the item's view recycled. 55 */ 56 static final int ITEM_VIEW_TYPE_IGNORE = -1; 57 58 /** 59 * The item view type returned by {@link Adapter#getItemViewType(int)} when 60 * the item is a header or footer. 61 */ 62 static final int ITEM_VIEW_TYPE_HEADER_OR_FOOTER = -2; 63 64 /** 65 * The position of the first child displayed 66 */ 67 @ViewDebug.ExportedProperty(category = "scrolling") 68 int mFirstPosition = 0; 69 70 /** 71 * The offset in pixels from the top of the AdapterView to the top 72 * of the view to select during the next layout. 73 */ 74 int mSpecificTop; 75 76 /** 77 * Position from which to start looking for mSyncRowId 78 */ 79 int mSyncPosition; 80 81 /** 82 * Row id to look for when data has changed 83 */ 84 long mSyncRowId = INVALID_ROW_ID; 85 86 /** 87 * Height of the view when mSyncPosition and mSyncRowId where set 88 */ 89 long mSyncHeight; 90 91 /** 92 * True if we need to sync to mSyncRowId 93 */ 94 boolean mNeedSync = false; 95 96 /** 97 * Indicates whether to sync based on the selection or position. Possible 98 * values are {@link #SYNC_SELECTED_POSITION} or 99 * {@link #SYNC_FIRST_POSITION}. 100 */ 101 int mSyncMode; 102 103 /** 104 * Our height after the last layout 105 */ 106 private int mLayoutHeight; 107 108 /** 109 * Sync based on the selected child 110 */ 111 static final int SYNC_SELECTED_POSITION = 0; 112 113 /** 114 * Sync based on the first child displayed 115 */ 116 static final int SYNC_FIRST_POSITION = 1; 117 118 /** 119 * Maximum amount of time to spend in {@link #findSyncPosition()} 120 */ 121 static final int SYNC_MAX_DURATION_MILLIS = 100; 122 123 /** 124 * Indicates that this view is currently being laid out. 125 */ 126 boolean mInLayout = false; 127 128 /** 129 * The listener that receives notifications when an item is selected. 130 */ 131 OnItemSelectedListener mOnItemSelectedListener; 132 133 /** 134 * The listener that receives notifications when an item is clicked. 135 */ 136 OnItemClickListener mOnItemClickListener; 137 138 /** 139 * The listener that receives notifications when an item is long clicked. 140 */ 141 OnItemLongClickListener mOnItemLongClickListener; 142 143 /** 144 * True if the data has changed since the last layout 145 */ 146 boolean mDataChanged; 147 148 /** 149 * The position within the adapter's data set of the item to select 150 * during the next layout. 151 */ 152 @ViewDebug.ExportedProperty(category = "list") 153 int mNextSelectedPosition = INVALID_POSITION; 154 155 /** 156 * The item id of the item to select during the next layout. 157 */ 158 long mNextSelectedRowId = INVALID_ROW_ID; 159 160 /** 161 * The position within the adapter's data set of the currently selected item. 162 */ 163 @ViewDebug.ExportedProperty(category = "list") 164 int mSelectedPosition = INVALID_POSITION; 165 166 /** 167 * The item id of the currently selected item. 168 */ 169 long mSelectedRowId = INVALID_ROW_ID; 170 171 /** 172 * View to show if there are no items to show. 173 */ 174 private View mEmptyView; 175 176 /** 177 * The number of items in the current adapter. 178 */ 179 @ViewDebug.ExportedProperty(category = "list") 180 int mItemCount; 181 182 /** 183 * The number of items in the adapter before a data changed event occurred. 184 */ 185 int mOldItemCount; 186 187 /** 188 * Represents an invalid position. All valid positions are in the range 0 to 1 less than the 189 * number of items in the current adapter. 190 */ 191 public static final int INVALID_POSITION = -1; 192 193 /** 194 * Represents an empty or invalid row id 195 */ 196 public static final long INVALID_ROW_ID = Long.MIN_VALUE; 197 198 /** 199 * The last selected position we used when notifying 200 */ 201 int mOldSelectedPosition = INVALID_POSITION; 202 203 /** 204 * The id of the last selected position we used when notifying 205 */ 206 long mOldSelectedRowId = INVALID_ROW_ID; 207 208 /** 209 * Indicates what focusable state is requested when calling setFocusable(). 210 * In addition to this, this view has other criteria for actually 211 * determining the focusable state (such as whether its empty or the text 212 * filter is shown). 213 * 214 * @see #setFocusable(boolean) 215 * @see #checkFocus() 216 */ 217 private boolean mDesiredFocusableState; 218 private boolean mDesiredFocusableInTouchModeState; 219 220 private SelectionNotifier mSelectionNotifier; 221 /** 222 * When set to true, calls to requestLayout() will not propagate up the parent hierarchy. 223 * This is used to layout the children during a layout pass. 224 */ 225 boolean mBlockLayoutRequests = false; 226 227 AdapterViewICS(Context context) { 228 super(context); 229 } 230 231 AdapterViewICS(Context context, AttributeSet attrs) { 232 super(context, attrs); 233 } 234 235 AdapterViewICS(Context context, AttributeSet attrs, int defStyle) { 236 super(context, attrs, defStyle); 237 } 238 239 /** 240 * Interface definition for a callback to be invoked when an item in this 241 * AdapterView has been clicked. 242 */ 243 public interface OnItemClickListener { 244 245 /** 246 * Callback method to be invoked when an item in this AdapterView has 247 * been clicked. 248 * <p> 249 * Implementers can call getItemAtPosition(position) if they need 250 * to access the data associated with the selected item. 251 * 252 * @param parent The AdapterView where the click happened. 253 * @param view The view within the AdapterView that was clicked (this 254 * will be a view provided by the adapter) 255 * @param position The position of the view in the adapter. 256 * @param id The row id of the item that was clicked. 257 */ 258 void onItemClick(AdapterViewICS<?> parent, View view, int position, long id); 259 } 260 261 class OnItemClickListenerWrapper implements AdapterView.OnItemClickListener { 262 263 private final OnItemClickListener mWrappedListener; 264 265 public OnItemClickListenerWrapper(OnItemClickListener listener) { 266 mWrappedListener = listener; 267 } 268 269 @Override 270 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 271 mWrappedListener.onItemClick(AdapterViewICS.this, view, position, id); 272 } 273 } 274 275 /** 276 * Register a callback to be invoked when an item in this AdapterView has 277 * been clicked. 278 * 279 * @param listener The callback that will be invoked. 280 */ 281 public void setOnItemClickListener(OnItemClickListener listener) { 282 mOnItemClickListener = listener; 283 } 284 285 /** 286 * @return The callback to be invoked with an item in this AdapterView has 287 * been clicked, or null id no callback has been set. 288 */ 289 public final OnItemClickListener getOnItemClickListener() { 290 return mOnItemClickListener; 291 } 292 293 /** 294 * Call the OnItemClickListener, if it is defined. 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 if (view != null) { 306 view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); 307 } 308 mOnItemClickListener.onItemClick(this, view, position, id); 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(AdapterViewICS<?> 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(AdapterViewICS<?> 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(AdapterViewICS<?> 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 public void setEmptyView(View emptyView) { 659 mEmptyView = emptyView; 660 661 final T adapter = getAdapter(); 662 final boolean empty = ((adapter == null) || adapter.isEmpty()); 663 updateEmptyStatus(empty); 664 } 665 666 /** 667 * When the current adapter is empty, the AdapterView can display a special view 668 * call the empty view. The empty view is used to provide feedback to the user 669 * that no data is available in this AdapterView. 670 * 671 * @return The view to show if the adapter is empty. 672 */ 673 public View getEmptyView() { 674 return mEmptyView; 675 } 676 677 /** 678 * Indicates whether this view is in filter mode. Filter mode can for instance 679 * be enabled by a user when typing on the keyboard. 680 * 681 * @return True if the view is in filter mode, false otherwise. 682 */ 683 boolean isInFilterMode() { 684 return false; 685 } 686 687 @Override 688 public void setFocusable(boolean focusable) { 689 final T adapter = getAdapter(); 690 final boolean empty = adapter == null || adapter.getCount() == 0; 691 692 mDesiredFocusableState = focusable; 693 if (!focusable) { 694 mDesiredFocusableInTouchModeState = false; 695 } 696 697 super.setFocusable(focusable && (!empty || isInFilterMode())); 698 } 699 700 @Override 701 public void setFocusableInTouchMode(boolean focusable) { 702 final T adapter = getAdapter(); 703 final boolean empty = adapter == null || adapter.getCount() == 0; 704 705 mDesiredFocusableInTouchModeState = focusable; 706 if (focusable) { 707 mDesiredFocusableState = true; 708 } 709 710 super.setFocusableInTouchMode(focusable && (!empty || isInFilterMode())); 711 } 712 713 void checkFocus() { 714 final T adapter = getAdapter(); 715 final boolean empty = adapter == null || adapter.getCount() == 0; 716 final boolean focusable = !empty || isInFilterMode(); 717 // The order in which we set focusable in touch mode/focusable may matter 718 // for the client, see View.setFocusableInTouchMode() comments for more 719 // details 720 super.setFocusableInTouchMode(focusable && mDesiredFocusableInTouchModeState); 721 super.setFocusable(focusable && mDesiredFocusableState); 722 if (mEmptyView != null) { 723 updateEmptyStatus((adapter == null) || adapter.isEmpty()); 724 } 725 } 726 727 /** 728 * Update the status of the list based on the empty parameter. If empty is true and 729 * we have an empty view, display it. In all the other cases, make sure that the listview 730 * is VISIBLE and that the empty view is GONE (if it's not null). 731 */ 732 private void updateEmptyStatus(boolean empty) { 733 if (isInFilterMode()) { 734 empty = false; 735 } 736 737 if (empty) { 738 if (mEmptyView != null) { 739 mEmptyView.setVisibility(View.VISIBLE); 740 setVisibility(View.GONE); 741 } else { 742 // If the caller just removed our empty view, make sure the list view is visible 743 setVisibility(View.VISIBLE); 744 } 745 746 // We are now GONE, so pending layouts will not be dispatched. 747 // Force one here to make sure that the state of the list matches 748 // the state of the adapter. 749 if (mDataChanged) { 750 this.onLayout(false, getLeft(), getTop(), getRight(), getBottom()); 751 } 752 } else { 753 if (mEmptyView != null) mEmptyView.setVisibility(View.GONE); 754 setVisibility(View.VISIBLE); 755 } 756 } 757 758 /** 759 * Gets the data associated with the specified position in the list. 760 * 761 * @param position Which data to get 762 * @return The data associated with the specified position in the list 763 */ 764 public Object getItemAtPosition(int position) { 765 T adapter = getAdapter(); 766 return (adapter == null || position < 0) ? null : adapter.getItem(position); 767 } 768 769 public long getItemIdAtPosition(int position) { 770 T adapter = getAdapter(); 771 return (adapter == null || position < 0) ? INVALID_ROW_ID : adapter.getItemId(position); 772 } 773 774 @Override 775 public void setOnClickListener(OnClickListener l) { 776 throw new RuntimeException("Don't call setOnClickListener for an AdapterView. " 777 + "You probably want setOnItemClickListener instead"); 778 } 779 780 /** 781 * Override to prevent freezing of any views created by the adapter. 782 */ 783 @Override 784 protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) { 785 dispatchFreezeSelfOnly(container); 786 } 787 788 /** 789 * Override to prevent thawing of any views created by the adapter. 790 */ 791 @Override 792 protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) { 793 dispatchThawSelfOnly(container); 794 } 795 796 class AdapterDataSetObserver extends DataSetObserver { 797 798 private Parcelable mInstanceState = null; 799 800 @Override 801 public void onChanged() { 802 mDataChanged = true; 803 mOldItemCount = mItemCount; 804 mItemCount = getAdapter().getCount(); 805 806 // Detect the case where a cursor that was previously invalidated has 807 // been repopulated with new data. 808 if (AdapterViewICS.this.getAdapter().hasStableIds() && mInstanceState != null 809 && mOldItemCount == 0 && mItemCount > 0) { 810 AdapterViewICS.this.onRestoreInstanceState(mInstanceState); 811 mInstanceState = null; 812 } else { 813 rememberSyncState(); 814 } 815 checkFocus(); 816 requestLayout(); 817 } 818 819 @Override 820 public void onInvalidated() { 821 mDataChanged = true; 822 823 if (AdapterViewICS.this.getAdapter().hasStableIds()) { 824 // Remember the current state for the case where our hosting activity is being 825 // stopped and later restarted 826 mInstanceState = AdapterViewICS.this.onSaveInstanceState(); 827 } 828 829 // Data is invalid so we should reset our state 830 mOldItemCount = mItemCount; 831 mItemCount = 0; 832 mSelectedPosition = INVALID_POSITION; 833 mSelectedRowId = INVALID_ROW_ID; 834 mNextSelectedPosition = INVALID_POSITION; 835 mNextSelectedRowId = INVALID_ROW_ID; 836 mNeedSync = false; 837 838 checkFocus(); 839 requestLayout(); 840 } 841 842 public void clearSavedState() { 843 mInstanceState = null; 844 } 845 } 846 847 @Override 848 protected void onDetachedFromWindow() { 849 super.onDetachedFromWindow(); 850 removeCallbacks(mSelectionNotifier); 851 } 852 853 private class SelectionNotifier implements Runnable { 854 public void run() { 855 if (mDataChanged) { 856 // Data has changed between when this SelectionNotifier 857 // was posted and now. We need to wait until the AdapterView 858 // has been synched to the new data. 859 if (getAdapter() != null) { 860 post(this); 861 } 862 } else { 863 fireOnSelected(); 864 } 865 } 866 } 867 868 void selectionChanged() { 869 if (mOnItemSelectedListener != null) { 870 if (mInLayout || mBlockLayoutRequests) { 871 // If we are in a layout traversal, defer notification 872 // by posting. This ensures that the view tree is 873 // in a consistent state and is able to accomodate 874 // new layout or invalidate requests. 875 if (mSelectionNotifier == null) { 876 mSelectionNotifier = new SelectionNotifier(); 877 } 878 post(mSelectionNotifier); 879 } else { 880 fireOnSelected(); 881 } 882 } 883 884 // we fire selection events here not in View 885 if (mSelectedPosition != ListView.INVALID_POSITION && isShown() && !isInTouchMode()) { 886 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); 887 } 888 } 889 890 private void fireOnSelected() { 891 if (mOnItemSelectedListener == null) 892 return; 893 894 int selection = this.getSelectedItemPosition(); 895 if (selection >= 0) { 896 View v = getSelectedView(); 897 mOnItemSelectedListener.onItemSelected(this, v, selection, 898 getAdapter().getItemId(selection)); 899 } else { 900 mOnItemSelectedListener.onNothingSelected(this); 901 } 902 } 903 904 @Override 905 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 906 View selectedView = getSelectedView(); 907 if (selectedView != null && selectedView.getVisibility() == VISIBLE 908 && selectedView.dispatchPopulateAccessibilityEvent(event)) { 909 return true; 910 } 911 return false; 912 } 913 914 @Override 915 protected boolean canAnimate() { 916 return super.canAnimate() && mItemCount > 0; 917 } 918 919 void handleDataChanged() { 920 final int count = mItemCount; 921 boolean found = false; 922 923 if (count > 0) { 924 925 int newPos; 926 927 // Find the row we are supposed to sync to 928 if (mNeedSync) { 929 // Update this first, since setNextSelectedPositionInt inspects 930 // it 931 mNeedSync = false; 932 933 // See if we can find a position in the new data with the same 934 // id as the old selection 935 newPos = findSyncPosition(); 936 if (newPos >= 0) { 937 // Verify that new selection is selectable 938 int selectablePos = lookForSelectablePosition(newPos, true); 939 if (selectablePos == newPos) { 940 // Same row id is selected 941 setNextSelectedPositionInt(newPos); 942 found = true; 943 } 944 } 945 } 946 if (!found) { 947 // Try to use the same position if we can't find matching data 948 newPos = getSelectedItemPosition(); 949 950 // Pin position to the available range 951 if (newPos >= count) { 952 newPos = count - 1; 953 } 954 if (newPos < 0) { 955 newPos = 0; 956 } 957 958 // Make sure we select something selectable -- first look down 959 int selectablePos = lookForSelectablePosition(newPos, true); 960 if (selectablePos < 0) { 961 // Looking down didn't work -- try looking up 962 selectablePos = lookForSelectablePosition(newPos, false); 963 } 964 if (selectablePos >= 0) { 965 setNextSelectedPositionInt(selectablePos); 966 checkSelectionChanged(); 967 found = true; 968 } 969 } 970 } 971 if (!found) { 972 // Nothing is selected 973 mSelectedPosition = INVALID_POSITION; 974 mSelectedRowId = INVALID_ROW_ID; 975 mNextSelectedPosition = INVALID_POSITION; 976 mNextSelectedRowId = INVALID_ROW_ID; 977 mNeedSync = false; 978 checkSelectionChanged(); 979 } 980 } 981 982 void checkSelectionChanged() { 983 if ((mSelectedPosition != mOldSelectedPosition) || (mSelectedRowId != mOldSelectedRowId)) { 984 selectionChanged(); 985 mOldSelectedPosition = mSelectedPosition; 986 mOldSelectedRowId = mSelectedRowId; 987 } 988 } 989 990 /** 991 * Searches the adapter for a position matching mSyncRowId. The search starts at mSyncPosition 992 * and then alternates between moving up and moving down until 1) we find the right position, or 993 * 2) we run out of time, or 3) we have looked at every position 994 * 995 * @return Position of the row that matches mSyncRowId, or {@link #INVALID_POSITION} if it can't 996 * be found 997 */ 998 int findSyncPosition() { 999 int count = mItemCount; 1000 1001 if (count == 0) { 1002 return INVALID_POSITION; 1003 } 1004 1005 long idToMatch = mSyncRowId; 1006 int seed = mSyncPosition; 1007 1008 // If there isn't a selection don't hunt for it 1009 if (idToMatch == INVALID_ROW_ID) { 1010 return INVALID_POSITION; 1011 } 1012 1013 // Pin seed to reasonable values 1014 seed = Math.max(0, seed); 1015 seed = Math.min(count - 1, seed); 1016 1017 long endTime = SystemClock.uptimeMillis() + SYNC_MAX_DURATION_MILLIS; 1018 1019 long rowId; 1020 1021 // first position scanned so far 1022 int first = seed; 1023 1024 // last position scanned so far 1025 int last = seed; 1026 1027 // True if we should move down on the next iteration 1028 boolean next = false; 1029 1030 // True when we have looked at the first item in the data 1031 boolean hitFirst; 1032 1033 // True when we have looked at the last item in the data 1034 boolean hitLast; 1035 1036 // Get the item ID locally (instead of getItemIdAtPosition), so 1037 // we need the adapter 1038 T adapter = getAdapter(); 1039 if (adapter == null) { 1040 return INVALID_POSITION; 1041 } 1042 1043 while (SystemClock.uptimeMillis() <= endTime) { 1044 rowId = adapter.getItemId(seed); 1045 if (rowId == idToMatch) { 1046 // Found it! 1047 return seed; 1048 } 1049 1050 hitLast = last == count - 1; 1051 hitFirst = first == 0; 1052 1053 if (hitLast && hitFirst) { 1054 // Looked at everything 1055 break; 1056 } 1057 1058 if (hitFirst || (next && !hitLast)) { 1059 // Either we hit the top, or we are trying to move down 1060 last++; 1061 seed = last; 1062 // Try going up next time 1063 next = false; 1064 } else if (hitLast || (!next && !hitFirst)) { 1065 // Either we hit the bottom, or we are trying to move up 1066 first--; 1067 seed = first; 1068 // Try going down next time 1069 next = true; 1070 } 1071 1072 } 1073 1074 return INVALID_POSITION; 1075 } 1076 1077 /** 1078 * Find a position that can be selected (i.e., is not a separator). 1079 * 1080 * @param position The starting position to look at. 1081 * @param lookDown Whether to look down for other positions. 1082 * @return The next selectable position starting at position and then searching either up or 1083 * down. Returns {@link #INVALID_POSITION} if nothing can be found. 1084 */ 1085 int lookForSelectablePosition(int position, boolean lookDown) { 1086 return position; 1087 } 1088 1089 /** 1090 * Utility to keep mSelectedPosition and mSelectedRowId in sync 1091 * @param position Our current position 1092 */ 1093 void setSelectedPositionInt(int position) { 1094 mSelectedPosition = position; 1095 mSelectedRowId = getItemIdAtPosition(position); 1096 } 1097 1098 /** 1099 * Utility to keep mNextSelectedPosition and mNextSelectedRowId in sync 1100 * @param position Intended value for mSelectedPosition the next time we go 1101 * through layout 1102 */ 1103 void setNextSelectedPositionInt(int position) { 1104 mNextSelectedPosition = position; 1105 mNextSelectedRowId = getItemIdAtPosition(position); 1106 // If we are trying to sync to the selection, update that too 1107 if (mNeedSync && mSyncMode == SYNC_SELECTED_POSITION && position >= 0) { 1108 mSyncPosition = position; 1109 mSyncRowId = mNextSelectedRowId; 1110 } 1111 } 1112 1113 /** 1114 * Remember enough information to restore the screen state when the data has 1115 * changed. 1116 * 1117 */ 1118 void rememberSyncState() { 1119 if (getChildCount() > 0) { 1120 mNeedSync = true; 1121 mSyncHeight = mLayoutHeight; 1122 if (mSelectedPosition >= 0) { 1123 // Sync the selection state 1124 View v = getChildAt(mSelectedPosition - mFirstPosition); 1125 mSyncRowId = mNextSelectedRowId; 1126 mSyncPosition = mNextSelectedPosition; 1127 if (v != null) { 1128 mSpecificTop = v.getTop(); 1129 } 1130 mSyncMode = SYNC_SELECTED_POSITION; 1131 } else { 1132 // Sync the based on the offset of the first view 1133 View v = getChildAt(0); 1134 T adapter = getAdapter(); 1135 if (mFirstPosition >= 0 && mFirstPosition < adapter.getCount()) { 1136 mSyncRowId = adapter.getItemId(mFirstPosition); 1137 } else { 1138 mSyncRowId = NO_ID; 1139 } 1140 mSyncPosition = mFirstPosition; 1141 if (v != null) { 1142 mSpecificTop = v.getTop(); 1143 } 1144 mSyncMode = SYNC_FIRST_POSITION; 1145 } 1146 } 1147 } 1148}