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