blob: a5fad60c439aeffd19b4ac10dfa7e7cd0601770a [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;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080034
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080035/**
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.
Joe Fernandez61fd1e82011-10-26 13:39:11 -070041 *
42 * <div class="special reference">
43 * <h3>Developer Guides</h3>
44 * <p>For more information about using AdapterView, read the
45 * <a href="{@docRoot}guide/topics/ui/binding.html">Binding to Data with AdapterView</a>
46 * developer guide.</p></div>
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080047 */
48public abstract class AdapterView<T extends Adapter> extends ViewGroup {
49
50 /**
51 * The item view type returned by {@link Adapter#getItemViewType(int)} when
52 * the adapter does not want the item's view recycled.
53 */
54 public static final int ITEM_VIEW_TYPE_IGNORE = -1;
55
56 /**
57 * The item view type returned by {@link Adapter#getItemViewType(int)} when
58 * the item is a header or footer.
59 */
60 public static final int ITEM_VIEW_TYPE_HEADER_OR_FOOTER = -2;
61
62 /**
63 * The position of the first child displayed
64 */
Konstantin Lopyrevbea95162010-08-10 17:02:18 -070065 @ViewDebug.ExportedProperty(category = "scrolling")
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080066 int mFirstPosition = 0;
67
68 /**
69 * The offset in pixels from the top of the AdapterView to the top
70 * of the view to select during the next layout.
71 */
72 int mSpecificTop;
73
74 /**
75 * Position from which to start looking for mSyncRowId
76 */
77 int mSyncPosition;
78
79 /**
80 * Row id to look for when data has changed
81 */
82 long mSyncRowId = INVALID_ROW_ID;
83
84 /**
85 * Height of the view when mSyncPosition and mSyncRowId where set
86 */
87 long mSyncHeight;
88
89 /**
90 * True if we need to sync to mSyncRowId
91 */
92 boolean mNeedSync = false;
93
94 /**
95 * Indicates whether to sync based on the selection or position. Possible
96 * values are {@link #SYNC_SELECTED_POSITION} or
97 * {@link #SYNC_FIRST_POSITION}.
98 */
99 int mSyncMode;
100
101 /**
102 * Our height after the last layout
103 */
104 private int mLayoutHeight;
105
106 /**
107 * Sync based on the selected child
108 */
109 static final int SYNC_SELECTED_POSITION = 0;
110
111 /**
112 * Sync based on the first child displayed
113 */
114 static final int SYNC_FIRST_POSITION = 1;
115
116 /**
117 * Maximum amount of time to spend in {@link #findSyncPosition()}
118 */
119 static final int SYNC_MAX_DURATION_MILLIS = 100;
120
121 /**
122 * Indicates that this view is currently being laid out.
123 */
124 boolean mInLayout = false;
125
126 /**
127 * The listener that receives notifications when an item is selected.
128 */
129 OnItemSelectedListener mOnItemSelectedListener;
130
131 /**
132 * The listener that receives notifications when an item is clicked.
133 */
134 OnItemClickListener mOnItemClickListener;
135
136 /**
137 * The listener that receives notifications when an item is long clicked.
138 */
139 OnItemLongClickListener mOnItemLongClickListener;
140
141 /**
142 * True if the data has changed since the last layout
143 */
144 boolean mDataChanged;
145
146 /**
147 * The position within the adapter's data set of the item to select
148 * during the next layout.
149 */
Konstantin Lopyrevbea95162010-08-10 17:02:18 -0700150 @ViewDebug.ExportedProperty(category = "list")
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800151 int mNextSelectedPosition = INVALID_POSITION;
152
153 /**
154 * The item id of the item to select during the next layout.
155 */
156 long mNextSelectedRowId = INVALID_ROW_ID;
157
158 /**
159 * The position within the adapter's data set of the currently selected item.
160 */
Konstantin Lopyrevbea95162010-08-10 17:02:18 -0700161 @ViewDebug.ExportedProperty(category = "list")
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800162 int mSelectedPosition = INVALID_POSITION;
163
164 /**
165 * The item id of the currently selected item.
166 */
167 long mSelectedRowId = INVALID_ROW_ID;
168
169 /**
170 * View to show if there are no items to show.
171 */
Mihai Predab6af5332009-04-28 14:21:57 +0200172 private View mEmptyView;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800173
174 /**
175 * The number of items in the current adapter.
176 */
Konstantin Lopyrevbea95162010-08-10 17:02:18 -0700177 @ViewDebug.ExportedProperty(category = "list")
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800178 int mItemCount;
179
180 /**
Chet Haase5e25c2c2010-09-16 11:15:56 -0700181 * The number of items in the adapter before a data changed event occurred.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800182 */
183 int mOldItemCount;
184
185 /**
186 * Represents an invalid position. All valid positions are in the range 0 to 1 less than the
187 * number of items in the current adapter.
188 */
189 public static final int INVALID_POSITION = -1;
190
191 /**
192 * Represents an empty or invalid row id
193 */
194 public static final long INVALID_ROW_ID = Long.MIN_VALUE;
195
196 /**
197 * The last selected position we used when notifying
198 */
199 int mOldSelectedPosition = INVALID_POSITION;
200
201 /**
202 * The id of the last selected position we used when notifying
203 */
204 long mOldSelectedRowId = INVALID_ROW_ID;
205
206 /**
207 * Indicates what focusable state is requested when calling setFocusable().
208 * In addition to this, this view has other criteria for actually
209 * determining the focusable state (such as whether its empty or the text
210 * filter is shown).
211 *
212 * @see #setFocusable(boolean)
213 * @see #checkFocus()
214 */
215 private boolean mDesiredFocusableState;
216 private boolean mDesiredFocusableInTouchModeState;
217
218 private SelectionNotifier mSelectionNotifier;
219 /**
220 * When set to true, calls to requestLayout() will not propagate up the parent hierarchy.
221 * This is used to layout the children during a layout pass.
222 */
223 boolean mBlockLayoutRequests = false;
224
225 public AdapterView(Context context) {
226 super(context);
227 }
228
229 public AdapterView(Context context, AttributeSet attrs) {
230 super(context, attrs);
231 }
232
233 public AdapterView(Context context, AttributeSet attrs, int defStyle) {
234 super(context, attrs, defStyle);
Svetoslav Ganov42138042012-03-20 11:51:39 -0700235
236 // If not explicitly specified this view is important for accessibility.
237 if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
238 setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
239 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800240 }
241
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800242 /**
243 * Interface definition for a callback to be invoked when an item in this
244 * AdapterView has been clicked.
245 */
246 public interface OnItemClickListener {
247
248 /**
249 * Callback method to be invoked when an item in this AdapterView has
250 * been clicked.
251 * <p>
252 * Implementers can call getItemAtPosition(position) if they need
253 * to access the data associated with the selected item.
254 *
255 * @param parent The AdapterView where the click happened.
256 * @param view The view within the AdapterView that was clicked (this
257 * will be a view provided by the adapter)
258 * @param position The position of the view in the adapter.
259 * @param id The row id of the item that was clicked.
260 */
261 void onItemClick(AdapterView<?> parent, View view, int position, long id);
262 }
263
264 /**
265 * Register a callback to be invoked when an item in this AdapterView has
266 * been clicked.
267 *
268 * @param listener The callback that will be invoked.
269 */
270 public void setOnItemClickListener(OnItemClickListener listener) {
271 mOnItemClickListener = listener;
272 }
273
274 /**
275 * @return The callback to be invoked with an item in this AdapterView has
276 * been clicked, or null id no callback has been set.
277 */
278 public final OnItemClickListener getOnItemClickListener() {
279 return mOnItemClickListener;
280 }
281
282 /**
Alan Viverettefed60952013-09-17 13:00:49 -0700283 * Call the OnItemClickListener, if it is defined. Performs all normal
284 * actions associated with clicking: reporting accessibility event, playing
285 * a sound, etc.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800286 *
287 * @param view The view within the AdapterView that was clicked.
288 * @param position The position of the view in the adapter.
289 * @param id The row id of the item that was clicked.
290 * @return True if there was an assigned OnItemClickListener that was
291 * called, false otherwise is returned.
292 */
293 public boolean performItemClick(View view, int position, long id) {
294 if (mOnItemClickListener != null) {
295 playSoundEffect(SoundEffectConstants.CLICK);
Adam Powellcbff4172011-09-18 17:48:35 -0700296 if (view != null) {
297 view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
298 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800299 mOnItemClickListener.onItemClick(this, view, position, id);
300 return true;
301 }
302
303 return false;
304 }
305
306 /**
307 * Interface definition for a callback to be invoked when an item in this
308 * view has been clicked and held.
309 */
310 public interface OnItemLongClickListener {
311 /**
312 * Callback method to be invoked when an item in this view has been
313 * clicked and held.
314 *
315 * Implementers can call getItemAtPosition(position) if they need to access
316 * the data associated with the selected item.
317 *
318 * @param parent The AbsListView where the click happened
319 * @param view The view within the AbsListView that was clicked
320 * @param position The position of the view in the list
321 * @param id The row id of the item that was clicked
322 *
323 * @return true if the callback consumed the long click, false otherwise
324 */
325 boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id);
326 }
327
328
329 /**
330 * Register a callback to be invoked when an item in this AdapterView has
331 * been clicked and held
332 *
333 * @param listener The callback that will run
334 */
335 public void setOnItemLongClickListener(OnItemLongClickListener listener) {
336 if (!isLongClickable()) {
337 setLongClickable(true);
338 }
339 mOnItemLongClickListener = listener;
340 }
341
342 /**
343 * @return The callback to be invoked with an item in this AdapterView has
344 * been clicked and held, or null id no callback as been set.
345 */
346 public final OnItemLongClickListener getOnItemLongClickListener() {
347 return mOnItemLongClickListener;
348 }
349
350 /**
351 * Interface definition for a callback to be invoked when
352 * an item in this view has been selected.
353 */
354 public interface OnItemSelectedListener {
355 /**
Romain Guyf18c8dd2011-09-16 16:16:36 -0700356 * <p>Callback method to be invoked when an item in this view has been
357 * selected. This callback is invoked only when the newly selected
358 * position is different from the previously selected position or if
359 * there was no selected item.</p>
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800360 *
361 * Impelmenters can call getItemAtPosition(position) if they need to access the
362 * data associated with the selected item.
363 *
364 * @param parent The AdapterView where the selection happened
365 * @param view The view within the AdapterView that was clicked
366 * @param position The position of the view in the adapter
367 * @param id The row id of the item that is selected
368 */
369 void onItemSelected(AdapterView<?> parent, View view, int position, long id);
370
371 /**
372 * Callback method to be invoked when the selection disappears from this
373 * view. The selection can disappear for instance when touch is activated
374 * or when the adapter becomes empty.
375 *
376 * @param parent The AdapterView that now contains no selected item.
377 */
378 void onNothingSelected(AdapterView<?> parent);
379 }
380
381
382 /**
383 * Register a callback to be invoked when an item in this AdapterView has
384 * been selected.
385 *
386 * @param listener The callback that will run
387 */
388 public void setOnItemSelectedListener(OnItemSelectedListener listener) {
389 mOnItemSelectedListener = listener;
390 }
391
392 public final OnItemSelectedListener getOnItemSelectedListener() {
393 return mOnItemSelectedListener;
394 }
395
396 /**
397 * Extra menu information provided to the
398 * {@link android.view.View.OnCreateContextMenuListener#onCreateContextMenu(ContextMenu, View, ContextMenuInfo) }
399 * callback when a context menu is brought up for this AdapterView.
400 *
401 */
402 public static class AdapterContextMenuInfo implements ContextMenu.ContextMenuInfo {
403
404 public AdapterContextMenuInfo(View targetView, int position, long id) {
405 this.targetView = targetView;
406 this.position = position;
407 this.id = id;
408 }
409
410 /**
411 * The child view for which the context menu is being displayed. This
412 * will be one of the children of this AdapterView.
413 */
414 public View targetView;
415
416 /**
417 * The position in the adapter for which the context menu is being
418 * displayed.
419 */
420 public int position;
421
422 /**
423 * The row id of the item for which the context menu is being displayed.
424 */
425 public long id;
426 }
427
428 /**
429 * Returns the adapter currently associated with this widget.
430 *
431 * @return The adapter used to provide this view's content.
432 */
433 public abstract T getAdapter();
434
435 /**
436 * Sets the adapter that provides the data and the views to represent the data
437 * in this widget.
438 *
439 * @param adapter The adapter to use to create this view's content.
440 */
441 public abstract void setAdapter(T adapter);
442
443 /**
444 * This method is not supported and throws an UnsupportedOperationException when called.
445 *
446 * @param child Ignored.
447 *
448 * @throws UnsupportedOperationException Every time this method is invoked.
449 */
450 @Override
451 public void addView(View child) {
452 throw new UnsupportedOperationException("addView(View) 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 *
461 * @throws UnsupportedOperationException Every time this method is invoked.
462 */
463 @Override
464 public void addView(View child, int index) {
465 throw new UnsupportedOperationException("addView(View, int) is not supported in AdapterView");
466 }
467
468 /**
469 * This method is not supported and throws an UnsupportedOperationException when called.
470 *
471 * @param child Ignored.
472 * @param params Ignored.
473 *
474 * @throws UnsupportedOperationException Every time this method is invoked.
475 */
476 @Override
477 public void addView(View child, LayoutParams params) {
478 throw new UnsupportedOperationException("addView(View, LayoutParams) "
479 + "is not supported in AdapterView");
480 }
481
482 /**
483 * This method is not supported and throws an UnsupportedOperationException when called.
484 *
485 * @param child Ignored.
486 * @param index Ignored.
487 * @param params Ignored.
488 *
489 * @throws UnsupportedOperationException Every time this method is invoked.
490 */
491 @Override
492 public void addView(View child, int index, LayoutParams params) {
493 throw new UnsupportedOperationException("addView(View, int, LayoutParams) "
494 + "is not supported in AdapterView");
495 }
496
497 /**
498 * This method is not supported and throws an UnsupportedOperationException when called.
499 *
500 * @param child Ignored.
501 *
502 * @throws UnsupportedOperationException Every time this method is invoked.
503 */
504 @Override
505 public void removeView(View child) {
506 throw new UnsupportedOperationException("removeView(View) is not supported in AdapterView");
507 }
508
509 /**
510 * This method is not supported and throws an UnsupportedOperationException when called.
511 *
512 * @param index Ignored.
513 *
514 * @throws UnsupportedOperationException Every time this method is invoked.
515 */
516 @Override
517 public void removeViewAt(int index) {
518 throw new UnsupportedOperationException("removeViewAt(int) is not supported in AdapterView");
519 }
520
521 /**
522 * This method is not supported and throws an UnsupportedOperationException when called.
523 *
524 * @throws UnsupportedOperationException Every time this method is invoked.
525 */
526 @Override
527 public void removeAllViews() {
528 throw new UnsupportedOperationException("removeAllViews() is not supported in AdapterView");
529 }
530
531 @Override
532 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
533 mLayoutHeight = getHeight();
534 }
535
536 /**
537 * Return the position of the currently selected item within the adapter's data set
538 *
539 * @return int Position (starting at 0), or {@link #INVALID_POSITION} if there is nothing selected.
540 */
541 @ViewDebug.CapturedViewProperty
542 public int getSelectedItemPosition() {
543 return mNextSelectedPosition;
544 }
545
546 /**
547 * @return The id corresponding to the currently selected item, or {@link #INVALID_ROW_ID}
548 * if nothing is selected.
549 */
550 @ViewDebug.CapturedViewProperty
551 public long getSelectedItemId() {
552 return mNextSelectedRowId;
553 }
554
555 /**
556 * @return The view corresponding to the currently selected item, or null
557 * if nothing is selected
558 */
559 public abstract View getSelectedView();
560
561 /**
562 * @return The data corresponding to the currently selected item, or
563 * null if there is nothing selected.
564 */
565 public Object getSelectedItem() {
566 T adapter = getAdapter();
567 int selection = getSelectedItemPosition();
568 if (adapter != null && adapter.getCount() > 0 && selection >= 0) {
569 return adapter.getItem(selection);
570 } else {
571 return null;
572 }
573 }
574
575 /**
576 * @return The number of items owned by the Adapter associated with this
577 * AdapterView. (This is the number of data items, which may be
Chet Haase5e25c2c2010-09-16 11:15:56 -0700578 * larger than the number of visible views.)
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800579 */
580 @ViewDebug.CapturedViewProperty
581 public int getCount() {
582 return mItemCount;
583 }
584
585 /**
586 * Get the position within the adapter's data set for the view, where view is a an adapter item
587 * or a descendant of an adapter item.
588 *
589 * @param view an adapter item, or a descendant of an adapter item. This must be visible in this
590 * AdapterView at the time of the call.
591 * @return the position within the adapter's data set of the view, or {@link #INVALID_POSITION}
592 * if the view does not correspond to a list item (or it is not currently visible).
593 */
594 public int getPositionForView(View view) {
595 View listItem = view;
596 try {
597 View v;
598 while (!(v = (View) listItem.getParent()).equals(this)) {
599 listItem = v;
600 }
601 } catch (ClassCastException e) {
602 // We made it up to the window without find this list view
603 return INVALID_POSITION;
604 }
605
606 // Search the children for the list item
607 final int childCount = getChildCount();
608 for (int i = 0; i < childCount; i++) {
609 if (getChildAt(i).equals(listItem)) {
610 return mFirstPosition + i;
611 }
612 }
613
614 // Child not found!
615 return INVALID_POSITION;
616 }
617
618 /**
619 * Returns the position within the adapter's data set for the first item
620 * displayed on screen.
621 *
622 * @return The position within the adapter's data set
623 */
624 public int getFirstVisiblePosition() {
625 return mFirstPosition;
626 }
627
628 /**
629 * Returns the position within the adapter's data set for the last item
630 * displayed on screen.
631 *
632 * @return The position within the adapter's data set
633 */
634 public int getLastVisiblePosition() {
635 return mFirstPosition + getChildCount() - 1;
636 }
637
638 /**
svetoslavganov75986cf2009-05-14 22:28:01 -0700639 * Sets the currently selected item. To support accessibility subclasses that
640 * override this method must invoke the overriden super method first.
641 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800642 * @param position Index (starting at 0) of the data item to be selected.
643 */
644 public abstract void setSelection(int position);
645
646 /**
647 * Sets the view to show if the adapter is empty
648 */
Adam Cohen1480fdd2010-08-25 17:24:53 -0700649 @android.view.RemotableViewMethod
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800650 public void setEmptyView(View emptyView) {
651 mEmptyView = emptyView;
652
Svetoslav Ganov42138042012-03-20 11:51:39 -0700653 // If not explicitly specified this view is important for accessibility.
Mindy Pereiraf9373132012-04-16 08:58:53 -0700654 if (emptyView != null
655 && emptyView.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
Svetoslav Ganov42138042012-03-20 11:51:39 -0700656 emptyView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
657 }
658
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800659 final T adapter = getAdapter();
660 final boolean empty = ((adapter == null) || adapter.isEmpty());
661 updateEmptyStatus(empty);
662 }
663
664 /**
665 * When the current adapter is empty, the AdapterView can display a special view
666 * call the empty view. The empty view is used to provide feedback to the user
667 * that no data is available in this AdapterView.
668 *
669 * @return The view to show if the adapter is empty.
670 */
671 public View getEmptyView() {
672 return mEmptyView;
673 }
674
675 /**
676 * Indicates whether this view is in filter mode. Filter mode can for instance
677 * be enabled by a user when typing on the keyboard.
678 *
679 * @return True if the view is in filter mode, false otherwise.
680 */
681 boolean isInFilterMode() {
682 return false;
683 }
684
685 @Override
686 public void setFocusable(boolean focusable) {
687 final T adapter = getAdapter();
688 final boolean empty = adapter == null || adapter.getCount() == 0;
689
690 mDesiredFocusableState = focusable;
691 if (!focusable) {
692 mDesiredFocusableInTouchModeState = false;
693 }
694
695 super.setFocusable(focusable && (!empty || isInFilterMode()));
696 }
697
698 @Override
699 public void setFocusableInTouchMode(boolean focusable) {
700 final T adapter = getAdapter();
701 final boolean empty = adapter == null || adapter.getCount() == 0;
702
703 mDesiredFocusableInTouchModeState = focusable;
704 if (focusable) {
705 mDesiredFocusableState = true;
706 }
707
708 super.setFocusableInTouchMode(focusable && (!empty || isInFilterMode()));
709 }
710
711 void checkFocus() {
712 final T adapter = getAdapter();
713 final boolean empty = adapter == null || adapter.getCount() == 0;
714 final boolean focusable = !empty || isInFilterMode();
715 // The order in which we set focusable in touch mode/focusable may matter
716 // for the client, see View.setFocusableInTouchMode() comments for more
717 // details
718 super.setFocusableInTouchMode(focusable && mDesiredFocusableInTouchModeState);
719 super.setFocusable(focusable && mDesiredFocusableState);
720 if (mEmptyView != null) {
721 updateEmptyStatus((adapter == null) || adapter.isEmpty());
722 }
723 }
724
725 /**
726 * Update the status of the list based on the empty parameter. If empty is true and
727 * we have an empty view, display it. In all the other cases, make sure that the listview
728 * is VISIBLE and that the empty view is GONE (if it's not null).
729 */
730 private void updateEmptyStatus(boolean empty) {
731 if (isInFilterMode()) {
732 empty = false;
733 }
734
735 if (empty) {
736 if (mEmptyView != null) {
737 mEmptyView.setVisibility(View.VISIBLE);
738 setVisibility(View.GONE);
739 } else {
740 // If the caller just removed our empty view, make sure the list view is visible
741 setVisibility(View.VISIBLE);
742 }
743
744 // We are now GONE, so pending layouts will not be dispatched.
745 // Force one here to make sure that the state of the list matches
746 // the state of the adapter.
747 if (mDataChanged) {
748 this.onLayout(false, mLeft, mTop, mRight, mBottom);
749 }
750 } else {
751 if (mEmptyView != null) mEmptyView.setVisibility(View.GONE);
752 setVisibility(View.VISIBLE);
753 }
754 }
755
756 /**
757 * Gets the data associated with the specified position in the list.
758 *
759 * @param position Which data to get
760 * @return The data associated with the specified position in the list
761 */
762 public Object getItemAtPosition(int position) {
763 T adapter = getAdapter();
764 return (adapter == null || position < 0) ? null : adapter.getItem(position);
765 }
766
767 public long getItemIdAtPosition(int position) {
768 T adapter = getAdapter();
769 return (adapter == null || position < 0) ? INVALID_ROW_ID : adapter.getItemId(position);
770 }
771
772 @Override
773 public void setOnClickListener(OnClickListener l) {
774 throw new RuntimeException("Don't call setOnClickListener for an AdapterView. "
775 + "You probably want setOnItemClickListener instead");
776 }
777
778 /**
779 * Override to prevent freezing of any views created by the adapter.
780 */
781 @Override
782 protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
783 dispatchFreezeSelfOnly(container);
784 }
785
786 /**
787 * Override to prevent thawing of any views created by the adapter.
788 */
789 @Override
790 protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
791 dispatchThawSelfOnly(container);
792 }
793
794 class AdapterDataSetObserver extends DataSetObserver {
795
796 private Parcelable mInstanceState = null;
797
798 @Override
799 public void onChanged() {
800 mDataChanged = true;
801 mOldItemCount = mItemCount;
802 mItemCount = getAdapter().getCount();
803
804 // Detect the case where a cursor that was previously invalidated has
805 // been repopulated with new data.
806 if (AdapterView.this.getAdapter().hasStableIds() && mInstanceState != null
807 && mOldItemCount == 0 && mItemCount > 0) {
808 AdapterView.this.onRestoreInstanceState(mInstanceState);
809 mInstanceState = null;
810 } else {
811 rememberSyncState();
812 }
813 checkFocus();
814 requestLayout();
815 }
816
817 @Override
818 public void onInvalidated() {
819 mDataChanged = true;
820
821 if (AdapterView.this.getAdapter().hasStableIds()) {
822 // Remember the current state for the case where our hosting activity is being
823 // stopped and later restarted
824 mInstanceState = AdapterView.this.onSaveInstanceState();
825 }
826
827 // Data is invalid so we should reset our state
828 mOldItemCount = mItemCount;
829 mItemCount = 0;
830 mSelectedPosition = INVALID_POSITION;
831 mSelectedRowId = INVALID_ROW_ID;
832 mNextSelectedPosition = INVALID_POSITION;
833 mNextSelectedRowId = INVALID_ROW_ID;
834 mNeedSync = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800835
836 checkFocus();
837 requestLayout();
838 }
839
840 public void clearSavedState() {
841 mInstanceState = null;
842 }
843 }
844
Adam Powell9c17a4c12010-08-30 18:04:15 -0700845 @Override
846 protected void onDetachedFromWindow() {
847 super.onDetachedFromWindow();
848 removeCallbacks(mSelectionNotifier);
849 }
850
851 private class SelectionNotifier implements Runnable {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800852 public void run() {
853 if (mDataChanged) {
854 // Data has changed between when this SelectionNotifier
855 // was posted and now. We need to wait until the AdapterView
856 // has been synched to the new data.
Adam Powell9c17a4c12010-08-30 18:04:15 -0700857 if (getAdapter() != null) {
858 post(this);
859 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800860 } else {
861 fireOnSelected();
Svetoslav Ganov42138042012-03-20 11:51:39 -0700862 performAccessibilityActionsOnSelected();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800863 }
864 }
865 }
866
867 void selectionChanged() {
Svetoslav Ganov42138042012-03-20 11:51:39 -0700868 if (mOnItemSelectedListener != null
869 || AccessibilityManager.getInstance(mContext).isEnabled()) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800870 if (mInLayout || mBlockLayoutRequests) {
871 // If we are in a layout traversal, defer notification
872 // by posting. This ensures that the view tree is
873 // in a consistent state and is able to accomodate
874 // new layout or invalidate requests.
875 if (mSelectionNotifier == null) {
876 mSelectionNotifier = new SelectionNotifier();
877 }
Adam Powell9c17a4c12010-08-30 18:04:15 -0700878 post(mSelectionNotifier);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800879 } else {
880 fireOnSelected();
Svetoslav Ganov42138042012-03-20 11:51:39 -0700881 performAccessibilityActionsOnSelected();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800882 }
883 }
884 }
885
886 private void fireOnSelected() {
Svetoslav Ganov42138042012-03-20 11:51:39 -0700887 if (mOnItemSelectedListener == null) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800888 return;
Svetoslav Ganov42138042012-03-20 11:51:39 -0700889 }
890 final int selection = getSelectedItemPosition();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800891 if (selection >= 0) {
892 View v = getSelectedView();
893 mOnItemSelectedListener.onItemSelected(this, v, selection,
894 getAdapter().getItemId(selection));
895 } else {
896 mOnItemSelectedListener.onNothingSelected(this);
897 }
898 }
899
Svetoslav Ganov42138042012-03-20 11:51:39 -0700900 private void performAccessibilityActionsOnSelected() {
901 if (!AccessibilityManager.getInstance(mContext).isEnabled()) {
902 return;
903 }
904 final int position = getSelectedItemPosition();
905 if (position >= 0) {
906 // we fire selection events here not in View
907 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
908 }
909 }
910
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800911 @Override
svetoslavganov75986cf2009-05-14 22:28:01 -0700912 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
Svetoslav Ganov0b0a41d2011-09-07 18:06:03 -0700913 View selectedView = getSelectedView();
Svetoslav Ganovd9ee72f2011-10-05 22:26:05 -0700914 if (selectedView != null && selectedView.getVisibility() == VISIBLE
915 && selectedView.dispatchPopulateAccessibilityEvent(event)) {
916 return true;
Svetoslav Ganov0b0a41d2011-09-07 18:06:03 -0700917 }
Svetoslav Ganovbad9d972011-05-27 16:37:55 -0700918 return false;
919 }
920
921 @Override
Svetoslav Ganovbad9d972011-05-27 16:37:55 -0700922 public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) {
Svetoslav Ganov6179ea32011-06-28 01:12:41 -0700923 if (super.onRequestSendAccessibilityEvent(child, event)) {
924 // Add a record for ourselves as well.
925 AccessibilityEvent record = AccessibilityEvent.obtain();
926 onInitializeAccessibilityEvent(record);
927 // Populate with the text of the requesting child.
928 child.dispatchPopulateAccessibilityEvent(record);
929 event.appendRecord(record);
930 return true;
931 }
932 return false;
Svetoslav Ganov736c2752011-04-22 18:30:36 -0700933 }
Adam Powell3fb3d7c2011-04-22 17:08:55 -0700934
Svetoslav Ganov736c2752011-04-22 18:30:36 -0700935 @Override
Svetoslav Ganovd9ee72f2011-10-05 22:26:05 -0700936 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
937 super.onInitializeAccessibilityNodeInfo(info);
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -0800938 info.setClassName(AdapterView.class.getName());
Svetoslav Ganovd9ee72f2011-10-05 22:26:05 -0700939 info.setScrollable(isScrollableForAccessibility());
940 View selectedView = getSelectedView();
941 if (selectedView != null) {
942 info.setEnabled(selectedView.isEnabled());
943 }
944 }
945
946 @Override
Svetoslav Ganov30401322011-05-12 18:53:45 -0700947 public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
948 super.onInitializeAccessibilityEvent(event);
Svetoslav Ganov8a78fd42012-01-17 14:36:46 -0800949 event.setClassName(AdapterView.class.getName());
Svetoslav Ganovd9ee72f2011-10-05 22:26:05 -0700950 event.setScrollable(isScrollableForAccessibility());
Svetoslav Ganov736c2752011-04-22 18:30:36 -0700951 View selectedView = getSelectedView();
952 if (selectedView != null) {
953 event.setEnabled(selectedView.isEnabled());
954 }
Svetoslav Ganoveb0c52e2011-10-10 18:15:41 -0700955 event.setCurrentItemIndex(getSelectedItemPosition());
Svetoslav Ganovd9ee72f2011-10-05 22:26:05 -0700956 event.setFromIndex(getFirstVisiblePosition());
957 event.setToIndex(getLastVisiblePosition());
Svetoslav Ganov0e6fa8b2011-10-19 12:38:04 -0700958 event.setItemCount(getCount());
Svetoslav Ganovd9ee72f2011-10-05 22:26:05 -0700959 }
960
961 private boolean isScrollableForAccessibility() {
Svetoslav Ganov98348512011-10-10 17:18:19 -0700962 T adapter = getAdapter();
963 if (adapter != null) {
964 final int itemCount = adapter.getCount();
965 return itemCount > 0
966 && (getFirstVisiblePosition() > 0 || getLastVisiblePosition() < itemCount - 1);
967 }
968 return false;
svetoslavganov75986cf2009-05-14 22:28:01 -0700969 }
970
971 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800972 protected boolean canAnimate() {
973 return super.canAnimate() && mItemCount > 0;
974 }
975
976 void handleDataChanged() {
977 final int count = mItemCount;
978 boolean found = false;
979
980 if (count > 0) {
981
982 int newPos;
983
984 // Find the row we are supposed to sync to
985 if (mNeedSync) {
986 // Update this first, since setNextSelectedPositionInt inspects
987 // it
988 mNeedSync = false;
989
990 // See if we can find a position in the new data with the same
991 // id as the old selection
992 newPos = findSyncPosition();
993 if (newPos >= 0) {
994 // Verify that new selection is selectable
995 int selectablePos = lookForSelectablePosition(newPos, true);
996 if (selectablePos == newPos) {
997 // Same row id is selected
998 setNextSelectedPositionInt(newPos);
999 found = true;
1000 }
1001 }
1002 }
1003 if (!found) {
1004 // Try to use the same position if we can't find matching data
1005 newPos = getSelectedItemPosition();
1006
1007 // Pin position to the available range
1008 if (newPos >= count) {
1009 newPos = count - 1;
1010 }
1011 if (newPos < 0) {
1012 newPos = 0;
1013 }
1014
1015 // Make sure we select something selectable -- first look down
1016 int selectablePos = lookForSelectablePosition(newPos, true);
1017 if (selectablePos < 0) {
1018 // Looking down didn't work -- try looking up
1019 selectablePos = lookForSelectablePosition(newPos, false);
1020 }
1021 if (selectablePos >= 0) {
1022 setNextSelectedPositionInt(selectablePos);
1023 checkSelectionChanged();
1024 found = true;
1025 }
1026 }
1027 }
1028 if (!found) {
1029 // Nothing is selected
1030 mSelectedPosition = INVALID_POSITION;
1031 mSelectedRowId = INVALID_ROW_ID;
1032 mNextSelectedPosition = INVALID_POSITION;
1033 mNextSelectedRowId = INVALID_ROW_ID;
1034 mNeedSync = false;
1035 checkSelectionChanged();
1036 }
Svetoslav Ganov42138042012-03-20 11:51:39 -07001037
Svetoslav00dbe812013-06-10 12:51:09 -07001038 notifySubtreeAccessibilityStateChangedIfNeeded();
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}