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