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