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