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