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