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