• 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.app.animation.Interpolators.ACCELERATED_EASE;
22 import static com.android.app.animation.Interpolators.DECELERATED_EASE;
23 import static com.android.app.animation.Interpolators.EMPHASIZED_ACCELERATE;
24 import static com.android.app.animation.Interpolators.EMPHASIZED_DECELERATE;
25 import static com.android.app.animation.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 mOpenDuration = 276;
74     protected int mOpenFadeStartDelay = 0;
75     protected int mOpenFadeDuration = 38;
76     protected int mOpenChildFadeStartDelay = 38;
77     protected int mOpenChildFadeDuration = 76;
78 
79     protected int mCloseDuration = 200;
80     protected int mCloseFadeStartDelay = 140;
81     protected int mCloseFadeDuration = 50;
82     protected int mCloseChildFadeStartDelay = 0;
83     protected int mCloseChildFadeDuration = 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     protected final int mChildContainerMargin;
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         mElevation = getResources().getDimension(R.dimen.deep_shortcuts_elevation);
144 
145         // Initialize arrow view
146         final Resources resources = getResources();
147         mArrowColor = getColorStateList(getContext(), R.color.popup_color_background)
148                 .getDefaultColor();
149         mChildContainerMargin = resources.getDimensionPixelSize(R.dimen.popup_margin);
150         mArrowWidth = resources.getDimensionPixelSize(R.dimen.popup_arrow_width);
151         mArrowHeight = resources.getDimensionPixelSize(R.dimen.popup_arrow_height);
152         mArrow = new View(context);
153         mArrow.setLayoutParams(new DragLayer.LayoutParams(mArrowWidth, mArrowHeight));
154         mArrowOffsetVertical = resources.getDimensionPixelSize(R.dimen.popup_arrow_vertical_offset);
155         mArrowOffsetHorizontal = resources.getDimensionPixelSize(
156                 R.dimen.popup_arrow_horizontal_center_offset) - (mArrowWidth / 2);
157         mArrowPointRadius = resources.getDimensionPixelSize(R.dimen.popup_arrow_corner_radius);
158 
159         int smallerRadius = resources.getDimensionPixelSize(R.dimen.popup_smaller_radius);
160         mRoundedTop = new GradientDrawable();
161         int popupPrimaryColor = Themes.getAttrColor(context, R.attr.popupColorPrimary);
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_color_background};
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 = mChildContainerMargin;
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) * mChildContainerMargin;
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                         mOpenDuration,
588                         mOpenFadeStartDelay,
589                         mOpenFadeDuration,
590                         mOpenChildFadeStartDelay,
591                         mOpenChildFadeDuration,
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 = ENABLE_MATERIAL_U_POPUP.get()
677                 ? getMaterialUOpenCloseAnimator(
678                         false,
679                         CLOSE_DURATION_U,
680                         CLOSE_FADE_START_DELAY_U,
681                         CLOSE_FADE_DURATION_U,
682                         CLOSE_CHILD_FADE_START_DELAY_U,
683                         CLOSE_CHILD_FADE_DURATION_U,
684                         EMPHASIZED_ACCELERATE)
685                 : getOpenCloseAnimator(false,
686                         mCloseDuration,
687                         mCloseFadeStartDelay,
688                         mCloseFadeDuration,
689                         mCloseChildFadeStartDelay,
690                         mCloseChildFadeDuration,
691                         ACCELERATED_EASE);
692 
693         onCreateCloseAnimation(mOpenCloseAnimator);
694         mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() {
695             @Override
696             public void onAnimationEnd(Animator animation) {
697                 mOpenCloseAnimator = null;
698                 if (mDeferContainerRemoval) {
699                     setVisibility(INVISIBLE);
700                 } else {
701                     closeComplete();
702                 }
703             }
704         });
705         mOpenCloseAnimator.start();
706     }
707 
getMaterialUOpenCloseAnimator(boolean isOpening, int scaleDuration, int fadeStartDelay, int fadeDuration, int childFadeStartDelay, int childFadeDuration, Interpolator interpolator)708     protected AnimatorSet getMaterialUOpenCloseAnimator(boolean isOpening, int scaleDuration,
709             int fadeStartDelay, int fadeDuration, int childFadeStartDelay, int childFadeDuration,
710             Interpolator interpolator) {
711 
712         int arrowCenter = mArrowOffsetHorizontal + mArrowWidth / 2;
713         if (mIsArrowRotated) {
714             setPivotX(mIsLeftAligned ? 0f : getMeasuredWidth());
715             setPivotY(arrowCenter);
716         } else {
717             setPivotX(mIsLeftAligned ? arrowCenter : getMeasuredWidth() - arrowCenter);
718             setPivotY(mIsAboveIcon ? getMeasuredHeight() : 0f);
719         }
720 
721         float[] alphaValues = isOpening ? new float[] {0, 1} : new float[] {1, 0};
722         float[] scaleValues = isOpening ? new float[] {0.5f, 1.02f} : new float[] {1f, 0.5f};
723         Animator alpha = getAnimatorOfFloat(this, View.ALPHA, fadeDuration, fadeStartDelay,
724                 LINEAR, alphaValues);
725         Animator arrowAlpha = getAnimatorOfFloat(mArrow, View.ALPHA, fadeDuration, fadeStartDelay,
726                 LINEAR, alphaValues);
727         Animator scaleY = getAnimatorOfFloat(this, View.SCALE_Y, scaleDuration, 0, interpolator,
728                 scaleValues);
729         Animator scaleX = getAnimatorOfFloat(this, View.SCALE_X, scaleDuration, 0, interpolator,
730                 scaleValues);
731 
732         final AnimatorSet animatorSet = new AnimatorSet();
733         if (isOpening) {
734             float[] scaleValuesOvershoot = new float[] {1.02f, 1f};
735             PathInterpolator overshootInterpolator = new PathInterpolator(0.3f, 0, 0.33f, 1f);
736             Animator overshootY = getAnimatorOfFloat(this, View.SCALE_Y,
737                     OPEN_OVERSHOOT_DURATION_U, scaleDuration, overshootInterpolator,
738                     scaleValuesOvershoot);
739             Animator overshootX = getAnimatorOfFloat(this, View.SCALE_X,
740                     OPEN_OVERSHOOT_DURATION_U, scaleDuration, overshootInterpolator,
741                     scaleValuesOvershoot);
742 
743             animatorSet.playTogether(alpha, arrowAlpha, scaleY, scaleX, overshootX, overshootY);
744         } else {
745             animatorSet.playTogether(alpha, arrowAlpha, scaleY, scaleX);
746         }
747 
748         fadeInChildViews(this, alphaValues, childFadeStartDelay, childFadeDuration, animatorSet);
749         return animatorSet;
750     }
751 
getAnimatorOfFloat(View view, Property<View, Float> property, int duration, int startDelay, Interpolator interpolator, float... values)752     private Animator getAnimatorOfFloat(View view, Property<View, Float> property,
753             int duration, int startDelay, Interpolator interpolator,  float... values) {
754         Animator animator = ObjectAnimator.ofFloat(view, property, values);
755         animator.setDuration(duration);
756         animator.setInterpolator(interpolator);
757         animator.setStartDelay(startDelay);
758         return animator;
759     }
760 
761     /**
762      * Called when creating the open transition allowing subclass can add additional animations.
763      */
onCreateOpenAnimation(AnimatorSet anim)764     protected void onCreateOpenAnimation(AnimatorSet anim) { }
765 
766     /**
767      * Called when creating the close transition allowing subclass can add additional animations.
768      */
onCreateCloseAnimation(AnimatorSet anim)769     protected void onCreateCloseAnimation(AnimatorSet anim) { }
770 
771     /**
772      * Closes the popup without animation.
773      */
closeComplete()774     protected void closeComplete() {
775         if (mOpenCloseAnimator != null) {
776             mOpenCloseAnimator.cancel();
777             mOpenCloseAnimator = null;
778         }
779         mIsOpen = false;
780         mDeferContainerRemoval = false;
781         getPopupContainer().removeView(this);
782         getPopupContainer().removeView(mArrow);
783         mOnCloseCallbacks.executeAllAndClear();
784     }
785 
786     /**
787      * Callbacks to be called when the popup is closed
788      */
addOnCloseCallback(Runnable callback)789     public void addOnCloseCallback(Runnable callback) {
790         mOnCloseCallbacks.add(callback);
791     }
792 
getPopupContainer()793     protected BaseDragLayer getPopupContainer() {
794         return mActivityContext.getDragLayer();
795     }
796 }
797