extends ViewGroup {
24 |
25 | /**
26 | * The item view type returned by {@link Adapter#getItemViewType(int)} when
27 | * the adapter does not want the item's view recycled.
28 | */
29 | public static final int ITEM_VIEW_TYPE_IGNORE = -1;
30 |
31 | /**
32 | * The item view type returned by {@link Adapter#getItemViewType(int)} when
33 | * the item is a header or footer.
34 | */
35 | public static final int ITEM_VIEW_TYPE_HEADER_OR_FOOTER = -2;
36 |
37 | /**
38 | * The position of the first child displayed
39 | */
40 | @ViewDebug.ExportedProperty(category = "scrolling")
41 | int mFirstPosition = 0;
42 |
43 | /**
44 | * The offset in pixels from the top of the AdapterView to the top
45 | * of the view to select during the next layout.
46 | */
47 | int mSpecificTop;
48 |
49 | /**
50 | * Position from which to start looking for mSyncRowId
51 | */
52 | int mSyncPosition;
53 |
54 | /**
55 | * Row id to look for when data has changed
56 | */
57 | long mSyncRowId = INVALID_ROW_ID;
58 |
59 | /**
60 | * Height of the view when mSyncPosition and mSyncRowId where set
61 | */
62 | long mSyncHeight;
63 |
64 | /**
65 | * True if we need to sync to mSyncRowId
66 | */
67 | boolean mNeedSync = false;
68 |
69 | /**
70 | * Indicates whether to sync based on the selection or position. Possible
71 | * values are {@link #SYNC_SELECTED_POSITION} or
72 | * {@link #SYNC_FIRST_POSITION}.
73 | */
74 | int mSyncMode;
75 |
76 | /**
77 | * Our height after the last layout
78 | */
79 | private int mLayoutHeight;
80 |
81 | /**
82 | * Sync based on the selected child
83 | */
84 | static final int SYNC_SELECTED_POSITION = 0;
85 |
86 | /**
87 | * Sync based on the first child displayed
88 | */
89 | static final int SYNC_FIRST_POSITION = 1;
90 |
91 | /**
92 | * Maximum amount of time to spend in {@link #findSyncPosition()}
93 | */
94 | static final int SYNC_MAX_DURATION_MILLIS = 100;
95 |
96 | /**
97 | * Indicates that this view is currently being laid out.
98 | */
99 | boolean mInLayout = false;
100 |
101 | /**
102 | * The listener that receives notifications when an item is selected.
103 | */
104 | OnItemSelectedListener mOnItemSelectedListener;
105 |
106 | /**
107 | * The listener that receives notifications when an item is clicked.
108 | */
109 | OnItemClickListener mOnItemClickListener;
110 |
111 | /**
112 | * The listener that receives notifications when an item is long clicked.
113 | */
114 | OnItemLongClickListener mOnItemLongClickListener;
115 |
116 | /**
117 | * True if the data has changed since the last layout
118 | */
119 | boolean mDataChanged;
120 |
121 | /**
122 | * The position within the adapter's data set of the item to select
123 | * during the next layout.
124 | */
125 | @ViewDebug.ExportedProperty(category = "list")
126 | int mNextSelectedPosition = INVALID_POSITION;
127 |
128 | /**
129 | * The item id of the item to select during the next layout.
130 | */
131 | long mNextSelectedRowId = INVALID_ROW_ID;
132 |
133 | /**
134 | * The position within the adapter's data set of the currently selected item.
135 | */
136 | @ViewDebug.ExportedProperty(category = "list")
137 | int mSelectedPosition = INVALID_POSITION;
138 |
139 | /**
140 | * The item id of the currently selected item.
141 | */
142 | long mSelectedRowId = INVALID_ROW_ID;
143 |
144 | /**
145 | * View to show if there are no items to show.
146 | */
147 | private View mEmptyView;
148 |
149 | /**
150 | * The number of items in the current adapter.
151 | */
152 | @ViewDebug.ExportedProperty(category = "list")
153 | int mItemCount;
154 |
155 | /**
156 | * The number of items in the adapter before a data changed event occurred.
157 | */
158 | int mOldItemCount;
159 |
160 | /**
161 | * Represents an invalid position. All valid positions are in the range 0 to 1 less than the
162 | * number of items in the current adapter.
163 | */
164 | public static final int INVALID_POSITION = -1;
165 |
166 | /**
167 | * Represents an empty or invalid row id
168 | */
169 | public static final long INVALID_ROW_ID = Long.MIN_VALUE;
170 |
171 | /**
172 | * The last selected position we used when notifying
173 | */
174 | int mOldSelectedPosition = INVALID_POSITION;
175 |
176 | /**
177 | * The id of the last selected position we used when notifying
178 | */
179 | long mOldSelectedRowId = INVALID_ROW_ID;
180 |
181 | /**
182 | * Indicates what focusable state is requested when calling setFocusable().
183 | * In addition to this, this view has other criteria for actually
184 | * determining the focusable state (such as whether its empty or the text
185 | * filter is shown).
186 | *
187 | * @see #setFocusable(boolean)
188 | * @see #checkFocus()
189 | */
190 | private boolean mDesiredFocusableState;
191 | private boolean mDesiredFocusableInTouchModeState;
192 |
193 | private SelectionNotifier mSelectionNotifier;
194 | /**
195 | * When set to true, calls to requestLayout() will not propagate up the parent hierarchy.
196 | * This is used to layout the children during a layout pass.
197 | */
198 | boolean mBlockLayoutRequests = false;
199 |
200 | public AdapterView(Context context) {
201 | super(context);
202 | }
203 |
204 | public AdapterView(Context context, AttributeSet attrs) {
205 | super(context, attrs);
206 | }
207 |
208 | public AdapterView(Context context, AttributeSet attrs, int defStyle) {
209 | super(context, attrs, defStyle);
210 | }
211 |
212 | /**
213 | * Interface definition for a callback to be invoked when an item in this
214 | * AdapterView has been clicked.
215 | */
216 | public interface OnItemClickListener {
217 |
218 | /**
219 | * Callback method to be invoked when an item in this AdapterView has
220 | * been clicked.
221 | *
222 | * Implementers can call getItemAtPosition(position) if they need
223 | * to access the data associated with the selected item.
224 | *
225 | * @param parent The AdapterView where the click happened.
226 | * @param view The view within the AdapterView that was clicked (this
227 | * will be a view provided by the adapter)
228 | * @param position The position of the view in the adapter.
229 | * @param id The row id of the item that was clicked.
230 | */
231 | void onItemClick(AdapterView> parent, View view, int position, long id);
232 | }
233 |
234 | /**
235 | * Register a callback to be invoked when an item in this AdapterView has
236 | * been clicked.
237 | *
238 | * @param listener The callback that will be invoked.
239 | */
240 | public void setOnItemClickListener(OnItemClickListener listener) {
241 | mOnItemClickListener = listener;
242 | }
243 |
244 | /**
245 | * @return The callback to be invoked with an item in this AdapterView has
246 | * been clicked, or null id no callback has been set.
247 | */
248 | public final OnItemClickListener getOnItemClickListener() {
249 | return mOnItemClickListener;
250 | }
251 |
252 | /**
253 | * Call the OnItemClickListener, if it is defined.
254 | *
255 | * @param view The view within the AdapterView that was clicked.
256 | * @param position The position of the view in the adapter.
257 | * @param id The row id of the item that was clicked.
258 | * @return True if there was an assigned OnItemClickListener that was
259 | * called, false otherwise is returned.
260 | */
261 | public boolean performItemClick(View view, int position, long id) {
262 | if (mOnItemClickListener != null) {
263 | playSoundEffect(SoundEffectConstants.CLICK);
264 | if (view != null) {
265 | view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
266 | }
267 | mOnItemClickListener.onItemClick(this, view, position, id);
268 | return true;
269 | }
270 |
271 | return false;
272 | }
273 |
274 | /**
275 | * Interface definition for a callback to be invoked when an item in this
276 | * view has been clicked and held.
277 | */
278 | public interface OnItemLongClickListener {
279 | /**
280 | * Callback method to be invoked when an item in this view has been
281 | * clicked and held.
282 | *
283 | * Implementers can call getItemAtPosition(position) if they need to access
284 | * the data associated with the selected item.
285 | *
286 | * @param parent The AbsListView where the click happened
287 | * @param view The view within the AbsListView that was clicked
288 | * @param position The position of the view in the list
289 | * @param id The row id of the item that was clicked
290 | * @return true if the callback consumed the long click, false otherwise
291 | */
292 | boolean onItemLongClick(AdapterView> parent, View view, int position, long id);
293 | }
294 |
295 |
296 | /**
297 | * Register a callback to be invoked when an item in this AdapterView has
298 | * been clicked and held
299 | *
300 | * @param listener The callback that will run
301 | */
302 | public void setOnItemLongClickListener(OnItemLongClickListener listener) {
303 | if (!isLongClickable()) {
304 | setLongClickable(true);
305 | }
306 | mOnItemLongClickListener = listener;
307 | }
308 |
309 | /**
310 | * @return The callback to be invoked with an item in this AdapterView has
311 | * been clicked and held, or null id no callback as been set.
312 | */
313 | public final OnItemLongClickListener getOnItemLongClickListener() {
314 | return mOnItemLongClickListener;
315 | }
316 |
317 | /**
318 | * Interface definition for a callback to be invoked when
319 | * an item in this view has been selected.
320 | */
321 | public interface OnItemSelectedListener {
322 | /**
323 | * Callback method to be invoked when an item in this view has been
324 | * selected. This callback is invoked only when the newly selected
325 | * position is different from the previously selected position or if
326 | * there was no selected item.
327 | *
328 | * Impelmenters can call getItemAtPosition(position) if they need to access the
329 | * data associated with the selected item.
330 | *
331 | * @param parent The AdapterView where the selection happened
332 | * @param view The view within the AdapterView that was clicked
333 | * @param position The position of the view in the adapter
334 | * @param id The row id of the item that is selected
335 | */
336 | void onItemSelected(AdapterView> parent, View view, int position, long id);
337 |
338 | /**
339 | * Callback method to be invoked when the selection disappears from this
340 | * view. The selection can disappear for instance when touch is activated
341 | * or when the adapter becomes empty.
342 | *
343 | * @param parent The AdapterView that now contains no selected item.
344 | */
345 | void onNothingSelected(AdapterView> parent);
346 | }
347 |
348 |
349 | /**
350 | * Register a callback to be invoked when an item in this AdapterView has
351 | * been selected.
352 | *
353 | * @param listener The callback that will run
354 | */
355 | public void setOnItemSelectedListener(OnItemSelectedListener listener) {
356 | mOnItemSelectedListener = listener;
357 | }
358 |
359 | public final OnItemSelectedListener getOnItemSelectedListener() {
360 | return mOnItemSelectedListener;
361 | }
362 |
363 | /**
364 | * Extra menu information provided to the
365 | * {@link android.view.View.OnCreateContextMenuListener#onCreateContextMenu(ContextMenu, View, ContextMenuInfo) }
366 | * callback when a context menu is brought up for this AdapterView.
367 | */
368 | public static class AdapterContextMenuInfo implements ContextMenu.ContextMenuInfo {
369 |
370 | public AdapterContextMenuInfo(View targetView, int position, long id) {
371 | this.targetView = targetView;
372 | this.position = position;
373 | this.id = id;
374 | }
375 |
376 | /**
377 | * The child view for which the context menu is being displayed. This
378 | * will be one of the children of this AdapterView.
379 | */
380 | public View targetView;
381 |
382 | /**
383 | * The position in the adapter for which the context menu is being
384 | * displayed.
385 | */
386 | public int position;
387 |
388 | /**
389 | * The row id of the item for which the context menu is being displayed.
390 | */
391 | public long id;
392 | }
393 |
394 | /**
395 | * Returns the adapter currently associated with this widget.
396 | *
397 | * @return The adapter used to provide this view's content.
398 | */
399 | public abstract T getAdapter();
400 |
401 | /**
402 | * Sets the adapter that provides the data and the views to represent the data
403 | * in this widget.
404 | *
405 | * @param adapter The adapter to use to create this view's content.
406 | */
407 | public abstract void setAdapter(T adapter);
408 |
409 | /**
410 | * This method is not supported and throws an UnsupportedOperationException when called.
411 | *
412 | * @param child Ignored.
413 | * @throws UnsupportedOperationException Every time this method is invoked.
414 | */
415 | @Override
416 | public void addView(View child) {
417 | throw new UnsupportedOperationException("addView(View) is not supported in AdapterView");
418 | }
419 |
420 | /**
421 | * This method is not supported and throws an UnsupportedOperationException when called.
422 | *
423 | * @param child Ignored.
424 | * @param index Ignored.
425 | * @throws UnsupportedOperationException Every time this method is invoked.
426 | */
427 | @Override
428 | public void addView(View child, int index) {
429 | throw new UnsupportedOperationException("addView(View, int) is not supported in AdapterView");
430 | }
431 |
432 | /**
433 | * This method is not supported and throws an UnsupportedOperationException when called.
434 | *
435 | * @param child Ignored.
436 | * @param params Ignored.
437 | * @throws UnsupportedOperationException Every time this method is invoked.
438 | */
439 | @Override
440 | public void addView(View child, LayoutParams params) {
441 | throw new UnsupportedOperationException("addView(View, LayoutParams) "
442 | + "is not supported in AdapterView");
443 | }
444 |
445 | /**
446 | * This method is not supported and throws an UnsupportedOperationException when called.
447 | *
448 | * @param child Ignored.
449 | * @param index Ignored.
450 | * @param params Ignored.
451 | * @throws UnsupportedOperationException Every time this method is invoked.
452 | */
453 | @Override
454 | public void addView(View child, int index, LayoutParams params) {
455 | throw new UnsupportedOperationException("addView(View, int, LayoutParams) "
456 | + "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 | * @throws UnsupportedOperationException Every time this method is invoked.
464 | */
465 | @Override
466 | public void removeView(View child) {
467 | throw new UnsupportedOperationException("removeView(View) is not supported in AdapterView");
468 | }
469 |
470 | /**
471 | * This method is not supported and throws an UnsupportedOperationException when called.
472 | *
473 | * @param index Ignored.
474 | * @throws UnsupportedOperationException Every time this method is invoked.
475 | */
476 | @Override
477 | public void removeViewAt(int index) {
478 | throw new UnsupportedOperationException("removeViewAt(int) is not supported in AdapterView");
479 | }
480 |
481 | /**
482 | * This method is not supported and throws an UnsupportedOperationException when called.
483 | *
484 | * @throws UnsupportedOperationException Every time this method is invoked.
485 | */
486 | @Override
487 | public void removeAllViews() {
488 | throw new UnsupportedOperationException("removeAllViews() is not supported in AdapterView");
489 | }
490 |
491 | @Override
492 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
493 | mLayoutHeight = getHeight();
494 | }
495 |
496 | /**
497 | * Return the position of the currently selected item within the adapter's data set
498 | *
499 | * @return int Position (starting at 0), or {@link #INVALID_POSITION} if there is nothing selected.
500 | */
501 | @ViewDebug.CapturedViewProperty
502 | public int getSelectedItemPosition() {
503 | return mNextSelectedPosition;
504 | }
505 |
506 | /**
507 | * @return The id corresponding to the currently selected item, or {@link #INVALID_ROW_ID}
508 | * if nothing is selected.
509 | */
510 | @ViewDebug.CapturedViewProperty
511 | public long getSelectedItemId() {
512 | return mNextSelectedRowId;
513 | }
514 |
515 | /**
516 | * @return The view corresponding to the currently selected item, or null
517 | * if nothing is selected
518 | */
519 | public abstract View getSelectedView();
520 |
521 | /**
522 | * @return The data corresponding to the currently selected item, or
523 | * null if there is nothing selected.
524 | */
525 | public Object getSelectedItem() {
526 | T adapter = getAdapter();
527 | int selection = getSelectedItemPosition();
528 | if (adapter != null && adapter.getCount() > 0 && selection >= 0) {
529 | return adapter.getItem(selection);
530 | } else {
531 | return null;
532 | }
533 | }
534 |
535 | /**
536 | * @return The number of items owned by the Adapter associated with this
537 | * AdapterView. (This is the number of data items, which may be
538 | * larger than the number of visible views.)
539 | */
540 | @ViewDebug.CapturedViewProperty
541 | public int getCount() {
542 | return mItemCount;
543 | }
544 |
545 | /**
546 | * Get the position within the adapter's data set for the view, where view is a an adapter item
547 | * or a descendant of an adapter item.
548 | *
549 | * @param view an adapter item, or a descendant of an adapter item. This must be visible in this
550 | * AdapterView at the time of the call.
551 | * @return the position within the adapter's data set of the view, or {@link #INVALID_POSITION}
552 | * if the view does not correspond to a list item (or it is not currently visible).
553 | */
554 | public int getPositionForView(View view) {
555 | View listItem = view;
556 | try {
557 | View v;
558 | while (!(v = (View) listItem.getParent()).equals(this)) {
559 | listItem = v;
560 | }
561 | } catch (ClassCastException e) {
562 | // We made it up to the window without find this list view
563 | return INVALID_POSITION;
564 | }
565 |
566 | // Search the children for the list item
567 | final int childCount = getChildCount();
568 | for (int i = 0; i < childCount; i++) {
569 | if (getChildAt(i).equals(listItem)) {
570 | return mFirstPosition + i;
571 | }
572 | }
573 |
574 | // Child not found!
575 | return INVALID_POSITION;
576 | }
577 |
578 | /**
579 | * Returns the position within the adapter's data set for the first item
580 | * displayed on screen.
581 | *
582 | * @return The position within the adapter's data set
583 | */
584 | public int getFirstVisiblePosition() {
585 | return mFirstPosition;
586 | }
587 |
588 | /**
589 | * Returns the position within the adapter's data set for the last item
590 | * displayed on screen.
591 | *
592 | * @return The position within the adapter's data set
593 | */
594 | public int getLastVisiblePosition() {
595 | return mFirstPosition + getChildCount() - 1;
596 | }
597 |
598 | /**
599 | * Sets the currently selected item. To support accessibility subclasses that
600 | * override this method must invoke the overriden super method first.
601 | *
602 | * @param position Index (starting at 0) of the data item to be selected.
603 | */
604 | public abstract void setSelection(int position);
605 |
606 | /**
607 | * Sets the view to show if the adapter is empty
608 | */
609 |
610 | public void setEmptyView(View emptyView) {
611 | mEmptyView = emptyView;
612 |
613 | final T adapter = getAdapter();
614 | final boolean empty = ((adapter == null) || adapter.isEmpty());
615 | updateEmptyStatus(empty);
616 | }
617 |
618 | /**
619 | * When the current adapter is empty, the AdapterView can display a special view
620 | * call the empty view. The empty view is used to provide feedback to the user
621 | * that no data is available in this AdapterView.
622 | *
623 | * @return The view to show if the adapter is empty.
624 | */
625 | public View getEmptyView() {
626 | return mEmptyView;
627 | }
628 |
629 | /**
630 | * Indicates whether this view is in filter mode. Filter mode can for instance
631 | * be enabled by a user when typing on the keyboard.
632 | *
633 | * @return True if the view is in filter mode, false otherwise.
634 | */
635 | boolean isInFilterMode() {
636 | return false;
637 | }
638 |
639 | @Override
640 | public void setFocusable(boolean focusable) {
641 | final T adapter = getAdapter();
642 | final boolean empty = adapter == null || adapter.getCount() == 0;
643 |
644 | mDesiredFocusableState = focusable;
645 | if (!focusable) {
646 | mDesiredFocusableInTouchModeState = false;
647 | }
648 |
649 | super.setFocusable(focusable && (!empty || isInFilterMode()));
650 | }
651 |
652 | @Override
653 | public void setFocusableInTouchMode(boolean focusable) {
654 | final T adapter = getAdapter();
655 | final boolean empty = adapter == null || adapter.getCount() == 0;
656 |
657 | mDesiredFocusableInTouchModeState = focusable;
658 | if (focusable) {
659 | mDesiredFocusableState = true;
660 | }
661 |
662 | super.setFocusableInTouchMode(focusable && (!empty || isInFilterMode()));
663 | }
664 |
665 | void checkFocus() {
666 | final T adapter = getAdapter();
667 | final boolean empty = adapter == null || adapter.getCount() == 0;
668 | final boolean focusable = !empty || isInFilterMode();
669 | // The order in which we set focusable in touch mode/focusable may matter
670 | // for the client, see View.setFocusableInTouchMode() comments for more
671 | // details
672 | super.setFocusableInTouchMode(focusable && mDesiredFocusableInTouchModeState);
673 | super.setFocusable(focusable && mDesiredFocusableState);
674 | if (mEmptyView != null) {
675 | updateEmptyStatus((adapter == null) || adapter.isEmpty());
676 | }
677 | }
678 |
679 | /**
680 | * Update the status of the list based on the empty parameter. If empty is true and
681 | * we have an empty view, display it. In all the other cases, make sure that the listview
682 | * is VISIBLE and that the empty view is GONE (if it's not null).
683 | */
684 | private void updateEmptyStatus(boolean empty) {
685 | if (isInFilterMode()) {
686 | empty = false;
687 | }
688 |
689 | if (empty) {
690 | if (mEmptyView != null) {
691 | mEmptyView.setVisibility(View.VISIBLE);
692 | setVisibility(View.GONE);
693 | } else {
694 | // If the caller just removed our empty view, make sure the list view is visible
695 | setVisibility(View.VISIBLE);
696 | }
697 |
698 | // We are now GONE, so pending layouts will not be dispatched.
699 | // Force one here to make sure that the state of the list matches
700 | // the state of the adapter.
701 | if (mDataChanged) {
702 | // onLayout(false, getLeft(), getTop(), getRight(), getBottom());
703 | mLayoutHeight = getHeight();
704 | }
705 | } else {
706 | if (mEmptyView != null) mEmptyView.setVisibility(View.GONE);
707 | setVisibility(View.VISIBLE);
708 | }
709 | }
710 |
711 | /**
712 | * Gets the data associated with the specified position in the list.
713 | *
714 | * @param position Which data to get
715 | * @return The data associated with the specified position in the list
716 | */
717 | public Object getItemAtPosition(int position) {
718 | T adapter = getAdapter();
719 | return (adapter == null || position < 0) ? null : adapter.getItem(position);
720 | }
721 |
722 | public long getItemIdAtPosition(int position) {
723 | T adapter = getAdapter();
724 | return (adapter == null || position < 0) ? INVALID_ROW_ID : adapter.getItemId(position);
725 | }
726 |
727 | @Override
728 | public void setOnClickListener(OnClickListener l) {
729 | throw new RuntimeException("Don't call setOnClickListener for an AdapterView. "
730 | + "You probably want setOnItemClickListener instead");
731 | }
732 |
733 | /**
734 | * Override to prevent freezing of any views created by the adapter.
735 | */
736 | @Override
737 | protected void dispatchSaveInstanceState(SparseArray container) {
738 | dispatchFreezeSelfOnly(container);
739 | }
740 |
741 | /**
742 | * Override to prevent thawing of any views created by the adapter.
743 | */
744 | @Override
745 | protected void dispatchRestoreInstanceState(SparseArray container) {
746 | dispatchThawSelfOnly(container);
747 | }
748 |
749 | class AdapterDataSetObserver extends DataSetObserver {
750 |
751 | private Parcelable mInstanceState = null;
752 |
753 | @Override
754 | public void onChanged() {
755 | mDataChanged = true;
756 | mOldItemCount = mItemCount;
757 | mItemCount = getAdapter().getCount();
758 |
759 | // Detect the case where a cursor that was previously invalidated has
760 | // been repopulated with new data.
761 | if (AdapterView.this.getAdapter().hasStableIds() && mInstanceState != null
762 | && mOldItemCount == 0 && mItemCount > 0) {
763 | AdapterView.this.onRestoreInstanceState(mInstanceState);
764 | mInstanceState = null;
765 | } else {
766 | rememberSyncState();
767 | }
768 | checkFocus();
769 | requestLayout();
770 | }
771 |
772 | @Override
773 | public void onInvalidated() {
774 | mDataChanged = true;
775 |
776 | if (AdapterView.this.getAdapter().hasStableIds()) {
777 | // Remember the current state for the case where our hosting activity is being
778 | // stopped and later restarted
779 | mInstanceState = AdapterView.this.onSaveInstanceState();
780 | }
781 |
782 | // Data is invalid so we should reset our state
783 | mOldItemCount = mItemCount;
784 | mItemCount = 0;
785 | mSelectedPosition = INVALID_POSITION;
786 | mSelectedRowId = INVALID_ROW_ID;
787 | mNextSelectedPosition = INVALID_POSITION;
788 | mNextSelectedRowId = INVALID_ROW_ID;
789 | mNeedSync = false;
790 |
791 | checkFocus();
792 | requestLayout();
793 | }
794 |
795 | public void clearSavedState() {
796 | mInstanceState = null;
797 | }
798 | }
799 |
800 | @Override
801 | protected void onDetachedFromWindow() {
802 | super.onDetachedFromWindow();
803 | removeCallbacks(mSelectionNotifier);
804 | }
805 |
806 | private class SelectionNotifier implements Runnable {
807 | public void run() {
808 | if (mDataChanged) {
809 | // Data has changed between when this SelectionNotifier
810 | // was posted and now. We need to wait until the AdapterView
811 | // has been synched to the new data.
812 | if (getAdapter() != null) {
813 | post(this);
814 | }
815 | } else {
816 | fireOnSelected();
817 | }
818 | }
819 | }
820 |
821 | void selectionChanged() {
822 | if (mOnItemSelectedListener != null) {
823 | if (mInLayout || mBlockLayoutRequests) {
824 | // If we are in a layout traversal, defer notification
825 | // by posting. This ensures that the view tree is
826 | // in a consistent state and is able to accomodate
827 | // new layout or invalidate requests.
828 | if (mSelectionNotifier == null) {
829 | mSelectionNotifier = new SelectionNotifier();
830 | }
831 | post(mSelectionNotifier);
832 | } else {
833 | fireOnSelected();
834 | }
835 | }
836 |
837 | // we fire selection events here not in View
838 | if (mSelectedPosition != ListView.INVALID_POSITION && isShown() && !isInTouchMode()) {
839 | sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
840 | }
841 | }
842 |
843 | private void fireOnSelected() {
844 | if (mOnItemSelectedListener == null)
845 | return;
846 |
847 | int selection = this.getSelectedItemPosition();
848 | if (selection >= 0) {
849 | View v = getSelectedView();
850 | mOnItemSelectedListener.onItemSelected(this, v, selection,
851 | getAdapter().getItemId(selection));
852 | } else {
853 | mOnItemSelectedListener.onNothingSelected(this);
854 | }
855 | }
856 |
857 | @Override
858 | public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
859 | View selectedView = getSelectedView();
860 | if (selectedView != null && selectedView.getVisibility() == VISIBLE
861 | && selectedView.dispatchPopulateAccessibilityEvent(event)) {
862 | return true;
863 | }
864 | return false;
865 | }
866 |
867 | @Override
868 | public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) {
869 | if (super.onRequestSendAccessibilityEvent(child, event)) {
870 | // Add a record for ourselves as well.
871 | AccessibilityEvent record = AccessibilityEvent.obtain();
872 | onInitializeAccessibilityEvent(record);
873 | // Populate with the text of the requesting child.
874 | child.dispatchPopulateAccessibilityEvent(record);
875 | event.appendRecord(record);
876 | return true;
877 | }
878 | return false;
879 | }
880 |
881 | @Override
882 | public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
883 | super.onInitializeAccessibilityNodeInfo(info);
884 | info.setScrollable(isScrollableForAccessibility());
885 | View selectedView = getSelectedView();
886 | if (selectedView != null) {
887 | info.setEnabled(selectedView.isEnabled());
888 | }
889 | }
890 |
891 | @Override
892 | public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
893 | super.onInitializeAccessibilityEvent(event);
894 | event.setScrollable(isScrollableForAccessibility());
895 | View selectedView = getSelectedView();
896 | if (selectedView != null) {
897 | event.setEnabled(selectedView.isEnabled());
898 | }
899 | event.setCurrentItemIndex(getSelectedItemPosition());
900 | event.setFromIndex(getFirstVisiblePosition());
901 | event.setToIndex(getLastVisiblePosition());
902 | event.setItemCount(getCount());
903 | }
904 |
905 | private boolean isScrollableForAccessibility() {
906 | T adapter = getAdapter();
907 | if (adapter != null) {
908 | final int itemCount = adapter.getCount();
909 | return itemCount > 0
910 | && (getFirstVisiblePosition() > 0 || getLastVisiblePosition() < itemCount - 1);
911 | }
912 | return false;
913 | }
914 |
915 | @Override
916 | protected boolean canAnimate() {
917 | return super.canAnimate() && mItemCount > 0;
918 | }
919 |
920 | void handleDataChanged() {
921 | final int count = mItemCount;
922 | boolean found = false;
923 |
924 | if (count > 0) {
925 |
926 | int newPos;
927 |
928 | // Find the row we are supposed to sync to
929 | if (mNeedSync) {
930 | // Update this first, since setNextSelectedPositionInt inspects
931 | // it
932 | mNeedSync = false;
933 |
934 | // See if we can find a position in the new data with the same
935 | // id as the old selection
936 | newPos = findSyncPosition();
937 | if (newPos >= 0) {
938 | // Verify that new selection is selectable
939 | int selectablePos = lookForSelectablePosition(newPos, true);
940 | if (selectablePos == newPos) {
941 | // Same row id is selected
942 | setNextSelectedPositionInt(newPos);
943 | found = true;
944 | }
945 | }
946 | }
947 | if (!found) {
948 | // Try to use the same position if we can't find matching data
949 | newPos = getSelectedItemPosition();
950 |
951 | // Pin position to the available range
952 | if (newPos >= count) {
953 | newPos = count - 1;
954 | }
955 | if (newPos < 0) {
956 | newPos = 0;
957 | }
958 |
959 | // Make sure we select something selectable -- first look down
960 | int selectablePos = lookForSelectablePosition(newPos, true);
961 | if (selectablePos < 0) {
962 | // Looking down didn't work -- try looking up
963 | selectablePos = lookForSelectablePosition(newPos, false);
964 | }
965 | if (selectablePos >= 0) {
966 | setNextSelectedPositionInt(selectablePos);
967 | checkSelectionChanged();
968 | found = true;
969 | }
970 | }
971 | }
972 | if (!found) {
973 | // Nothing is selected
974 | mSelectedPosition = INVALID_POSITION;
975 | mSelectedRowId = INVALID_ROW_ID;
976 | mNextSelectedPosition = INVALID_POSITION;
977 | mNextSelectedRowId = INVALID_ROW_ID;
978 | mNeedSync = false;
979 | checkSelectionChanged();
980 | }
981 | }
982 |
983 | void checkSelectionChanged() {
984 | if ((mSelectedPosition != mOldSelectedPosition) || (mSelectedRowId != mOldSelectedRowId)) {
985 | selectionChanged();
986 | mOldSelectedPosition = mSelectedPosition;
987 | mOldSelectedRowId = mSelectedRowId;
988 | }
989 | }
990 |
991 | /**
992 | * Searches the adapter for a position matching mSyncRowId. The search starts at mSyncPosition
993 | * and then alternates between moving up and moving down until 1) we find the right position, or
994 | * 2) we run out of time, or 3) we have looked at every position
995 | *
996 | * @return Position of the row that matches mSyncRowId, or {@link #INVALID_POSITION} if it can't
997 | * be found
998 | */
999 | int findSyncPosition() {
1000 | int count = mItemCount;
1001 |
1002 | if (count == 0) {
1003 | return INVALID_POSITION;
1004 | }
1005 |
1006 | long idToMatch = mSyncRowId;
1007 | int seed = mSyncPosition;
1008 |
1009 | // If there isn't a selection don't hunt for it
1010 | if (idToMatch == INVALID_ROW_ID) {
1011 | return INVALID_POSITION;
1012 | }
1013 |
1014 | // Pin seed to reasonable values
1015 | seed = Math.max(0, seed);
1016 | seed = Math.min(count - 1, seed);
1017 |
1018 | long endTime = SystemClock.uptimeMillis() + SYNC_MAX_DURATION_MILLIS;
1019 |
1020 | long rowId;
1021 |
1022 | // first position scanned so far
1023 | int first = seed;
1024 |
1025 | // last position scanned so far
1026 | int last = seed;
1027 |
1028 | // True if we should move down on the next iteration
1029 | boolean next = false;
1030 |
1031 | // True when we have looked at the first item in the data
1032 | boolean hitFirst;
1033 |
1034 | // True when we have looked at the last item in the data
1035 | boolean hitLast;
1036 |
1037 | // Get the item ID locally (instead of getItemIdAtPosition), so
1038 | // we need the adapter
1039 | T adapter = getAdapter();
1040 | if (adapter == null) {
1041 | return INVALID_POSITION;
1042 | }
1043 |
1044 | while (SystemClock.uptimeMillis() <= endTime) {
1045 | rowId = adapter.getItemId(seed);
1046 | if (rowId == idToMatch) {
1047 | // Found it!
1048 | return seed;
1049 | }
1050 |
1051 | hitLast = last == count - 1;
1052 | hitFirst = first == 0;
1053 |
1054 | if (hitLast && hitFirst) {
1055 | // Looked at everything
1056 | break;
1057 | }
1058 |
1059 | if (hitFirst || (next && !hitLast)) {
1060 | // Either we hit the top, or we are trying to move down
1061 | last++;
1062 | seed = last;
1063 | // Try going up next time
1064 | next = false;
1065 | } else if (hitLast || (!next && !hitFirst)) {
1066 | // Either we hit the bottom, or we are trying to move up
1067 | first--;
1068 | seed = first;
1069 | // Try going down next time
1070 | next = true;
1071 | }
1072 |
1073 | }
1074 |
1075 | return INVALID_POSITION;
1076 | }
1077 |
1078 | /**
1079 | * Find a position that can be selected (i.e., is not a separator).
1080 | *
1081 | * @param position The starting position to look at.
1082 | * @param lookDown Whether to look down for other positions.
1083 | * @return The next selectable position starting at position and then searching either up or
1084 | * down. Returns {@link #INVALID_POSITION} if nothing can be found.
1085 | */
1086 | int lookForSelectablePosition(int position, boolean lookDown) {
1087 | return position;
1088 | }
1089 |
1090 | /**
1091 | * Utility to keep mSelectedPosition and mSelectedRowId in sync
1092 | *
1093 | * @param position Our current position
1094 | */
1095 | void setSelectedPositionInt(int position) {
1096 | mSelectedPosition = position;
1097 | mSelectedRowId = getItemIdAtPosition(position);
1098 | }
1099 |
1100 | /**
1101 | * Utility to keep mNextSelectedPosition and mNextSelectedRowId in sync
1102 | *
1103 | * @param position Intended value for mSelectedPosition the next time we go
1104 | * through layout
1105 | */
1106 | void setNextSelectedPositionInt(int position) {
1107 | mNextSelectedPosition = position;
1108 | mNextSelectedRowId = getItemIdAtPosition(position);
1109 | // If we are trying to sync to the selection, update that too
1110 | if (mNeedSync && mSyncMode == SYNC_SELECTED_POSITION && position >= 0) {
1111 | mSyncPosition = position;
1112 | mSyncRowId = mNextSelectedRowId;
1113 | }
1114 | }
1115 |
1116 | /**
1117 | * Remember enough information to restore the screen state when the data has
1118 | * changed.
1119 | */
1120 | void rememberSyncState() {
1121 | if (getChildCount() > 0) {
1122 | mNeedSync = true;
1123 | mSyncHeight = mLayoutHeight;
1124 | if (mSelectedPosition >= 0) {
1125 | // Sync the selection state
1126 | View v = getChildAt(mSelectedPosition - mFirstPosition);
1127 | mSyncRowId = mNextSelectedRowId;
1128 | mSyncPosition = mNextSelectedPosition;
1129 | if (v != null) {
1130 | mSpecificTop = v.getTop();
1131 | }
1132 | mSyncMode = SYNC_SELECTED_POSITION;
1133 | } else {
1134 | // Sync the based on the offset of the first view
1135 | View v = getChildAt(0);
1136 | T adapter = getAdapter();
1137 | if (mFirstPosition >= 0 && mFirstPosition < adapter.getCount()) {
1138 | mSyncRowId = adapter.getItemId(mFirstPosition);
1139 | } else {
1140 | mSyncRowId = NO_ID;
1141 | }
1142 | mSyncPosition = mFirstPosition;
1143 | if (v != null) {
1144 | mSpecificTop = v.getTop();
1145 | }
1146 | mSyncMode = SYNC_FIRST_POSITION;
1147 | }
1148 | }
1149 | }
1150 |
1151 | protected boolean isTransformedTouchPointInView(float x, float y, View child, PointF outLocalPoint) {
1152 | //Log.d(TAG, "isTranformedTouchPointInView()");
1153 | final Rect frame = new Rect();
1154 | child.getHitRect(frame);
1155 | if (frame.contains((int) x, (int) y)) {
1156 | return true;
1157 | }
1158 | return false;
1159 | }
1160 | }
--------------------------------------------------------------------------------
/stackview/src/main/java/com/stackview/StackViewVertical.java:
--------------------------------------------------------------------------------
1 | package com.stackview;
2 |
3 | /**
4 | * Created by binary on 5/19/15.
5 | */
6 |
7 | import android.animation.ObjectAnimator;
8 | import android.animation.PropertyValuesHolder;
9 | import android.content.Context;
10 | import android.content.res.TypedArray;
11 | import android.graphics.Bitmap;
12 | import android.graphics.BlurMaskFilter;
13 | import android.graphics.Canvas;
14 | import android.graphics.Matrix;
15 | import android.graphics.Paint;
16 | import android.graphics.PorterDuff;
17 | import android.graphics.PorterDuffXfermode;
18 | import android.graphics.Rect;
19 | import android.graphics.RectF;
20 | import android.graphics.Region;
21 | import android.util.AttributeSet;
22 | import android.util.DisplayMetrics;
23 | import android.util.Log;
24 | import android.view.InputDevice;
25 | import android.view.MotionEvent;
26 | import android.view.VelocityTracker;
27 | import android.view.View;
28 | import android.view.ViewConfiguration;
29 | import android.view.ViewGroup;
30 | import android.view.WindowManager;
31 | import android.view.animation.LinearInterpolator;
32 | import android.widget.FrameLayout;
33 | import android.widget.ImageView;
34 |
35 | import java.lang.ref.WeakReference;
36 |
37 |
38 | /**
39 | * A view that displays its children in a stack and allows users to discretely swipe
40 | * through the children.
41 | */
42 | public class StackViewVertical extends AdapterViewAnimator {
43 | private final String TAG = "StackView";
44 |
45 | /**
46 | * Default animation parameters
47 | */
48 | private static final int DEFAULT_ANIMATION_DURATION = 400;
49 | private static final int MINIMUM_ANIMATION_DURATION = 50;
50 | private static final int STACK_RELAYOUT_DURATION = 100;
51 |
52 | /**
53 | * Parameters effecting the perspective visuals
54 | */
55 | private static final float PERSPECTIVE_SHIFT_FACTOR_Y = 0.04f;
56 | private static final float PERSPECTIVE_SHIFT_FACTOR_X = 0.00f;
57 |
58 | private float mPerspectiveShiftX;
59 | private float mPerspectiveShiftY;
60 | private float mNewPerspectiveShiftX;
61 | private float mNewPerspectiveShiftY;
62 |
63 | @SuppressWarnings({"FieldCanBeLocal"})
64 | private static final float PERSPECTIVE_SCALE_FACTOR = 0.03f;
65 |
66 | /**
67 | * Represent the two possible stack modes, one where items slide up, and the other
68 | * where items slide down. The perspective is also inverted between these two modes.
69 | */
70 | private static final int ITEMS_SLIDE_UP = 0;
71 | private static final int ITEMS_SLIDE_DOWN = 1;
72 |
73 | /**
74 | * These specify the different gesture states
75 | */
76 | private static final int GESTURE_NONE = 0;
77 | private static final int GESTURE_SLIDE_UP = 1;
78 | private static final int GESTURE_SLIDE_DOWN = 2;
79 |
80 |
81 | /**
82 | * Specifies how far you need to swipe (up or down) before it
83 | * will be consider a completed gesture when you lift your finger
84 | */
85 | private static final float SWIPE_THRESHOLD_RATIO = 0.2f;
86 |
87 | /**
88 | * Specifies the total distance, relative to the size of the stack,
89 | * that views will be slid, either up or down
90 | */
91 | private static final float SLIDE_UP_RATIO = 0.7f;
92 |
93 | /**
94 | * Sentinel value for no current active pointer.
95 | * Used by {@link #mActivePointerId}.
96 | */
97 | private static final int INVALID_POINTER = -1;
98 |
99 | /**
100 | * Number of active views in the stack. One fewer view is actually visible, as one is hidden.
101 | */
102 | private static final int NUM_ACTIVE_VIEWS = 5;
103 |
104 | private static final int FRAME_PADDING = 4;
105 |
106 | private final Rect mTouchRect = new Rect();
107 |
108 | private static final int MIN_TIME_BETWEEN_INTERACTION_AND_AUTOADVANCE = 5000;
109 |
110 | private static final long MIN_TIME_BETWEEN_SCROLLS = 100;
111 |
112 | /**
113 | * These variables are all related to the current state of touch interaction
114 | * with the stack
115 | */
116 | private float mInitialY;
117 | private float mInitialX;
118 | private int mActivePointerId;
119 | private int mYVelocity = 0;
120 | private int mSwipeGestureType = GESTURE_NONE;
121 | private int mSlideAmount;
122 | private int mSwipeThreshold;
123 | private int mTouchSlop;
124 | private int mMaximumVelocity;
125 | private VelocityTracker mVelocityTracker;
126 | private boolean mTransitionIsSetup = false;
127 | private int mResOutColor;
128 | private int mClickColor;
129 |
130 | private static HolographicHelper sHolographicHelper;
131 | private ImageView mHighlight;
132 | private ImageView mClickFeedback;
133 | private boolean mClickFeedbackIsValid = false;
134 | private StackSlider mStackSlider;
135 | private boolean mFirstLayoutHappened = false;
136 | private long mLastInteractionTime = 0;
137 | private long mLastScrollTime;
138 | private int mStackMode;
139 | private int mFramePadding;
140 | private final Rect stackInvalidateRect = new Rect();
141 |
142 | private final WindowManager WINDOWS_MANAGER = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
143 | private final DisplayMetrics DISPLAY_METRICS = new DisplayMetrics();
144 |
145 | /**
146 | * {@inheritDoc}
147 | */
148 | public StackViewVertical(Context context) {
149 | this(context, null);
150 | }
151 |
152 | /**
153 | * {@inheritDoc}
154 | */
155 | public StackViewVertical(Context context, AttributeSet attrs) {
156 | this(context, attrs, R.attr.stackViewStyleHorizontal);
157 | }
158 |
159 | /**
160 | * {@inheritDoc}
161 | */
162 | public StackViewVertical(Context context, AttributeSet attrs, int defStyleAttr) {
163 | super(context, attrs, defStyleAttr);
164 | WINDOWS_MANAGER.getDefaultDisplay().getMetrics(DISPLAY_METRICS);
165 | TypedArray a = context.obtainStyledAttributes(attrs,
166 | R.styleable.StackViewHorizontal, defStyleAttr, 0);
167 |
168 | mResOutColor = a.getColor(
169 | R.styleable.StackViewHorizontal_resOutColorHorizontal, 0);
170 | mClickColor = a.getColor(
171 | R.styleable.StackViewHorizontal_clickColorHorizontal, 0);
172 |
173 | a.recycle();
174 | initStackView();
175 | }
176 |
177 | private void initStackView() {
178 | configureViewAnimator(NUM_ACTIVE_VIEWS, 1);
179 | setStaticTransformationsEnabled(true);
180 | final ViewConfiguration configuration = ViewConfiguration.get(getContext());
181 | mTouchSlop = configuration.getScaledTouchSlop();
182 | mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
183 | mActivePointerId = INVALID_POINTER;
184 |
185 | mHighlight = new ImageView(getContext());
186 | mHighlight.setLayoutParams(new LayoutParams(mHighlight));
187 | addViewInLayout(mHighlight, -1, new LayoutParams(mHighlight));
188 |
189 | mClickFeedback = new ImageView(getContext());
190 | mClickFeedback.setLayoutParams(new LayoutParams(mClickFeedback));
191 | addViewInLayout(mClickFeedback, -1, new LayoutParams(mClickFeedback));
192 | mClickFeedback.setVisibility(INVISIBLE);
193 |
194 | mStackSlider = new StackSlider();
195 |
196 | if (sHolographicHelper == null) {
197 | sHolographicHelper = new HolographicHelper(getContext());
198 | }
199 | setClipChildren(false);
200 | setClipToPadding(false);
201 |
202 | // This sets the form of the StackView, which is currently to have the perspective-shifted
203 | // views above the active view, and have items slide down when sliding out. The opposite is
204 | // available by using ITEMS_SLIDE_UP.
205 | mStackMode = ITEMS_SLIDE_DOWN;
206 |
207 | // This is a flag to indicate the the stack is loading for the first time
208 | mWhichChild = -1;
209 |
210 | // Adjust the frame padding based on the density, since the highlight changes based
211 | // on the density
212 | final float density = getResources().getDisplayMetrics().density;
213 | mFramePadding = (int) Math.ceil(density * FRAME_PADDING);
214 | }
215 |
216 | /**
217 | * Animate the views between different relative indexes within the {@link AdapterViewAnimator}
218 | */
219 | void transformViewForTransition(int fromIndex, int toIndex, final View view, boolean animate) {
220 | if (!animate) {
221 | ((StackFrame) view).cancelSliderAnimator();
222 | view.setRotationX(0f);
223 | LayoutParams lp = (LayoutParams) view.getLayoutParams();
224 | lp.setVerticalOffset(0);
225 | lp.setHorizontalOffset(0);
226 | }
227 |
228 | if (fromIndex == -1 && toIndex == getNumActiveViews() - 1) {
229 | transformViewAtIndex(toIndex, view, false);
230 | view.setVisibility(VISIBLE);
231 | view.setAlpha(1.0f);
232 | } else if (fromIndex == 0 && toIndex == 1) {
233 | // Slide item in
234 | ((StackFrame) view).cancelSliderAnimator();
235 | view.setVisibility(VISIBLE);
236 |
237 | int duration = Math.round(mStackSlider.getDurationForNeutralPosition(mYVelocity));
238 | StackSlider animationSlider = new StackSlider(mStackSlider);
239 | animationSlider.setView(view);
240 |
241 | if (animate) {
242 | PropertyValuesHolder slideInY = PropertyValuesHolder.ofFloat("YProgress", 0.0f);
243 | PropertyValuesHolder slideInX = PropertyValuesHolder.ofFloat("XProgress", 0.0f);
244 | ObjectAnimator slideIn = ObjectAnimator.ofPropertyValuesHolder(animationSlider,
245 | slideInX, slideInY);
246 | slideIn.setDuration(duration);
247 | slideIn.setInterpolator(new LinearInterpolator());
248 | ((StackFrame) view).setSliderAnimator(slideIn);
249 | slideIn.start();
250 | } else {
251 | animationSlider.setYProgress(0f);
252 | animationSlider.setXProgress(0f);
253 | }
254 | } else if (fromIndex == 1 && toIndex == 0) {
255 | // Slide item out
256 | ((StackFrame) view).cancelSliderAnimator();
257 | int duration = Math.round(mStackSlider.getDurationForOffscreenPosition(mYVelocity));
258 |
259 | StackSlider animationSlider = new StackSlider(mStackSlider);
260 | animationSlider.setView(view);
261 | if (animate) {
262 | PropertyValuesHolder slideOutY = PropertyValuesHolder.ofFloat("YProgress", 1.0f);
263 | PropertyValuesHolder slideOutX = PropertyValuesHolder.ofFloat("XProgress", 0.0f);
264 | ObjectAnimator slideOut = ObjectAnimator.ofPropertyValuesHolder(animationSlider,
265 | slideOutX, slideOutY);
266 | slideOut.setDuration(duration);
267 | slideOut.setInterpolator(new LinearInterpolator());
268 | ((StackFrame) view).setSliderAnimator(slideOut);
269 | slideOut.start();
270 | } else {
271 | animationSlider.setYProgress(1.0f);
272 | animationSlider.setXProgress(0f);
273 | }
274 | } else if (toIndex == 0) {
275 | // Make sure this view that is "waiting in the wings" is invisible
276 | view.setAlpha(0.0f);
277 | view.setVisibility(INVISIBLE);
278 | } else if ((fromIndex == 0 || fromIndex == 1) && toIndex > 1) {
279 | view.setVisibility(VISIBLE);
280 | view.setAlpha(1.0f);
281 | view.setRotationX(0f);
282 | LayoutParams lp = (LayoutParams) view.getLayoutParams();
283 | lp.setVerticalOffset(0);
284 | lp.setHorizontalOffset(0);
285 | } else if (fromIndex == -1) {
286 | view.setAlpha(1.0f);
287 | view.setVisibility(VISIBLE);
288 | } else if (toIndex == -1) {
289 | if (animate) {
290 | postDelayed(new Runnable() {
291 | public void run() {
292 | view.setAlpha(0);
293 | }
294 | }, STACK_RELAYOUT_DURATION);
295 | } else {
296 | view.setAlpha(0f);
297 | }
298 | }
299 |
300 | // Implement the faked perspective
301 | if (toIndex != -1) {
302 | transformViewAtIndex(toIndex, view, animate);
303 | }
304 | }
305 |
306 | private void transformViewAtIndex(int index, final View view, boolean animate) {
307 |
308 | //TODO: fix this workaround, item doesn't show when adapter size == 1
309 | if (getAdapter().getCount() == 1) {
310 | return;
311 | }
312 |
313 | final float maxPerspectiveShiftY = mPerspectiveShiftY;
314 | final float maxPerspectiveShiftX = mPerspectiveShiftX;
315 |
316 | if (mStackMode == ITEMS_SLIDE_DOWN) {
317 | index = getNumActiveViews() - index - 1;
318 | if (index == getNumActiveViews() - 1) index--;
319 | } else {
320 | index--;
321 | if (index < 0) index++;
322 | }
323 |
324 | float r = (index * 1f) / (getNumActiveViews() - 2);
325 |
326 | float scale = 1f - PERSPECTIVE_SCALE_FACTOR * (1 - r);
327 |
328 | float perspectiveTranslationY = r * maxPerspectiveShiftY;
329 | float scaleShiftCorrectionY = (scale - 1) *
330 | (getMeasuredHeight() * (1 - PERSPECTIVE_SHIFT_FACTOR_Y) / 2.0f);
331 | final float transY = perspectiveTranslationY + scaleShiftCorrectionY;
332 |
333 | float perspectiveTranslationX = (1 - r) * maxPerspectiveShiftX;
334 | float scaleShiftCorrectionX = (1 - scale) *
335 | (getMeasuredWidth() * (1 - PERSPECTIVE_SHIFT_FACTOR_X) / 2.0f);
336 | final float transX = perspectiveTranslationX + (scaleShiftCorrectionX / 10);
337 |
338 | // If this view is currently being animated for a certain position, we need to cancel
339 | // this animation so as not to interfere with the new transformation.
340 | if (view instanceof StackFrame) {
341 | ((StackFrame) view).cancelTransformAnimator();
342 | }
343 |
344 | if (animate) {
345 | PropertyValuesHolder translationX = PropertyValuesHolder.ofFloat("translationX", transX);
346 | PropertyValuesHolder translationY = PropertyValuesHolder.ofFloat("translationY", transY);
347 | PropertyValuesHolder scalePropX = PropertyValuesHolder.ofFloat("scaleX", scale);
348 | PropertyValuesHolder scalePropY = PropertyValuesHolder.ofFloat("scaleY", scale);
349 |
350 | ObjectAnimator oa = ObjectAnimator.ofPropertyValuesHolder(view, scalePropX, scalePropY,
351 | translationY, translationX);
352 | oa.setDuration(STACK_RELAYOUT_DURATION);
353 | if (view instanceof StackFrame) {
354 | ((StackFrame) view).setTransformAnimator(oa);
355 | }
356 | oa.start();
357 | } else {
358 | view.setTranslationX(transX);
359 | view.setTranslationY(transY);
360 | view.setScaleX(scale);
361 | view.setScaleY(scale);
362 | }
363 | }
364 |
365 | private void setupStackSlider(View v, int mode) {
366 | mStackSlider.setMode(mode);
367 | if (v != null) {
368 | mHighlight.setImageBitmap(sHolographicHelper.createResOutline(v, mResOutColor));
369 | // mHighlight.setRotation(v.getRotation());
370 | mHighlight.setTranslationY(v.getTranslationY());
371 | mHighlight.setTranslationX(v.getTranslationX());
372 | mHighlight.bringToFront();
373 | v.bringToFront();
374 | mStackSlider.setView(v);
375 |
376 | v.setVisibility(VISIBLE);
377 | }
378 | }
379 |
380 | /**
381 | * {@inheritDoc}
382 | */
383 | @Override
384 | public void showNext() {
385 | if (mSwipeGestureType != GESTURE_NONE) return;
386 | if (!mTransitionIsSetup) {
387 | View v = getViewAtRelativeIndex(1);
388 | if (v != null) {
389 | setupStackSlider(v, StackSlider.NORMAL_MODE);
390 | mStackSlider.setYProgress(0);
391 | mStackSlider.setXProgress(0);
392 | }
393 | }
394 | super.showNext();
395 | }
396 |
397 | /**
398 | * {@inheritDoc}
399 | */
400 | @Override
401 | public void showPrevious() {
402 | if (mSwipeGestureType != GESTURE_NONE) return;
403 | if (!mTransitionIsSetup) {
404 | View v = getViewAtRelativeIndex(0);
405 | if (v != null) {
406 | setupStackSlider(v, StackSlider.NORMAL_MODE);
407 | mStackSlider.setYProgress(1);
408 | mStackSlider.setXProgress(0);
409 | }
410 | }
411 | super.showPrevious();
412 | }
413 |
414 | @Override
415 | void showOnly(int childIndex, boolean animate) {
416 | super.showOnly(childIndex, animate);
417 |
418 | // Here we need to make sure that the z-order of the children is correct
419 | for (int i = mCurrentWindowEnd; i >= mCurrentWindowStart; i--) {
420 | int index = modulo(i, getWindowSize());
421 | ViewAndMetaData vm = mViewsMap.get(index);
422 | if (vm != null) {
423 | View v = mViewsMap.get(index).view;
424 | if (v != null) v.bringToFront();
425 | }
426 | }
427 | if (mHighlight != null) {
428 | mHighlight.bringToFront();
429 | }
430 | mTransitionIsSetup = false;
431 | mClickFeedbackIsValid = false;
432 | }
433 |
434 | void updateClickFeedback() {
435 | if (!mClickFeedbackIsValid) {
436 | View v = getViewAtRelativeIndex(1);
437 | if (v != null) {
438 | mClickFeedback.setImageBitmap(
439 | sHolographicHelper.createClickOutline(v, mClickColor));
440 | mClickFeedback.setTranslationX(v.getTranslationX());
441 | mClickFeedback.setTranslationY(v.getTranslationY());
442 | }
443 | mClickFeedbackIsValid = true;
444 | }
445 | }
446 |
447 | @Override
448 | void showTapFeedback(View v) {
449 | updateClickFeedback();
450 | mClickFeedback.setVisibility(VISIBLE);
451 | mClickFeedback.bringToFront();
452 | invalidate();
453 | }
454 |
455 | @Override
456 | void hideTapFeedback(View v) {
457 | mClickFeedback.setVisibility(INVISIBLE);
458 | invalidate();
459 | }
460 |
461 | private void updateChildTransforms() {
462 | for (int i = 0; i < getNumActiveViews(); i++) {
463 | View v = getViewAtRelativeIndex(i);
464 | if (v != null) {
465 | transformViewAtIndex(i, v, false);
466 | }
467 | }
468 | }
469 |
470 | private static class StackFrame extends FrameLayout {
471 | WeakReference transformAnimator;
472 | WeakReference sliderAnimator;
473 |
474 | public StackFrame(Context context) {
475 | super(context);
476 | }
477 |
478 | void setTransformAnimator(ObjectAnimator oa) {
479 | transformAnimator = new WeakReference(oa);
480 | }
481 |
482 | void setSliderAnimator(ObjectAnimator oa) {
483 | sliderAnimator = new WeakReference(oa);
484 | }
485 |
486 | boolean cancelTransformAnimator() {
487 | if (transformAnimator != null) {
488 | ObjectAnimator oa = transformAnimator.get();
489 | if (oa != null) {
490 | oa.cancel();
491 | return true;
492 | }
493 | }
494 | return false;
495 | }
496 |
497 | boolean cancelSliderAnimator() {
498 | if (sliderAnimator != null) {
499 | ObjectAnimator oa = sliderAnimator.get();
500 | if (oa != null) {
501 | oa.cancel();
502 | return true;
503 | }
504 | }
505 | return false;
506 | }
507 | }
508 |
509 | @Override
510 | FrameLayout getFrameForChild() {
511 | StackFrame fl = new StackFrame(getContext());
512 | fl.setPadding(mFramePadding, mFramePadding, mFramePadding, mFramePadding);
513 | return fl;
514 | }
515 |
516 | /**
517 | * Apply any necessary tranforms for the child that is being added.
518 | */
519 | void applyTransformForChildAtIndex(View child, int relativeIndex) {
520 | }
521 |
522 | @Override
523 | protected void dispatchDraw(Canvas canvas) {
524 | boolean expandClipRegion = false;
525 |
526 | canvas.getClipBounds(stackInvalidateRect);
527 | final int childCount = getChildCount();
528 | for (int i = 0; i < childCount; i++) {
529 | final View child = getChildAt(i);
530 | LayoutParams lp = (LayoutParams) child.getLayoutParams();
531 | if ((lp.horizontalOffset == 0 && lp.verticalOffset == 0) ||
532 | child.getAlpha() == 0f || child.getVisibility() != VISIBLE) {
533 | lp.resetInvalidateRect();
534 | }
535 | Rect childInvalidateRect = lp.getInvalidateRect();
536 | if (!childInvalidateRect.isEmpty()) {
537 | expandClipRegion = true;
538 | stackInvalidateRect.union(childInvalidateRect);
539 | }
540 | }
541 |
542 | // We only expand the clip bounds if necessary.
543 | if (expandClipRegion) {
544 | canvas.save(Canvas.CLIP_SAVE_FLAG);
545 | canvas.clipRect(stackInvalidateRect, Region.Op.UNION);
546 | super.dispatchDraw(canvas);
547 | canvas.restore();
548 | } else {
549 | super.dispatchDraw(canvas);
550 | }
551 | }
552 |
553 | public void onLayoutView() {
554 | if (!mFirstLayoutHappened) {
555 | mFirstLayoutHappened = true;
556 | updateChildTransforms();
557 | }
558 |
559 | final int newSlideAmount = Math.round(SLIDE_UP_RATIO * getMeasuredHeight());
560 | if (mSlideAmount != newSlideAmount) {
561 | mSlideAmount = newSlideAmount;
562 | mSwipeThreshold = Math.round(SWIPE_THRESHOLD_RATIO * newSlideAmount);
563 | }
564 |
565 | if (Float.compare(mPerspectiveShiftY, mNewPerspectiveShiftY) != 0 ||
566 | Float.compare(mPerspectiveShiftX, mNewPerspectiveShiftX) != 0) {
567 |
568 | mPerspectiveShiftY = mNewPerspectiveShiftY;
569 | mPerspectiveShiftX = mNewPerspectiveShiftX;
570 | updateChildTransforms();
571 | }
572 | }
573 |
574 | @Override
575 | public boolean onGenericMotionEvent(MotionEvent event) {
576 | if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
577 | switch (event.getAction()) {
578 | case MotionEvent.ACTION_SCROLL: {
579 | final float vscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
580 | if (vscroll < 0) {
581 | pacedScroll(false);
582 | return true;
583 | } else if (vscroll > 0) {
584 | pacedScroll(true);
585 | return true;
586 | }
587 | }
588 | }
589 | }
590 | return super.onGenericMotionEvent(event);
591 | }
592 |
593 | // This ensures that the frequency of stack flips caused by scrolls is capped
594 | private void pacedScroll(boolean up) {
595 | long timeSinceLastScroll = System.currentTimeMillis() - mLastScrollTime;
596 | if (timeSinceLastScroll > MIN_TIME_BETWEEN_SCROLLS) {
597 | if (up) {
598 | showPrevious();
599 | } else {
600 | showNext();
601 | }
602 | mLastScrollTime = System.currentTimeMillis();
603 | }
604 | }
605 |
606 | /**
607 | * {@inheritDoc}
608 | */
609 | @Override
610 | public boolean onInterceptTouchEvent(MotionEvent ev) {
611 | int action = ev.getAction();
612 | switch (action & MotionEvent.ACTION_MASK) {
613 | case MotionEvent.ACTION_DOWN: {
614 | if (mActivePointerId == INVALID_POINTER) {
615 | mInitialX = ev.getX();
616 | mInitialY = ev.getY();
617 | mActivePointerId = ev.getPointerId(0);
618 | }
619 | break;
620 | }
621 | // case MotionEvent.ACTION_MOVE: {
622 | // int pointerIndex = ev.findPointerIndex(mActivePointerId);
623 | // if (pointerIndex == INVALID_POINTER) {
624 | // // no data for our primary pointer, this shouldn't happen, log it
625 | // Log.d(TAG, "Error: No data for our primary pointer.");
626 | // return false;
627 | // }
628 | // float newY = ev.getY(pointerIndex);
629 | // float deltaY = newY - mInitialY;
630 | //
631 | // float newX = ev.getX(pointerIndex);
632 | // float deltaX = newX - mInitialX;
633 | //
634 | // // beginGestureIfNeeded(deltaX);
635 | // break;
636 | // }
637 | case MotionEvent.ACTION_POINTER_UP: {
638 | onSecondaryPointerUp(ev);
639 | break;
640 | }
641 | case MotionEvent.ACTION_UP:
642 | case MotionEvent.ACTION_CANCEL: {
643 | mActivePointerId = INVALID_POINTER;
644 | mSwipeGestureType = GESTURE_NONE;
645 | }
646 | }
647 |
648 | return mSwipeGestureType != GESTURE_NONE;
649 | }
650 |
651 | private void beginGestureIfNeeded(float deltaY) {
652 | if ((int) Math.abs(deltaY) > mTouchSlop && mSwipeGestureType == GESTURE_NONE) {
653 |
654 | final int swipeGestureTypeY = deltaY < 0 ? GESTURE_SLIDE_UP : GESTURE_SLIDE_DOWN;
655 |
656 |
657 | cancelLongPress();
658 | requestDisallowInterceptTouchEvent(true);
659 |
660 | if (getAdapter() == null) return;
661 | final int adapterCount = getCount();
662 |
663 | int activeIndex;
664 |
665 | if (mStackMode != ITEMS_SLIDE_UP) {
666 | activeIndex = (swipeGestureTypeY == GESTURE_SLIDE_DOWN) ? 0 : 1;
667 | } else {
668 | activeIndex = (swipeGestureTypeY == GESTURE_SLIDE_DOWN) ? 1 : 0;
669 | }
670 |
671 | boolean endOfStack = mLoopViews && adapterCount == 1 &&
672 | ((mStackMode == ITEMS_SLIDE_UP && swipeGestureTypeY == GESTURE_SLIDE_UP) ||
673 | (mStackMode == ITEMS_SLIDE_DOWN && swipeGestureTypeY == GESTURE_SLIDE_DOWN));
674 |
675 |
676 | boolean beginningOfStack = mLoopViews && adapterCount == 1 &&
677 | ((mStackMode == ITEMS_SLIDE_DOWN && swipeGestureTypeY == GESTURE_SLIDE_UP) ||
678 | (mStackMode == ITEMS_SLIDE_UP && swipeGestureTypeY == GESTURE_SLIDE_DOWN));
679 |
680 |
681 | int stackMode;
682 | if (mLoopViews && !beginningOfStack && !endOfStack) {
683 | stackMode = StackSlider.NORMAL_MODE;
684 | } else if (mCurrentWindowStartUnbounded + activeIndex == -1 || beginningOfStack) {
685 |
686 | activeIndex++;
687 | stackMode = StackSlider.BEGINNING_OF_STACK_MODE;
688 | } else if (mCurrentWindowStartUnbounded + activeIndex == adapterCount - 1 || endOfStack) {
689 | stackMode = StackSlider.END_OF_STACK_MODE;
690 | } else {
691 | stackMode = StackSlider.NORMAL_MODE;
692 | }
693 |
694 | mTransitionIsSetup = stackMode == StackSlider.NORMAL_MODE;
695 |
696 | View v = getViewAtRelativeIndex(activeIndex);
697 | if (v == null) return;
698 |
699 | setupStackSlider(v, stackMode);
700 |
701 | // We only register this gesture if we've made it this far without a problem
702 | mSwipeGestureType = swipeGestureTypeY;
703 | cancelHandleClick();
704 | }
705 | }
706 |
707 |
708 | /**
709 | * {@inheritDoc}
710 | */
711 | @Override
712 | public boolean onTouchEvent(MotionEvent ev) {
713 | super.onTouchEvent(ev);
714 |
715 | int action = ev.getAction();
716 | int pointerIndex = ev.findPointerIndex(mActivePointerId);
717 | if (pointerIndex == INVALID_POINTER) {
718 | // no data for our primary pointer, this shouldn't happen, log it
719 | Log.d(TAG, "Error: No data for our primary pointer.");
720 | return false;
721 | }
722 |
723 | float newY = ev.getY(pointerIndex);
724 | float newX = ev.getX(pointerIndex);
725 | float deltaY = newY - mInitialY;
726 | float deltaX = newX - mInitialX;
727 | if (mVelocityTracker == null) {
728 | mVelocityTracker = VelocityTracker.obtain();
729 | }
730 | mVelocityTracker.addMovement(ev);
731 |
732 | switch (action & MotionEvent.ACTION_MASK) {
733 | case MotionEvent.ACTION_MOVE: {
734 | beginGestureIfNeeded(deltaX);
735 |
736 | float rx = deltaX / (mSlideAmount * 1.0f);
737 |
738 | if (mSwipeGestureType == GESTURE_SLIDE_DOWN) {
739 | float r = (deltaX - mTouchSlop * 1.0f) / mSlideAmount * 1.0f;
740 |
741 | if (mStackMode == ITEMS_SLIDE_DOWN) r = 1 - r;
742 | mStackSlider.setYProgress(r);
743 | // mStackSlider.setXProgress(rx);
744 | // mStackSlider.setYProgress(rx);
745 | // mStackSlider.setXProgress(1 - r);
746 | return true;
747 | } else if (mSwipeGestureType == GESTURE_SLIDE_UP) {
748 | float r = -(deltaX + mTouchSlop * 1.0f) / mSlideAmount * 1.0f;
749 | if (mStackMode == ITEMS_SLIDE_DOWN) r = 1 - r;
750 | mStackSlider.setYProgress(1 - r);
751 | // mStackSlider.setXProgress(rx);
752 | return true;
753 | }
754 | break;
755 | }
756 | case MotionEvent.ACTION_UP: {
757 | handlePointerUp(ev);
758 | break;
759 | }
760 | case MotionEvent.ACTION_POINTER_UP: {
761 | // onSecondaryPointerUp(ev);
762 | break;
763 | }
764 | case MotionEvent.ACTION_CANCEL: {
765 | mActivePointerId = INVALID_POINTER;
766 | mSwipeGestureType = GESTURE_NONE;
767 | break;
768 | }
769 | }
770 | return true;
771 | }
772 |
773 | private void onSecondaryPointerUp(MotionEvent ev) {
774 | final int activePointerIndex = ev.getActionIndex();
775 | final int pointerId = ev.getPointerId(activePointerIndex);
776 | if (pointerId == mActivePointerId) {
777 |
778 | int activeViewIndex = (mSwipeGestureType == GESTURE_SLIDE_DOWN) ? 0 : 1;
779 |
780 | View v = getViewAtRelativeIndex(activeViewIndex);
781 | if (v == null) return;
782 |
783 | // Our primary pointer has gone up -- let's see if we can find
784 | // another pointer on the view. If so, then we should replace
785 | // our primary pointer with this new pointer and adjust things
786 | // so that the view doesn't jump
787 | for (int index = 0; index < ev.getPointerCount(); index++) {
788 | if (index != activePointerIndex) {
789 |
790 | float x = ev.getX(index);
791 | float y = ev.getY(index);
792 |
793 | mTouchRect.set(v.getLeft(), v.getTop(), v.getRight(), v.getBottom());
794 | if (mTouchRect.contains(Math.round(x), Math.round(y))) {
795 | float oldX = ev.getX(activePointerIndex);
796 | float oldY = ev.getY(activePointerIndex);
797 |
798 | // adjust our frame of reference to avoid a jump
799 | mInitialY += (y - oldY);
800 | mInitialX += (x - oldX);
801 |
802 | mActivePointerId = ev.getPointerId(index);
803 | if (mVelocityTracker != null) {
804 | mVelocityTracker.clear();
805 | }
806 | // ok, we're good, we found a new pointer which is touching the active view
807 | return;
808 | }
809 | }
810 | }
811 | // if we made it this far, it means we didn't find a satisfactory new pointer :(,
812 | // so end the gesture
813 | handlePointerUp(ev);
814 | }
815 | }
816 |
817 | private void handlePointerUp(MotionEvent ev) {
818 | int pointerIndex = ev.findPointerIndex(mActivePointerId);
819 | float newX = ev.getX(pointerIndex);
820 | int deltaX = (int) (newX - mInitialY);
821 | mLastInteractionTime = System.currentTimeMillis();
822 |
823 | if (mVelocityTracker != null) {
824 | mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
825 | mYVelocity = (int) mVelocityTracker.getYVelocity(mActivePointerId);
826 | }
827 |
828 | if (mVelocityTracker != null) {
829 | mVelocityTracker.recycle();
830 | mVelocityTracker = null;
831 | }
832 |
833 |
834 | Log.d("==", "mSwipeGestureType :: " + mSwipeGestureType);
835 |
836 | if (deltaX < mSwipeThreshold && mSwipeGestureType == GESTURE_SLIDE_UP
837 | && mStackSlider.mMode == StackSlider.NORMAL_MODE) {
838 | Log.d("===", "1");
839 | // We reset the gesture variable, because otherwise we will ignore showPrevious() /
840 | // showNext();
841 | mSwipeGestureType = GESTURE_NONE;
842 |
843 | // Swipe threshold exceeded, swipe down
844 | if (mStackMode == ITEMS_SLIDE_UP) {
845 | showPrevious();
846 | } else {
847 | showNext();
848 | }
849 | mHighlight.bringToFront();
850 | } else if (deltaX > mSwipeThreshold && mSwipeGestureType == GESTURE_SLIDE_DOWN
851 | && mStackSlider.mMode == StackSlider.NORMAL_MODE) {
852 | //
853 | Log.d("===", "2");
854 | // We reset the gesture variable, because otherwise we will ignore showPrevious() /
855 | // showNext();
856 | mSwipeGestureType = GESTURE_NONE;
857 |
858 | // Swipe threshold exceeded, swipe up
859 | if (mStackMode == ITEMS_SLIDE_UP) {
860 | showNext();
861 | } else {
862 | showPrevious();
863 | }
864 |
865 | mHighlight.bringToFront();
866 | } else if (mSwipeGestureType == GESTURE_SLIDE_DOWN) {
867 | Log.d("===", "3");
868 | // Didn't swipe up far enough, snap back down
869 | int duration;
870 | float finalYProgress = (mStackMode == ITEMS_SLIDE_DOWN) ? 1 : 0;
871 | if (mStackMode == ITEMS_SLIDE_UP || mStackSlider.mMode != StackSlider.NORMAL_MODE) {
872 | duration = Math.round(mStackSlider.getDurationForNeutralPosition());
873 | } else {
874 | duration = Math.round(mStackSlider.getDurationForOffscreenPosition());
875 | }
876 |
877 | StackSlider animationSlider = new StackSlider(mStackSlider);
878 | PropertyValuesHolder snapBackY = PropertyValuesHolder.ofFloat("YProgress", finalYProgress);
879 | PropertyValuesHolder snapBackX = PropertyValuesHolder.ofFloat("XProgress", 0.0f);
880 | ObjectAnimator pa = ObjectAnimator.ofPropertyValuesHolder(animationSlider,
881 | snapBackX, snapBackY);
882 | pa.setDuration(duration);
883 | pa.setInterpolator(new LinearInterpolator());
884 | pa.start();
885 | } else if (mSwipeGestureType == GESTURE_SLIDE_UP) {
886 | Log.d("===", "4");
887 | // Didn't swipe down far enough, snap back up
888 | float finalYProgress = (mStackMode == ITEMS_SLIDE_DOWN) ? 0 : 1;
889 | int duration;
890 | if (mStackMode == ITEMS_SLIDE_DOWN || mStackSlider.mMode != StackSlider.NORMAL_MODE) {
891 | duration = Math.round(mStackSlider.getDurationForOffscreenPosition());
892 | } else {
893 | duration = Math.round(mStackSlider.getDurationForNeutralPosition());
894 |
895 | }
896 |
897 | StackSlider animationSlider = new StackSlider(mStackSlider);
898 | PropertyValuesHolder snapBackY =
899 | PropertyValuesHolder.ofFloat("YProgress", finalYProgress);
900 | PropertyValuesHolder snapBackX = PropertyValuesHolder.ofFloat("XProgress", 0.0f);
901 | ObjectAnimator pa = ObjectAnimator.ofPropertyValuesHolder(animationSlider,
902 | snapBackX, snapBackY);
903 | pa.setDuration(duration);
904 | pa.start();
905 | }
906 |
907 | mActivePointerId = INVALID_POINTER;
908 | mSwipeGestureType = GESTURE_NONE;
909 | }
910 |
911 | private class StackSlider {
912 | View mView;
913 | float mYProgress;
914 | float mXProgress;
915 |
916 | static final int NORMAL_MODE = 0;
917 | static final int BEGINNING_OF_STACK_MODE = 1;
918 | static final int END_OF_STACK_MODE = 2;
919 |
920 | int mMode = NORMAL_MODE;
921 |
922 | public StackSlider() {
923 | }
924 |
925 | public StackSlider(StackSlider copy) {
926 | mView = copy.mView;
927 | mYProgress = copy.mYProgress;
928 | mXProgress = copy.mXProgress;
929 | mMode = copy.mMode;
930 | }
931 |
932 | private float cubic(float r) {
933 | return (float) (Math.pow(2 * r - 1, 3) + 1) / 2.0f;
934 | }
935 |
936 | private float highlightAlphaInterpolator(float r) {
937 | float pivot = 0.3f;
938 | if (r < pivot) {
939 | return 0.45f * cubic(r / pivot);
940 | } else {
941 | return 0.45f * cubic(1 - (r - pivot) / (1 - pivot));
942 | }
943 | }
944 |
945 | private float viewAlphaInterpolator(float r) {
946 | float pivot = 0.01f;
947 | if (r > pivot) {
948 | return (r - pivot) / (1 - pivot);
949 | } else {
950 | return 0;
951 | }
952 | }
953 |
954 | private float rotationInterpolator(float r) {
955 | float pivot = 0.2f;
956 | if (r < pivot) {
957 | return 0;
958 | } else {
959 | return (r - pivot) / (1 - pivot);
960 | }
961 | }
962 |
963 | void setView(View v) {
964 | mView = v;
965 | }
966 |
967 | public void setYProgress(float r) {
968 |
969 | // enforce r between 0 and 1
970 | r = Math.min(1.0f, r);
971 | r = Math.max(0, r);
972 |
973 | mYProgress = r;
974 | if (mView == null) return;
975 |
976 | final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams();
977 | final LayoutParams highlightLp = (LayoutParams) mHighlight.getLayoutParams();
978 |
979 | int stackDirection = (mStackMode == ITEMS_SLIDE_UP) ? -1 : 1;
980 |
981 | // We need to prevent any clipping issues which may arise by setting a layer type.
982 | // This doesn't come for free however, so we only want to enable it when required.
983 | if (Float.compare(0f, mYProgress) != 0 && Float.compare(1.0f, mYProgress) != 0) {
984 | if (mView.getLayerType() == LAYER_TYPE_NONE) {
985 | mView.setLayerType(LAYER_TYPE_HARDWARE, null);
986 | }
987 | } else {
988 | if (mView.getLayerType() != LAYER_TYPE_NONE) {
989 | mView.setLayerType(LAYER_TYPE_NONE, null);
990 | }
991 | }
992 |
993 | switch (mMode) {
994 | case NORMAL_MODE:
995 | // viewLp.setVerticalOffset(Math.round(-r * stackDirection * mSlideAmount));
996 | // highlightLp.setVerticalOffset(Math.round(-r * stackDirection * mSlideAmount));
997 | viewLp.setHorizontalOffset(Math.round(-r * stackDirection * mSlideAmount));
998 | highlightLp.setHorizontalOffset(Math.round(-r * stackDirection * mSlideAmount));
999 | mHighlight.setAlpha(highlightAlphaInterpolator(r));
1000 |
1001 | float alpha = viewAlphaInterpolator(1 - r);
1002 |
1003 | // We make sure that views which can't be seen (have 0 alpha) are also invisible
1004 | // so that they don't interfere with click events.
1005 | if (mView.getAlpha() == 0 && alpha != 0 && mView.getVisibility() != VISIBLE) {
1006 | mView.setVisibility(VISIBLE);
1007 | } else if (alpha == 0 && mView.getAlpha() != 0
1008 | && mView.getVisibility() == VISIBLE) {
1009 | mView.setVisibility(INVISIBLE);
1010 | }
1011 |
1012 | mView.setAlpha(alpha);
1013 | // mView.setRotationX(stackDirection * 90.0f * rotationInterpolator(r));
1014 | // mHighlight.setRotationX(stackDirection * 90.0f * rotationInterpolator(r));
1015 | break;
1016 | case END_OF_STACK_MODE:
1017 | r = r * 0.2f;
1018 | viewLp.setHorizontalOffset(Math.round(-stackDirection * r * mSlideAmount));
1019 | highlightLp.setHorizontalOffset(Math.round(-stackDirection * r * mSlideAmount));
1020 | mHighlight.setAlpha(highlightAlphaInterpolator(r));
1021 | break;
1022 | case BEGINNING_OF_STACK_MODE:
1023 | r = (1 - r) * 0.2f;
1024 | viewLp.setHorizontalOffset(Math.round(stackDirection * r * mSlideAmount));
1025 | highlightLp.setHorizontalOffset(Math.round(stackDirection * r * mSlideAmount));
1026 | mHighlight.setAlpha(highlightAlphaInterpolator(r));
1027 | break;
1028 | }
1029 | }
1030 |
1031 | public void setXProgress(float r) {
1032 | // enforce r between 0 and 1
1033 | r = Math.min(2.0f, r);
1034 | r = Math.max(-2.0f, r);
1035 |
1036 | mXProgress = r;
1037 |
1038 | if (mView == null) return;
1039 | final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams();
1040 | final LayoutParams highlightLp = (LayoutParams) mHighlight.getLayoutParams();
1041 |
1042 |
1043 | int stackDirection = (mStackMode == ITEMS_SLIDE_UP) ? 1 : -1;
1044 | if (Float.compare(0f, mYProgress) != 0 && Float.compare(1.0f, mYProgress) != 0) {
1045 | if (mView.getLayerType() == LAYER_TYPE_NONE) {
1046 | mView.setLayerType(LAYER_TYPE_HARDWARE, null);
1047 | }
1048 | } else {
1049 | if (mView.getLayerType() != LAYER_TYPE_NONE) {
1050 | mView.setLayerType(LAYER_TYPE_NONE, null);
1051 | }
1052 | }
1053 |
1054 | switch (mMode) {
1055 | case NORMAL_MODE:
1056 | case END_OF_STACK_MODE:
1057 | case BEGINNING_OF_STACK_MODE:
1058 | }
1059 |
1060 |
1061 | r *= 0.2f;
1062 | viewLp.setVerticalOffset(Math.round(r * mSlideAmount));
1063 | highlightLp.setVerticalOffset(Math.round(r * mSlideAmount));
1064 | }
1065 |
1066 | void setMode(int mode) {
1067 | mMode = mode;
1068 | }
1069 |
1070 | float getDurationForNeutralPosition() {
1071 | return getDuration(false, 0);
1072 | }
1073 |
1074 | float getDurationForOffscreenPosition() {
1075 | return getDuration(true, 0);
1076 | }
1077 |
1078 | float getDurationForNeutralPosition(float velocity) {
1079 | return getDuration(false, velocity);
1080 | }
1081 |
1082 | float getDurationForOffscreenPosition(float velocity) {
1083 | return getDuration(true, velocity);
1084 | }
1085 |
1086 | private float getDuration(boolean invert, float velocity) {
1087 | if (mView != null) {
1088 | final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams();
1089 |
1090 | float d = (float) Math.sqrt(Math.pow(viewLp.horizontalOffset, 2) +
1091 | Math.pow(viewLp.verticalOffset, 2));
1092 | float maxd = (float) Math.sqrt(Math.pow(mSlideAmount, 2) +
1093 | Math.pow(0.4f * mSlideAmount, 2));
1094 |
1095 | if (velocity == 0) {
1096 | return (invert ? (1 - d / maxd) : d / maxd) * DEFAULT_ANIMATION_DURATION;
1097 | } else {
1098 | float duration = invert ? d / Math.abs(velocity) :
1099 | (maxd - d) / Math.abs(velocity);
1100 | if (duration < MINIMUM_ANIMATION_DURATION ||
1101 | duration > DEFAULT_ANIMATION_DURATION) {
1102 | return getDuration(invert, 0);
1103 | } else {
1104 | return duration;
1105 | }
1106 | }
1107 | }
1108 | return 0;
1109 | }
1110 |
1111 | // Used for animations
1112 | @SuppressWarnings({"UnusedDeclaration"})
1113 | public float getYProgress() {
1114 | return mYProgress;
1115 | }
1116 |
1117 | // Used for animations
1118 | @SuppressWarnings({"UnusedDeclaration"})
1119 | public float getXProgress() {
1120 | return mXProgress;
1121 | }
1122 | }
1123 |
1124 | LayoutParams createOrReuseLayoutParams(View v) {
1125 | final ViewGroup.LayoutParams currentLp = v.getLayoutParams();
1126 | if (currentLp instanceof LayoutParams) {
1127 | LayoutParams lp = (LayoutParams) currentLp;
1128 | lp.setHorizontalOffset(0);
1129 | lp.setVerticalOffset(0);
1130 | lp.width = 0;
1131 | lp.width = 0;
1132 | return lp;
1133 | }
1134 | return new LayoutParams(v);
1135 | }
1136 |
1137 | @Override
1138 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
1139 | checkForAndHandleDataChanged();
1140 |
1141 | final int childCount = getChildCount();
1142 | for (int i = 0; i < childCount; i++) {
1143 | final View child = getChildAt(i);
1144 |
1145 | int childRight = getPaddingLeft() + child.getMeasuredWidth();
1146 | int childBottom = getPaddingTop() + child.getMeasuredHeight();
1147 | LayoutParams lp = (LayoutParams) child.getLayoutParams();
1148 |
1149 | child.layout(getPaddingLeft() + lp.horizontalOffset, getPaddingTop() + lp.verticalOffset,
1150 | childRight + lp.horizontalOffset, childBottom + lp.verticalOffset);
1151 |
1152 | }
1153 | onLayoutView();
1154 | }
1155 |
1156 | @Override
1157 | public void advance() {
1158 | long timeSinceLastInteraction = System.currentTimeMillis() - mLastInteractionTime;
1159 |
1160 | if (mAdapter == null) return;
1161 | final int adapterCount = getCount();
1162 | if (adapterCount == 1 && mLoopViews) return;
1163 |
1164 | if (mSwipeGestureType == GESTURE_NONE &&
1165 | timeSinceLastInteraction > MIN_TIME_BETWEEN_INTERACTION_AND_AUTOADVANCE) {
1166 | showNext();
1167 | }
1168 | }
1169 |
1170 | private void measureChildren() {
1171 | final int count = getChildCount();
1172 |
1173 | final int measuredWidth = getMeasuredWidth();
1174 | final int measuredHeight = getMeasuredHeight();
1175 |
1176 | final int childWidth = Math.round(measuredWidth * (1 - PERSPECTIVE_SHIFT_FACTOR_X))
1177 | - getPaddingLeft() - getPaddingRight();
1178 | final int childHeight = Math.round(measuredHeight * (1 - PERSPECTIVE_SHIFT_FACTOR_Y))
1179 | - getPaddingTop() - getPaddingBottom();
1180 |
1181 | int maxWidth = 0;
1182 | int maxHeight = 0;
1183 |
1184 | for (int i = 0; i < count; i++) {
1185 | final View child = getChildAt(i);
1186 | child.measure(View.MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY),
1187 | View.MeasureSpec.makeMeasureSpec(childHeight, View.MeasureSpec.EXACTLY));
1188 |
1189 | if (child != mHighlight && child != mClickFeedback) {
1190 | final int childMeasuredWidth = child.getMeasuredWidth();
1191 | final int childMeasuredHeight = child.getMeasuredHeight();
1192 | if (childMeasuredWidth > maxWidth) {
1193 | maxWidth = childMeasuredWidth;
1194 | }
1195 | if (childMeasuredHeight > maxHeight) {
1196 | maxHeight = childMeasuredHeight;
1197 | }
1198 | }
1199 | }
1200 |
1201 | mNewPerspectiveShiftX = PERSPECTIVE_SHIFT_FACTOR_X * measuredWidth;
1202 | mNewPerspectiveShiftY = PERSPECTIVE_SHIFT_FACTOR_Y * measuredHeight;
1203 |
1204 | // If we have extra space, we try and spread the items out
1205 | if (maxWidth > 0 && count > 0 && maxWidth < childWidth) {
1206 | mNewPerspectiveShiftX = measuredWidth - maxWidth;
1207 | }
1208 |
1209 | if (maxHeight > 0 && count > 0 && maxHeight < childHeight) {
1210 | mNewPerspectiveShiftY = measuredHeight - maxHeight;
1211 | }
1212 | }
1213 |
1214 |
1215 | @Override
1216 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1217 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
1218 | int widthSpecSize = View.MeasureSpec.getSize(widthMeasureSpec);
1219 | ;
1220 | int heightInPX = DISPLAY_METRICS.heightPixels / 3;
1221 | int heightSpecSize = MeasureSpec.makeMeasureSpec(heightInPX, MeasureSpec.AT_MOST);
1222 | final int widthSpecMode = View.MeasureSpec.getMode(widthMeasureSpec);
1223 | final int heightSpecMode = MeasureSpec.makeMeasureSpec(heightInPX, MeasureSpec.AT_MOST);
1224 | boolean haveChildRefSize = (mReferenceChildWidth != -1 && mReferenceChildHeight != -1);
1225 |
1226 | // We need to deal with the case where our parent hasn't told us how
1227 | // big we should be. In this case we should
1228 | float factorY = 1 / (1 - PERSPECTIVE_SHIFT_FACTOR_Y);
1229 | if (heightSpecMode == View.MeasureSpec.UNSPECIFIED) {
1230 | heightSpecSize = haveChildRefSize ?
1231 | Math.round(mReferenceChildHeight * (1 + factorY)) +
1232 | getPaddingTop() + getPaddingBottom() : 0;
1233 | } else if (heightSpecMode == View.MeasureSpec.AT_MOST) {
1234 | if (haveChildRefSize) {
1235 | int height = Math.round(mReferenceChildHeight * (1 + factorY))
1236 | + getPaddingTop() + getPaddingBottom();
1237 | if (height <= heightSpecSize) {
1238 | heightSpecSize = height;
1239 | } else {
1240 | heightSpecSize |= View.MEASURED_STATE_TOO_SMALL;
1241 |
1242 | }
1243 | } else {
1244 | heightSpecSize = 0;
1245 | }
1246 | }
1247 |
1248 | float factorX = 1 / (1 - PERSPECTIVE_SHIFT_FACTOR_X);
1249 | if (widthSpecMode == View.MeasureSpec.UNSPECIFIED) {
1250 |
1251 | widthSpecSize = haveChildRefSize ?
1252 | Math.round(mReferenceChildWidth * (1 + factorX)) +
1253 | getPaddingLeft() + getPaddingRight() : 0;
1254 | } else if (heightSpecMode == View.MeasureSpec.AT_MOST) {
1255 |
1256 | if (haveChildRefSize) {
1257 | int width = mReferenceChildWidth + getPaddingLeft() + getPaddingRight();
1258 | if (width <= widthSpecSize) {
1259 | widthSpecSize = width;
1260 | } else {
1261 | widthSpecSize |= MEASURED_STATE_TOO_SMALL;
1262 | }
1263 | } else {
1264 | widthSpecSize = 0;
1265 | }
1266 | }
1267 |
1268 | setMeasuredDimension(widthSpecSize, heightSpecSize);
1269 | measureChildren();
1270 | }
1271 |
1272 | class LayoutParams extends ViewGroup.LayoutParams {
1273 | int horizontalOffset;
1274 | int verticalOffset;
1275 | View mView;
1276 | private final Rect parentRect = new Rect();
1277 | private final Rect invalidateRect = new Rect();
1278 | private final RectF invalidateRectf = new RectF();
1279 | private final Rect globalInvalidateRect = new Rect();
1280 |
1281 | LayoutParams(View view) {
1282 | super(0, 0);
1283 | width = 0;
1284 | height = 0;
1285 | horizontalOffset = 0;
1286 | verticalOffset = 0;
1287 | mView = view;
1288 | }
1289 |
1290 | LayoutParams(Context c, AttributeSet attrs) {
1291 | super(c, attrs);
1292 | horizontalOffset = 0;
1293 | verticalOffset = 0;
1294 | width = 0;
1295 | height = 0;
1296 | }
1297 |
1298 | void invalidateGlobalRegion(View v, Rect r) {
1299 | // We need to make a new rect here, so as not to modify the one passed
1300 | globalInvalidateRect.set(r);
1301 | globalInvalidateRect.union(0, 0, getWidth(), getHeight());
1302 | View p = v;
1303 | if (!(v.getParent() != null && v.getParent() instanceof View)) return;
1304 |
1305 | boolean firstPass = true;
1306 | parentRect.set(0, 0, 0, 0);
1307 | while (p.getParent() != null && p.getParent() instanceof View
1308 | && !parentRect.contains(globalInvalidateRect)) {
1309 | if (!firstPass) {
1310 | globalInvalidateRect.offset(p.getLeft() - p.getScrollX(), p.getTop()
1311 | - p.getScrollY());
1312 | }
1313 | firstPass = false;
1314 | p = (View) p.getParent();
1315 | parentRect.set(p.getScrollX(), p.getScrollY(),
1316 | p.getWidth() + p.getScrollX(), p.getHeight() + p.getScrollY());
1317 | p.invalidate(globalInvalidateRect.left, globalInvalidateRect.top,
1318 | globalInvalidateRect.right, globalInvalidateRect.bottom);
1319 | }
1320 |
1321 | p.invalidate(globalInvalidateRect.left, globalInvalidateRect.top,
1322 | globalInvalidateRect.right, globalInvalidateRect.bottom);
1323 | }
1324 |
1325 | Rect getInvalidateRect() {
1326 | return invalidateRect;
1327 | }
1328 |
1329 | void resetInvalidateRect() {
1330 | invalidateRect.set(0, 0, 0, 0);
1331 | }
1332 |
1333 | // This is public so that ObjectAnimator can access it
1334 | public void setVerticalOffset(int newVerticalOffset) {
1335 | setOffsets(horizontalOffset, newVerticalOffset);
1336 | }
1337 |
1338 | public void setHorizontalOffset(int newHorizontalOffset) {
1339 | setOffsets(newHorizontalOffset, verticalOffset);
1340 | }
1341 |
1342 | public void setOffsets(int newHorizontalOffset, int newVerticalOffset) {
1343 | int horizontalOffsetDelta = newHorizontalOffset - horizontalOffset;
1344 | horizontalOffset = newHorizontalOffset;
1345 | int verticalOffsetDelta = newVerticalOffset - verticalOffset;
1346 | verticalOffset = newVerticalOffset;
1347 |
1348 | if (mView != null) {
1349 | mView.requestLayout();
1350 | int left = Math.min(mView.getLeft() + horizontalOffsetDelta, mView.getLeft());
1351 | int right = Math.max(mView.getRight() + horizontalOffsetDelta, mView.getRight());
1352 | int top = Math.min(mView.getTop() + verticalOffsetDelta, mView.getTop());
1353 | int bottom = Math.max(mView.getBottom() + verticalOffsetDelta, mView.getBottom());
1354 |
1355 | invalidateRectf.set(left, top, right, bottom);
1356 |
1357 | float xoffset = -invalidateRectf.left;
1358 | float yoffset = -invalidateRectf.top;
1359 | invalidateRectf.offset(xoffset, yoffset);
1360 | mView.getMatrix().mapRect(invalidateRectf);
1361 | invalidateRectf.offset(-xoffset, -yoffset);
1362 |
1363 | invalidateRect.set((int) Math.floor(invalidateRectf.left),
1364 | (int) Math.floor(invalidateRectf.top),
1365 | (int) Math.ceil(invalidateRectf.right),
1366 | (int) Math.ceil(invalidateRectf.bottom));
1367 |
1368 | invalidateGlobalRegion(mView, invalidateRect);
1369 | }
1370 | }
1371 | }
1372 |
1373 | private static class HolographicHelper {
1374 | private final Paint mHolographicPaint = new Paint();
1375 | private final Paint mErasePaint = new Paint();
1376 | private final Paint mBlurPaint = new Paint();
1377 | private static final int RES_OUT = 0;
1378 | private static final int CLICK_FEEDBACK = 1;
1379 | private float mDensity;
1380 | private BlurMaskFilter mSmallBlurMaskFilter;
1381 | private BlurMaskFilter mLargeBlurMaskFilter;
1382 | private final Canvas mCanvas = new Canvas();
1383 | private final Canvas mMaskCanvas = new Canvas();
1384 | private final int[] mTmpXY = new int[2];
1385 | private final Matrix mIdentityMatrix = new Matrix();
1386 |
1387 | HolographicHelper(Context context) {
1388 | mDensity = context.getResources().getDisplayMetrics().density;
1389 |
1390 | mHolographicPaint.setFilterBitmap(true);
1391 | // mHolographicPaint.setMaskFilter(TableMaskFilter.CreateClipTable(0, 30));
1392 | mErasePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
1393 | mErasePaint.setFilterBitmap(true);
1394 |
1395 | mSmallBlurMaskFilter = new BlurMaskFilter(2 * mDensity, BlurMaskFilter.Blur.NORMAL);
1396 | mLargeBlurMaskFilter = new BlurMaskFilter(4 * mDensity, BlurMaskFilter.Blur.NORMAL);
1397 | }
1398 |
1399 | Bitmap createClickOutline(View v, int color) {
1400 | return createOutline(v, CLICK_FEEDBACK, color);
1401 | }
1402 |
1403 | Bitmap createResOutline(View v, int color) {
1404 | return createOutline(v, RES_OUT, color);
1405 | }
1406 |
1407 | Bitmap createOutline(View v, int type, int color) {
1408 | mHolographicPaint.setColor(color);
1409 | if (type == RES_OUT) {
1410 | mBlurPaint.setMaskFilter(mSmallBlurMaskFilter);
1411 | } else if (type == CLICK_FEEDBACK) {
1412 | mBlurPaint.setMaskFilter(mLargeBlurMaskFilter);
1413 | }
1414 |
1415 | if (v.getMeasuredWidth() == 0 || v.getMeasuredHeight() == 0) {
1416 | return null;
1417 | }
1418 |
1419 | Bitmap bitmap = Bitmap.createBitmap(v.getMeasuredWidth(), v.getMeasuredHeight(),
1420 | Bitmap.Config.ARGB_8888);
1421 | mCanvas.setBitmap(bitmap);
1422 |
1423 | float rotationX = v.getRotationX();
1424 | float rotation = v.getRotation();
1425 | float translationY = v.getTranslationY();
1426 | float translationX = v.getTranslationX();
1427 | v.setRotationX(0);
1428 | // v.setRotation(0);
1429 | v.setTranslationY(0);
1430 | v.setTranslationX(0);
1431 | v.draw(mCanvas);
1432 | v.setRotationX(rotationX);
1433 | // v.setRotation(rotation);
1434 | v.setTranslationY(translationY);
1435 | v.setTranslationX(translationX);
1436 |
1437 | drawOutline(mCanvas, bitmap);
1438 | mCanvas.setBitmap(null);
1439 | return bitmap;
1440 | }
1441 |
1442 | void drawOutline(Canvas dest, Bitmap src) {
1443 | final int[] xy = mTmpXY;
1444 | Bitmap mask = src.extractAlpha(mBlurPaint, xy);
1445 | mMaskCanvas.setBitmap(mask);
1446 | mMaskCanvas.drawBitmap(src, -xy[0], -xy[1], mErasePaint);
1447 | dest.drawColor(0, PorterDuff.Mode.CLEAR);
1448 | dest.setMatrix(mIdentityMatrix);
1449 | dest.drawBitmap(mask, xy[0], xy[1], mHolographicPaint);
1450 | mMaskCanvas.setBitmap(null);
1451 | mask.recycle();
1452 | }
1453 | }
1454 | }
--------------------------------------------------------------------------------