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