• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.launcher3.popup;
18 
19 import static com.android.app.animation.Interpolators.EMPHASIZED_ACCELERATE;
20 import static com.android.app.animation.Interpolators.EMPHASIZED_DECELERATE;
21 import static com.android.app.animation.Interpolators.LINEAR;
22 
23 import android.animation.Animator;
24 import android.animation.AnimatorListenerAdapter;
25 import android.animation.AnimatorSet;
26 import android.animation.ObjectAnimator;
27 import android.animation.ValueAnimator;
28 import android.content.Context;
29 import android.content.res.Resources;
30 import android.graphics.Color;
31 import android.graphics.Rect;
32 import android.graphics.drawable.ColorDrawable;
33 import android.graphics.drawable.Drawable;
34 import android.graphics.drawable.GradientDrawable;
35 import android.util.AttributeSet;
36 import android.util.Pair;
37 import android.util.Property;
38 import android.view.Gravity;
39 import android.view.LayoutInflater;
40 import android.view.View;
41 import android.view.ViewGroup;
42 import android.view.animation.Interpolator;
43 import android.view.animation.PathInterpolator;
44 import android.widget.FrameLayout;
45 
46 import com.android.launcher3.AbstractFloatingView;
47 import com.android.launcher3.InsettableFrameLayout;
48 import com.android.launcher3.R;
49 import com.android.launcher3.Utilities;
50 import com.android.launcher3.dragndrop.DragLayer;
51 import com.android.launcher3.shortcuts.DeepShortcutView;
52 import com.android.launcher3.util.RunnableList;
53 import com.android.launcher3.util.Themes;
54 import com.android.launcher3.views.ActivityContext;
55 import com.android.launcher3.views.BaseDragLayer;
56 
57 /**
58  * A container for shortcuts to deep links and notifications associated with an app.
59  *
60  * @param <T> The activity on with the popup shows
61  */
62 public abstract class ArrowPopup<T extends Context & ActivityContext>
63         extends AbstractFloatingView {
64 
65     // Duration values (ms) for popup open and close animations.
66     protected int mOpenDuration = 276;
67     protected int mOpenFadeStartDelay = 0;
68     protected int mOpenFadeDuration = 38;
69     protected int mOpenChildFadeStartDelay = 38;
70     protected int mOpenChildFadeDuration = 76;
71 
72     protected int mCloseDuration = 200;
73     protected int mCloseFadeStartDelay = 140;
74     protected int mCloseFadeDuration = 50;
75     protected int mCloseChildFadeStartDelay = 0;
76     protected int mCloseChildFadeDuration = 140;
77 
78     private static final int OPEN_DURATION_U = 200;
79     private static final int OPEN_FADE_START_DELAY_U = 0;
80     private static final int OPEN_FADE_DURATION_U = 83;
81     private static final int OPEN_CHILD_FADE_START_DELAY_U = 0;
82     private static final int OPEN_CHILD_FADE_DURATION_U = 83;
83     private static final int OPEN_OVERSHOOT_DURATION_U = 200;
84 
85     private static final int CLOSE_DURATION_U  = 233;
86     private static final int CLOSE_FADE_START_DELAY_U = 150;
87     private static final int CLOSE_FADE_DURATION_U = 83;
88     private static final int CLOSE_CHILD_FADE_START_DELAY_U = 150;
89     private static final int CLOSE_CHILD_FADE_DURATION_U = 83;
90 
91     protected final Rect mTempRect = new Rect();
92 
93     protected final LayoutInflater mInflater;
94     protected final float mOutlineRadius;
95     protected final T mActivityContext;
96     protected final boolean mIsRtl;
97 
98     protected final int mArrowOffsetVertical;
99     protected final int mArrowOffsetHorizontal;
100     protected final int mArrowWidth;
101     protected final int mArrowHeight;
102     protected final int mArrowPointRadius;
103     protected final View mArrow;
104 
105     protected final int mChildContainerMargin;
106 
107     protected boolean mIsLeftAligned;
108     protected boolean mIsAboveIcon;
109     protected int mGravity;
110 
111     protected AnimatorSet mOpenCloseAnimator;
112     protected boolean mDeferContainerRemoval;
113     protected boolean shouldScaleArrow = false;
114     protected boolean mIsArrowRotated = false;
115 
116     private final GradientDrawable mRoundedTop;
117     private final GradientDrawable mRoundedBottom;
118 
119     private RunnableList mOnCloseCallbacks = new RunnableList();
120 
121     // The rect string of the view that the arrow is attached to, in screen reference frame.
122     protected int mArrowColor;
123 
124     protected final float mElevation;
125 
126     // Tag for Views that have children that will need to be iterated to add styling.
127     private final String mIterateChildrenTag;
128 
129     protected final int[] mColors;
130 
ArrowPopup(Context context, AttributeSet attrs, int defStyleAttr)131     public ArrowPopup(Context context, AttributeSet attrs, int defStyleAttr) {
132         super(context, attrs, defStyleAttr);
133         mInflater = LayoutInflater.from(context);
134         mOutlineRadius = Themes.getDialogCornerRadius(context);
135         mActivityContext = ActivityContext.lookupContext(context);
136         mIsRtl = Utilities.isRtl(getResources());
137         mElevation = getResources().getDimension(R.dimen.deep_shortcuts_elevation);
138 
139         // Initialize arrow view
140         final Resources resources = getResources();
141         mArrowColor = getContext().getColor(R.color.materialColorSurfaceContainer);
142         mChildContainerMargin = resources.getDimensionPixelSize(R.dimen.popup_margin);
143         mArrowWidth = resources.getDimensionPixelSize(R.dimen.popup_arrow_width);
144         mArrowHeight = resources.getDimensionPixelSize(R.dimen.popup_arrow_height);
145         mArrow = new View(context);
146         mArrow.setLayoutParams(new DragLayer.LayoutParams(mArrowWidth, mArrowHeight));
147         mArrowOffsetVertical = resources.getDimensionPixelSize(R.dimen.popup_arrow_vertical_offset);
148         mArrowOffsetHorizontal = resources.getDimensionPixelSize(
149                 R.dimen.popup_arrow_horizontal_center_offset) - (mArrowWidth / 2);
150         mArrowPointRadius = resources.getDimensionPixelSize(R.dimen.popup_arrow_corner_radius);
151 
152         int smallerRadius = resources.getDimensionPixelSize(R.dimen.popup_smaller_radius);
153         mRoundedTop = new GradientDrawable();
154         int popupPrimaryColor = Themes.getAttrColor(context, R.attr.popupColorPrimary);
155         mRoundedTop.setColor(popupPrimaryColor);
156         mRoundedTop.setCornerRadii(new float[]{mOutlineRadius, mOutlineRadius, mOutlineRadius,
157                 mOutlineRadius, smallerRadius, smallerRadius, smallerRadius, smallerRadius});
158 
159         mRoundedBottom = new GradientDrawable();
160         mRoundedBottom.setColor(popupPrimaryColor);
161         mRoundedBottom.setCornerRadii(new float[]{smallerRadius, smallerRadius, smallerRadius,
162                 smallerRadius, mOutlineRadius, mOutlineRadius, mOutlineRadius, mOutlineRadius});
163 
164         mIterateChildrenTag = getContext().getString(R.string.popup_container_iterate_children);
165 
166         if (mActivityContext.canUseMultipleShadesForPopup()) {
167             mColors = new int[]{
168                     getContext().getColor(R.color.popup_shade_first),
169                     getContext().getColor(R.color.popup_shade_second),
170                     getContext().getColor(R.color.popup_shade_third)
171             };
172         } else {
173             mColors = new int[]{getContext().getColor(R.color.materialColorSurfaceContainer)};
174         }
175     }
176 
ArrowPopup(Context context, AttributeSet attrs)177     public ArrowPopup(Context context, AttributeSet attrs) {
178         this(context, attrs, 0);
179     }
180 
ArrowPopup(Context context)181     public ArrowPopup(Context context) {
182         this(context, null, 0);
183     }
184 
185     @Override
handleClose(boolean animate)186     protected void handleClose(boolean animate) {
187         if (animate) {
188             animateClose();
189         } else {
190             closeComplete();
191         }
192     }
193 
194     /**
195      * Utility method for inflating and adding a view
196      */
inflateAndAdd(int resId, ViewGroup container)197     public <R extends View> R inflateAndAdd(int resId, ViewGroup container) {
198         View view = mInflater.inflate(resId, container, false);
199         container.addView(view);
200         return (R) view;
201     }
202 
203     /**
204      * Utility method for inflating and adding a view
205      */
inflateAndAdd(int resId, ViewGroup container, int index)206     public <R extends View> R inflateAndAdd(int resId, ViewGroup container, int index) {
207         View view = mInflater.inflate(resId, container, false);
208         container.addView(view, index);
209         return (R) view;
210     }
211 
212     /**
213      * Set the margins and radius of backgrounds after views are properly ordered.
214      */
assignMarginsAndBackgrounds(ViewGroup viewGroup)215     public void assignMarginsAndBackgrounds(ViewGroup viewGroup) {
216         assignMarginsAndBackgrounds(viewGroup, Color.TRANSPARENT);
217     }
218 
219     /**
220      * @param backgroundColor When Color.TRANSPARENT, we get color from {@link #mColors}.
221      *                        Otherwise, we will use this color for all child views.
222      */
assignMarginsAndBackgrounds(ViewGroup viewGroup, int backgroundColor)223     protected void assignMarginsAndBackgrounds(ViewGroup viewGroup, int backgroundColor) {
224         int[] colors = null;
225         if (backgroundColor == Color.TRANSPARENT) {
226             // Lazily get the colors so they match the current wallpaper colors.
227             colors = mColors;
228         }
229 
230         int count = viewGroup.getChildCount();
231         int totalVisibleShortcuts = 0;
232         for (int i = 0; i < count; i++) {
233             View view = viewGroup.getChildAt(i);
234             if (view.getVisibility() == VISIBLE && isShortcutOrWrapper(view)) {
235                 totalVisibleShortcuts++;
236             }
237         }
238 
239         int numVisibleShortcut = 0;
240         View lastView = null;
241         AnimatorSet colorAnimator = new AnimatorSet();
242         for (int i = 0; i < count; i++) {
243             View view = viewGroup.getChildAt(i);
244             if (view.getVisibility() == VISIBLE) {
245                 if (lastView != null && (isShortcutContainer(lastView))) {
246                     MarginLayoutParams mlp = (MarginLayoutParams) lastView.getLayoutParams();
247                     mlp.bottomMargin = mChildContainerMargin;
248                 }
249                 lastView = view;
250                 MarginLayoutParams mlp = (MarginLayoutParams) lastView.getLayoutParams();
251                 mlp.bottomMargin = 0;
252 
253                 if (colors != null && isShortcutContainer(view)) {
254                     setChildColor(view, colors[0], colorAnimator);
255                     mArrowColor = colors[0];
256                 }
257 
258                 if (view instanceof ViewGroup && isShortcutContainer(view)) {
259                     assignMarginsAndBackgrounds((ViewGroup) view, backgroundColor);
260                     continue;
261                 }
262 
263                 if (isShortcutOrWrapper(view)) {
264                     if (totalVisibleShortcuts == 1) {
265                         view.setBackgroundResource(R.drawable.single_item_primary);
266                     } else if (totalVisibleShortcuts > 1) {
267                         if (numVisibleShortcut == 0) {
268                             view.setBackground(mRoundedTop.getConstantState().newDrawable());
269                         } else if (numVisibleShortcut == (totalVisibleShortcuts - 1)) {
270                             view.setBackground(mRoundedBottom.getConstantState().newDrawable());
271                         } else {
272                             view.setBackgroundResource(R.drawable.middle_item_primary);
273                         }
274                         numVisibleShortcut++;
275                     }
276                 }
277 
278                 setChildColor(view, backgroundColor, colorAnimator);
279             }
280         }
281 
282         colorAnimator.setDuration(0).start();
283         measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
284     }
285 
286     /**
287      * Returns {@code true} if the child is a shortcut or wraps a shortcut.
288      */
isShortcutOrWrapper(View view)289     protected boolean isShortcutOrWrapper(View view) {
290         return view instanceof DeepShortcutView;
291     }
292 
293     /**
294      * Returns {@code true} if view is a layout container of shortcuts
295      */
isShortcutContainer(View view)296     boolean isShortcutContainer(View view) {
297         return mIterateChildrenTag.equals(view.getTag());
298     }
299 
300     /**
301      * Sets the background color of the child.
302      */
setChildColor(View view, int color, AnimatorSet animatorSetOut)303     protected void setChildColor(View view, int color, AnimatorSet animatorSetOut) {
304         Drawable bg = view.getBackground();
305         if (bg instanceof GradientDrawable) {
306             GradientDrawable gd = (GradientDrawable) bg.mutate();
307             int oldColor = ((GradientDrawable) bg).getColor().getDefaultColor();
308             animatorSetOut.play(ObjectAnimator.ofArgb(gd, "color", oldColor, color));
309         } else if (bg instanceof ColorDrawable) {
310             ColorDrawable cd = (ColorDrawable) bg.mutate();
311             int oldColor = ((ColorDrawable) bg).getColor();
312             animatorSetOut.play(ObjectAnimator.ofArgb(cd, "color", oldColor, color));
313         }
314     }
315 
316     /**
317      * Shows the popup at the desired location.
318      */
show()319     public void show() {
320         setupForDisplay();
321         assignMarginsAndBackgrounds(this);
322         if (shouldAddArrow()) {
323             addArrow();
324         }
325         animateOpen();
326     }
327 
setupForDisplay()328     protected void setupForDisplay() {
329         setVisibility(View.INVISIBLE);
330         mIsOpen = true;
331         getPopupContainer().addView(this);
332         orientAboutObject();
333     }
334 
getArrowLeft()335     private int getArrowLeft() {
336         if (mIsLeftAligned) {
337             return mArrowOffsetHorizontal;
338         }
339         return getMeasuredWidth() - mArrowOffsetHorizontal - mArrowWidth;
340     }
341 
342     /**
343      * @param show If true, shows arrow (when applicable), otherwise hides arrow.
344      */
showArrow(boolean show)345     public void showArrow(boolean show) {
346         mArrow.setVisibility(show && shouldAddArrow() ? VISIBLE : INVISIBLE);
347     }
348 
addArrow()349     protected void addArrow() {
350         getPopupContainer().addView(mArrow);
351         mArrow.setX(getX() + getArrowLeft());
352 
353         if (Gravity.isVertical(mGravity)) {
354             // This is only true if there wasn't room for the container next to the icon,
355             // so we centered it instead. In that case we don't want to showDefaultOptions the arrow.
356             mArrow.setVisibility(INVISIBLE);
357         } else {
358             updateArrowColor();
359         }
360 
361         mArrow.setPivotX(mArrowWidth / 2.0f);
362         mArrow.setPivotY(mIsAboveIcon ? mArrowHeight : 0);
363     }
364 
updateArrowColor()365     protected void updateArrowColor() {
366         if (!Gravity.isVertical(mGravity)) {
367             mArrow.setBackground(new RoundedArrowDrawable(
368                     mArrowWidth, mArrowHeight, mArrowPointRadius,
369                     mOutlineRadius, getMeasuredWidth(), getMeasuredHeight(),
370                     mArrowOffsetHorizontal, -mArrowOffsetVertical,
371                     !mIsAboveIcon, mIsLeftAligned,
372                     mArrowColor));
373             setElevation(mElevation);
374             mArrow.setElevation(mElevation);
375         }
376     }
377 
378     /**
379      * Returns whether or not we should add the arrow.
380      */
shouldAddArrow()381     protected boolean shouldAddArrow() {
382         return true;
383     }
384 
385     /**
386      * Provide the location of the target object relative to the dragLayer.
387      */
getTargetObjectLocation(Rect outPos)388     protected abstract void getTargetObjectLocation(Rect outPos);
389 
390     /**
391      * Orients this container above or below the given icon, aligning with the left or right.
392      *
393      * These are the preferred orientations, in order (RTL prefers right-aligned over left):
394      * - Above and left-aligned
395      * - Above and right-aligned
396      * - Below and left-aligned
397      * - Below and right-aligned
398      *
399      * So we always align left if there is enough horizontal space
400      * and align above if there is enough vertical space.
401      */
orientAboutObject()402     protected void orientAboutObject() {
403         orientAboutObject(true /* allowAlignLeft */, true /* allowAlignRight */);
404     }
405 
406     /**
407      * @see #orientAboutObject()
408      *
409      * @param allowAlignLeft Set to false if we already tried aligning left and didn't have room.
410      * @param allowAlignRight Set to false if we already tried aligning right and didn't have room.
411      * TODO: Can we test this with all permutations of widths/heights and icon locations + RTL?
412      */
orientAboutObject(boolean allowAlignLeft, boolean allowAlignRight)413     private void orientAboutObject(boolean allowAlignLeft, boolean allowAlignRight) {
414         measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
415 
416         int extraVerticalSpace = mArrowHeight + mArrowOffsetVertical + getExtraVerticalOffset();
417         // The margins are added after we call this method, so we need to account for them here.
418         int numVisibleChildren = 0;
419         for (int i = getChildCount() - 1; i >= 0; --i) {
420             if (getChildAt(i).getVisibility() == VISIBLE) {
421                 numVisibleChildren++;
422             }
423         }
424         int childMargins = (numVisibleChildren - 1) * mChildContainerMargin;
425         int height = getMeasuredHeight() + extraVerticalSpace + childMargins;
426         int width = getMeasuredWidth() + getPaddingLeft() + getPaddingRight();
427 
428         getTargetObjectLocation(mTempRect);
429         InsettableFrameLayout dragLayer = getPopupContainer();
430         Rect insets = dragLayer.getInsets();
431 
432         // Align left (right in RTL) if there is room.
433         int leftAlignedX = mTempRect.left;
434         int rightAlignedX = mTempRect.right - width;
435         mIsLeftAligned = !mIsRtl ? allowAlignLeft : !allowAlignRight;
436         int x = mIsLeftAligned ? leftAlignedX : rightAlignedX;
437 
438         // Offset x so that the arrow and shortcut icons are center-aligned with the original icon.
439         int iconWidth = mTempRect.width();
440         int xOffset = iconWidth / 2 - mArrowOffsetHorizontal - mArrowWidth / 2;
441         x += mIsLeftAligned ? xOffset : -xOffset;
442 
443         // Check whether we can still align as we originally wanted, now that we've calculated x.
444         if (!allowAlignLeft && !allowAlignRight) {
445             // We've already tried both ways and couldn't make it fit. onLayout() will set the
446             // gravity to CENTER_HORIZONTAL, but continue below to update y.
447         } else {
448             boolean canBeLeftAligned = x + width + insets.left
449                     < dragLayer.getWidth() - insets.right;
450             boolean canBeRightAligned = x > insets.left;
451             boolean alignmentStillValid = mIsLeftAligned && canBeLeftAligned
452                     || !mIsLeftAligned && canBeRightAligned;
453             if (!alignmentStillValid) {
454                 // Try again, but don't allow this alignment we already know won't work.
455                 orientAboutObject(allowAlignLeft && !mIsLeftAligned /* allowAlignLeft */,
456                         allowAlignRight && mIsLeftAligned /* allowAlignRight */);
457                 return;
458             }
459         }
460 
461         // Open above icon if there is room.
462         int iconHeight = mTempRect.height();
463         int y = mTempRect.top - height;
464         mIsAboveIcon = y > dragLayer.getTop() + insets.top;
465         if (!mIsAboveIcon) {
466             y = mTempRect.top + iconHeight + extraVerticalSpace;
467             height -= extraVerticalSpace;
468         }
469 
470         // Insets are added later, so subtract them now.
471         x -= insets.left;
472         y -= insets.top;
473 
474         mGravity = 0;
475         if ((insets.top + y + height) > (dragLayer.getBottom() - insets.bottom)) {
476             // The container is opening off the screen, so just center it in the drag layer instead.
477             mGravity = Gravity.CENTER_VERTICAL;
478             // Put the container next to the icon, preferring the right side in ltr (left in rtl).
479             int rightSide = leftAlignedX + iconWidth - insets.left;
480             int leftSide = rightAlignedX - iconWidth - insets.left;
481             if (!mIsRtl) {
482                 if (rightSide + width < dragLayer.getRight()) {
483                     x = rightSide;
484                     mIsLeftAligned = true;
485                 } else {
486                     x = leftSide;
487                     mIsLeftAligned = false;
488                 }
489             } else {
490                 if (leftSide > dragLayer.getLeft()) {
491                     x = leftSide;
492                     mIsLeftAligned = false;
493                 } else {
494                     x = rightSide;
495                     mIsLeftAligned = true;
496                 }
497             }
498             mIsAboveIcon = true;
499         }
500 
501         setX(x);
502         if (Gravity.isVertical(mGravity)) {
503             return;
504         }
505 
506         FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams();
507         FrameLayout.LayoutParams arrowLp = (FrameLayout.LayoutParams) mArrow.getLayoutParams();
508         if (mIsAboveIcon) {
509             arrowLp.gravity = lp.gravity = Gravity.BOTTOM;
510             lp.bottomMargin =
511                     getPopupContainer().getHeight() - y - getMeasuredHeight() - insets.top;
512             arrowLp.bottomMargin =
513                     lp.bottomMargin - arrowLp.height - mArrowOffsetVertical - insets.bottom;
514         } else {
515             arrowLp.gravity = lp.gravity = Gravity.TOP;
516             lp.topMargin = y + insets.top;
517             arrowLp.topMargin = lp.topMargin - insets.top - arrowLp.height - mArrowOffsetVertical;
518         }
519     }
520 
521     @Override
onLayout(boolean changed, int l, int t, int r, int b)522     protected void onLayout(boolean changed, int l, int t, int r, int b) {
523         super.onLayout(changed, l, t, r, b);
524 
525         // enforce contained is within screen
526         BaseDragLayer dragLayer = getPopupContainer();
527         Rect insets = dragLayer.getInsets();
528         if (getTranslationX() + l < insets.left
529                 || getTranslationX() + r > dragLayer.getWidth() - insets.right) {
530             // If we are still off screen, center horizontally too.
531             mGravity |= Gravity.CENTER_HORIZONTAL;
532         }
533 
534         if (Gravity.isHorizontal(mGravity)) {
535             setX(dragLayer.getWidth() / 2 - getMeasuredWidth() / 2);
536             mArrow.setVisibility(INVISIBLE);
537         }
538         if (Gravity.isVertical(mGravity)) {
539             setY(dragLayer.getHeight() / 2 - getMeasuredHeight() / 2);
540         }
541     }
542 
543     @Override
getAccessibilityTarget()544     protected Pair<View, String> getAccessibilityTarget() {
545         return Pair.create(this, "");
546     }
547 
548     @Override
getAccessibilityInitialFocusView()549     protected View getAccessibilityInitialFocusView() {
550         return getChildCount() > 0 ? getChildAt(0) : this;
551     }
552 
animateOpen()553     protected void animateOpen() {
554         setVisibility(View.VISIBLE);
555         mOpenCloseAnimator = getOpenCloseAnimator(
556                         true,
557                         OPEN_DURATION_U,
558                         OPEN_FADE_START_DELAY_U,
559                         OPEN_FADE_DURATION_U,
560                         OPEN_CHILD_FADE_START_DELAY_U,
561                         OPEN_CHILD_FADE_DURATION_U,
562                         EMPHASIZED_DECELERATE);
563 
564         onCreateOpenAnimation(mOpenCloseAnimator);
565         mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() {
566             @Override
567             public void onAnimationEnd(Animator animation) {
568                 setAlpha(1f);
569                 announceAccessibilityChanges();
570                 mOpenCloseAnimator = null;
571             }
572         });
573         mOpenCloseAnimator.start();
574     }
575 
fadeInChildViews(ViewGroup group, float[] alphaValues, long startDelay, long duration, AnimatorSet out)576     private void fadeInChildViews(ViewGroup group, float[] alphaValues, long startDelay,
577             long duration, AnimatorSet out) {
578         for (int i = group.getChildCount() - 1; i >= 0; --i) {
579             View view = group.getChildAt(i);
580             if (view.getVisibility() == VISIBLE && view instanceof ViewGroup) {
581                 if (isShortcutContainer(view)) {
582                     fadeInChildViews((ViewGroup) view, alphaValues, startDelay, duration, out);
583                     continue;
584                 }
585                 for (int j = ((ViewGroup) view).getChildCount() - 1; j >= 0; --j) {
586                     View childView = ((ViewGroup) view).getChildAt(j);
587                     childView.setAlpha(alphaValues[0]);
588                     ValueAnimator childFade = ObjectAnimator.ofFloat(childView, ALPHA, alphaValues);
589                     childFade.setStartDelay(startDelay);
590                     childFade.setDuration(duration);
591                     childFade.setInterpolator(LINEAR);
592 
593                     out.play(childFade);
594                 }
595             }
596         }
597     }
598 
animateClose()599     protected void animateClose() {
600         if (!mIsOpen) {
601             return;
602         }
603         if (mOpenCloseAnimator != null) {
604             mOpenCloseAnimator.cancel();
605         }
606         mIsOpen = false;
607 
608         mOpenCloseAnimator = getOpenCloseAnimator(
609                         false,
610                         CLOSE_DURATION_U,
611                         CLOSE_FADE_START_DELAY_U,
612                         CLOSE_FADE_DURATION_U,
613                         CLOSE_CHILD_FADE_START_DELAY_U,
614                         CLOSE_CHILD_FADE_DURATION_U,
615                         EMPHASIZED_ACCELERATE);
616 
617         onCreateCloseAnimation(mOpenCloseAnimator);
618         mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() {
619             @Override
620             public void onAnimationEnd(Animator animation) {
621                 mOpenCloseAnimator = null;
622                 if (mDeferContainerRemoval) {
623                     setVisibility(INVISIBLE);
624                 } else {
625                     closeComplete();
626                 }
627             }
628         });
629         mOpenCloseAnimator.start();
630     }
631 
getExtraVerticalOffset()632     public int getExtraVerticalOffset() {
633         return getResources().getDimensionPixelSize(R.dimen.popup_vertical_padding);
634     }
635 
636     /**
637      * Sets X and Y pivots for the view animation considering arrow position.
638      */
setPivotForOpenCloseAnimation()639     protected void setPivotForOpenCloseAnimation() {
640         int arrowCenter = mArrowOffsetHorizontal + mArrowWidth / 2;
641         if (mIsArrowRotated) {
642             setPivotX(mIsLeftAligned ? 0f : getMeasuredWidth());
643             setPivotY(arrowCenter);
644         } else {
645             setPivotX(mIsLeftAligned ? arrowCenter : getMeasuredWidth() - arrowCenter);
646             setPivotY(mIsAboveIcon ? getMeasuredHeight() : 0f);
647         }
648     }
649 
650 
getOpenCloseAnimator(boolean isOpening, int scaleDuration, int fadeStartDelay, int fadeDuration, int childFadeStartDelay, int childFadeDuration, Interpolator interpolator)651     protected AnimatorSet getOpenCloseAnimator(boolean isOpening, int scaleDuration,
652             int fadeStartDelay, int fadeDuration, int childFadeStartDelay, int childFadeDuration,
653             Interpolator interpolator) {
654 
655         setPivotForOpenCloseAnimation();
656 
657         float[] alphaValues = isOpening ? new float[] {0, 1} : new float[] {1, 0};
658         float[] scaleValues = isOpening ? new float[] {0.5f, 1.02f} : new float[] {1f, 0.5f};
659         Animator alpha = getAnimatorOfFloat(this, View.ALPHA, fadeDuration, fadeStartDelay,
660                 LINEAR, alphaValues);
661         Animator arrowAlpha = getAnimatorOfFloat(mArrow, View.ALPHA, fadeDuration, fadeStartDelay,
662                 LINEAR, alphaValues);
663         Animator scaleY = getAnimatorOfFloat(this, View.SCALE_Y, scaleDuration, 0, interpolator,
664                 scaleValues);
665         Animator scaleX = getAnimatorOfFloat(this, View.SCALE_X, scaleDuration, 0, interpolator,
666                 scaleValues);
667 
668         final AnimatorSet animatorSet = new AnimatorSet();
669         if (isOpening) {
670             float[] scaleValuesOvershoot = new float[] {1.02f, 1f};
671             PathInterpolator overshootInterpolator = new PathInterpolator(0.3f, 0, 0.33f, 1f);
672             Animator overshootY = getAnimatorOfFloat(this, View.SCALE_Y,
673                     OPEN_OVERSHOOT_DURATION_U, scaleDuration, overshootInterpolator,
674                     scaleValuesOvershoot);
675             Animator overshootX = getAnimatorOfFloat(this, View.SCALE_X,
676                     OPEN_OVERSHOOT_DURATION_U, scaleDuration, overshootInterpolator,
677                     scaleValuesOvershoot);
678 
679             animatorSet.playTogether(alpha, arrowAlpha, scaleY, scaleX, overshootX, overshootY);
680         } else {
681             animatorSet.playTogether(alpha, arrowAlpha, scaleY, scaleX);
682         }
683 
684         fadeInChildViews(this, alphaValues, childFadeStartDelay, childFadeDuration, animatorSet);
685         return animatorSet;
686     }
687 
getAnimatorOfFloat(View view, Property<View, Float> property, int duration, int startDelay, Interpolator interpolator, float... values)688     private Animator getAnimatorOfFloat(View view, Property<View, Float> property,
689             int duration, int startDelay, Interpolator interpolator,  float... values) {
690         Animator animator = ObjectAnimator.ofFloat(view, property, values);
691         animator.setDuration(duration);
692         animator.setInterpolator(interpolator);
693         animator.setStartDelay(startDelay);
694         return animator;
695     }
696 
697     /**
698      * Called when creating the open transition allowing subclass can add additional animations.
699      */
onCreateOpenAnimation(AnimatorSet anim)700     protected void onCreateOpenAnimation(AnimatorSet anim) { }
701 
702     /**
703      * Called when creating the close transition allowing subclass can add additional animations.
704      */
onCreateCloseAnimation(AnimatorSet anim)705     protected void onCreateCloseAnimation(AnimatorSet anim) { }
706 
707     /**
708      * Closes the popup without animation.
709      */
closeComplete()710     protected void closeComplete() {
711         if (mOpenCloseAnimator != null) {
712             mOpenCloseAnimator.cancel();
713             mOpenCloseAnimator = null;
714         }
715         mIsOpen = false;
716         mDeferContainerRemoval = false;
717         getPopupContainer().removeView(this);
718         getPopupContainer().removeView(mArrow);
719         mOnCloseCallbacks.executeAllAndClear();
720     }
721 
722     /**
723      * Callbacks to be called when the popup is closed
724      */
addOnCloseCallback(Runnable callback)725     public void addOnCloseCallback(Runnable callback) {
726         mOnCloseCallbacks.add(callback);
727     }
728 
getPopupContainer()729     protected BaseDragLayer getPopupContainer() {
730         return mActivityContext.getDragLayer();
731     }
732 }
733