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