/* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.launcher3.popup; import static androidx.core.content.ContextCompat.getColorStateList; import static com.android.launcher3.anim.Interpolators.ACCELERATED_EASE; import static com.android.launcher3.anim.Interpolators.DECELERATED_EASE; import static com.android.launcher3.anim.Interpolators.LINEAR; import static com.android.launcher3.config.FeatureFlags.ENABLE_LOCAL_COLOR_POPUPS; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.annotation.TargetApi; import android.content.Context; import android.content.res.Resources; import android.graphics.Color; import android.graphics.Rect; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.os.Build; import android.util.AttributeSet; import android.util.Pair; import android.util.SparseIntArray; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.animation.Interpolator; import android.widget.FrameLayout; import androidx.annotation.NonNull; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.InsettableFrameLayout; import com.android.launcher3.Launcher; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.Workspace; import com.android.launcher3.dragndrop.DragLayer; import com.android.launcher3.shortcuts.DeepShortcutView; import com.android.launcher3.util.Themes; import com.android.launcher3.views.ActivityContext; import com.android.launcher3.views.BaseDragLayer; import com.android.launcher3.widget.LocalColorExtractor; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; /** * A container for shortcuts to deep links and notifications associated with an app. * * @param The activity on with the popup shows */ public abstract class ArrowPopup extends AbstractFloatingView { // Duration values (ms) for popup open and close animations. protected int OPEN_DURATION = 276; protected int OPEN_FADE_START_DELAY = 0; protected int OPEN_FADE_DURATION = 38; protected int OPEN_CHILD_FADE_START_DELAY = 38; protected int OPEN_CHILD_FADE_DURATION = 76; protected int CLOSE_DURATION = 200; protected int CLOSE_FADE_START_DELAY = 140; protected int CLOSE_FADE_DURATION = 50; protected int CLOSE_CHILD_FADE_START_DELAY = 0; protected int CLOSE_CHILD_FADE_DURATION = 140; // Index used to get background color when using local wallpaper color extraction, private static final int DARK_COLOR_EXTRACTION_INDEX = android.R.color.system_neutral2_800; private static final int LIGHT_COLOR_EXTRACTION_INDEX = android.R.color.system_accent2_50; protected final Rect mTempRect = new Rect(); protected final LayoutInflater mInflater; protected final float mOutlineRadius; protected final T mActivityContext; protected final boolean mIsRtl; protected final int mArrowOffsetVertical; protected final int mArrowOffsetHorizontal; protected final int mArrowWidth; protected final int mArrowHeight; protected final int mArrowPointRadius; protected final View mArrow; private final int mMargin; protected boolean mIsLeftAligned; protected boolean mIsAboveIcon; protected int mGravity; protected AnimatorSet mOpenCloseAnimator; protected boolean mDeferContainerRemoval; protected boolean shouldScaleArrow = false; private final GradientDrawable mRoundedTop; private final GradientDrawable mRoundedBottom; private Runnable mOnCloseCallback = () -> { }; // The rect string of the view that the arrow is attached to, in screen reference frame. protected int mArrowColor; protected final List mColorExtractors; protected final float mElevation; private final int mBackgroundColor; private final String mIterateChildrenTag; private final int[] mColorIds; public ArrowPopup(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mInflater = LayoutInflater.from(context); mOutlineRadius = Themes.getDialogCornerRadius(context); mActivityContext = ActivityContext.lookupContext(context); mIsRtl = Utilities.isRtl(getResources()); mBackgroundColor = Themes.getAttrColor(context, R.attr.popupColorPrimary); mArrowColor = mBackgroundColor; mElevation = getResources().getDimension(R.dimen.deep_shortcuts_elevation); // Initialize arrow view final Resources resources = getResources(); mMargin = resources.getDimensionPixelSize(R.dimen.popup_margin); mArrowWidth = resources.getDimensionPixelSize(R.dimen.popup_arrow_width); mArrowHeight = resources.getDimensionPixelSize(R.dimen.popup_arrow_height); mArrow = new View(context); mArrow.setLayoutParams(new DragLayer.LayoutParams(mArrowWidth, mArrowHeight)); mArrowOffsetVertical = resources.getDimensionPixelSize(R.dimen.popup_arrow_vertical_offset); mArrowOffsetHorizontal = resources.getDimensionPixelSize( R.dimen.popup_arrow_horizontal_center_offset) - (mArrowWidth / 2); mArrowPointRadius = resources.getDimensionPixelSize(R.dimen.popup_arrow_corner_radius); int smallerRadius = resources.getDimensionPixelSize(R.dimen.popup_smaller_radius); mRoundedTop = new GradientDrawable(); mRoundedTop.setColor(mBackgroundColor); mRoundedTop.setCornerRadii(new float[] { mOutlineRadius, mOutlineRadius, mOutlineRadius, mOutlineRadius, smallerRadius, smallerRadius, smallerRadius, smallerRadius}); mRoundedBottom = new GradientDrawable(); mRoundedBottom.setColor(mBackgroundColor); mRoundedBottom.setCornerRadii(new float[] { smallerRadius, smallerRadius, smallerRadius, smallerRadius, mOutlineRadius, mOutlineRadius, mOutlineRadius, mOutlineRadius}); mIterateChildrenTag = getContext().getString(R.string.popup_container_iterate_children); boolean shouldUseColorExtraction = mActivityContext.shouldUseColorExtractionForPopup(); if (shouldUseColorExtraction && Utilities.ATLEAST_S && ENABLE_LOCAL_COLOR_POPUPS.get()) { mColorExtractors = new ArrayList<>(); } else { mColorExtractors = null; } if (shouldUseColorExtraction) { mColorIds = new int[]{R.color.popup_shade_first, R.color.popup_shade_second, R.color.popup_shade_third}; } else { mColorIds = new int[]{R.color.popup_shade_first}; } } public ArrowPopup(Context context, AttributeSet attrs) { this(context, attrs, 0); } public ArrowPopup(Context context) { this(context, null, 0); } @Override protected void handleClose(boolean animate) { if (animate) { animateClose(); } else { closeComplete(); } } /** * Utility method for inflating and adding a view */ public R inflateAndAdd(int resId, ViewGroup container) { View view = mInflater.inflate(resId, container, false); container.addView(view); return (R) view; } /** * Utility method for inflating and adding a view */ public R inflateAndAdd(int resId, ViewGroup container, int index) { View view = mInflater.inflate(resId, container, false); container.addView(view, index); return (R) view; } /** * Called when all view inflation and reordering in complete. */ protected void onInflationComplete(boolean isReversed) { } /** * Set the margins and radius of backgrounds after views are properly ordered. */ public void assignMarginsAndBackgrounds(ViewGroup viewGroup) { assignMarginsAndBackgrounds(viewGroup, Color.TRANSPARENT); } /** * @param backgroundColor When Color.TRANSPARENT, we get color from {@link #mColorIds}. * Otherwise, we will use this color for all child views. */ protected void assignMarginsAndBackgrounds(ViewGroup viewGroup, int backgroundColor) { int[] colors = null; if (backgroundColor == Color.TRANSPARENT) { // Lazily get the colors so they match the current wallpaper colors. colors = Arrays.stream(mColorIds).map( r -> getColorStateList(getContext(), r).getDefaultColor()).toArray(); } int count = viewGroup.getChildCount(); int totalVisibleShortcuts = 0; for (int i = 0; i < count; i++) { View view = viewGroup.getChildAt(i); if (view.getVisibility() == VISIBLE && isShortcutOrWrapper(view)) { totalVisibleShortcuts++; } } int numVisibleChild = 0; int numVisibleShortcut = 0; View lastView = null; AnimatorSet colorAnimator = new AnimatorSet(); for (int i = 0; i < count; i++) { View view = viewGroup.getChildAt(i); if (view.getVisibility() == VISIBLE) { if (lastView != null) { MarginLayoutParams mlp = (MarginLayoutParams) lastView.getLayoutParams(); mlp.bottomMargin = mMargin; } lastView = view; MarginLayoutParams mlp = (MarginLayoutParams) lastView.getLayoutParams(); mlp.bottomMargin = 0; if (colors != null) { backgroundColor = colors[numVisibleChild % colors.length]; } if (!ENABLE_LOCAL_COLOR_POPUPS.get()) { // Arrow color matches the first child or the last child. if (!mIsAboveIcon && numVisibleChild == 0 && viewGroup == this) { mArrowColor = backgroundColor; } else if (mIsAboveIcon) { mArrowColor = backgroundColor; } } if (view instanceof ViewGroup && mIterateChildrenTag.equals(view.getTag())) { assignMarginsAndBackgrounds((ViewGroup) view, backgroundColor); numVisibleChild++; continue; } if (isShortcutOrWrapper(view)) { if (totalVisibleShortcuts == 1) { view.setBackgroundResource(R.drawable.single_item_primary); } else if (totalVisibleShortcuts > 1) { if (numVisibleShortcut == 0) { view.setBackground(mRoundedTop.getConstantState().newDrawable()); } else if (numVisibleShortcut == (totalVisibleShortcuts - 1)) { view.setBackground(mRoundedBottom.getConstantState().newDrawable()); } else { view.setBackgroundResource(R.drawable.middle_item_primary); } numVisibleShortcut++; } } if (!ENABLE_LOCAL_COLOR_POPUPS.get()) { setChildColor(view, backgroundColor, colorAnimator); } numVisibleChild++; } } colorAnimator.setDuration(0).start(); measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); } /** * Returns {@code true} if the child is a shortcut or wraps a shortcut. */ protected boolean isShortcutOrWrapper(View view) { return view instanceof DeepShortcutView; } @TargetApi(Build.VERSION_CODES.S) private int getExtractedColor(SparseIntArray colors) { int index = Utilities.isDarkTheme(getContext()) ? DARK_COLOR_EXTRACTION_INDEX : LIGHT_COLOR_EXTRACTION_INDEX; return colors.get(index, mBackgroundColor); } protected void addPreDrawForColorExtraction(Launcher launcher) { getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { @Override public boolean onPreDraw() { getViewTreeObserver().removeOnPreDrawListener(this); initColorExtractionLocations(launcher); return true; } }); } /** * Returns list of child views that will receive local color extraction treatment. * Note: Order should match the view hierarchy. */ protected List getChildrenForColorExtraction() { return Collections.emptyList(); } private void initColorExtractionLocations(Launcher launcher) { if (mColorExtractors == null) { return; } Workspace workspace = launcher.getWorkspace(); if (workspace == null) { return; } boolean firstVisibleChild = true; int screenId = workspace.getScreenIdForPageIndex(workspace.getCurrentPage()); DragLayer dragLayer = launcher.getDragLayer(); final View[] viewAlignedWithArrow = new View[1]; // Order matters here, since we need the arrow to match the color of its adjacent view. for (final View view : getChildrenForColorExtraction()) { if (view != null && view.getVisibility() == VISIBLE) { Rect pos = new Rect(); dragLayer.getDescendantRectRelativeToSelf(view, pos); if (!pos.isEmpty()) { LocalColorExtractor extractor = LocalColorExtractor.newInstance(launcher); extractor.setWorkspaceLocation(pos, dragLayer, screenId); extractor.setListener(extractedColors -> { AnimatorSet colors = new AnimatorSet(); int newColor = getExtractedColor(extractedColors); setChildColor(view, newColor, colors); int numChildren = view instanceof ViewGroup ? ((ViewGroup) view).getChildCount() : 0; for (int i = 0; i < numChildren; ++i) { View childView = ((ViewGroup) view).getChildAt(i); setChildColor(childView, newColor, colors); } if (viewAlignedWithArrow[0] == view) { mArrowColor = newColor; updateArrowColor(); } colors.setDuration(150); view.post(colors::start); }); mColorExtractors.add(extractor); if (mIsAboveIcon || firstVisibleChild) { viewAlignedWithArrow[0] = view; } firstVisibleChild = false; } } } } /** * Sets the background color of the child. */ protected void setChildColor(View view, int color, AnimatorSet animatorSetOut) { Drawable bg = view.getBackground(); if (bg instanceof GradientDrawable) { GradientDrawable gd = (GradientDrawable) bg.mutate(); int oldColor = ((GradientDrawable) bg).getColor().getDefaultColor(); animatorSetOut.play(ObjectAnimator.ofArgb(gd, "color", oldColor, color)); } else if (bg instanceof ColorDrawable) { ColorDrawable cd = (ColorDrawable) bg.mutate(); int oldColor = ((ColorDrawable) bg).getColor(); animatorSetOut.play(ObjectAnimator.ofArgb(cd, "color", oldColor, color)); } } /** * Shows the popup at the desired location, optionally reversing the children. * @param viewsToFlip number of views from the top to to flip in case of reverse order */ protected void reorderAndShow(int viewsToFlip) { setupForDisplay(); boolean reverseOrder = mIsAboveIcon; if (reverseOrder) { reverseOrder(viewsToFlip); } onInflationComplete(reverseOrder); assignMarginsAndBackgrounds(this); if (shouldAddArrow()) { addArrow(); } animateOpen(); } /** * Shows the popup at the desired location. */ public void show() { setupForDisplay(); onInflationComplete(false); assignMarginsAndBackgrounds(this); if (shouldAddArrow()) { addArrow(); } animateOpen(); } protected void setupForDisplay() { setVisibility(View.INVISIBLE); mIsOpen = true; getPopupContainer().addView(this); orientAboutObject(); } private void reverseOrder(int viewsToFlip) { int count = getChildCount(); ArrayList allViews = new ArrayList<>(count); for (int i = 0; i < count; i++) { if (i == viewsToFlip) { Collections.reverse(allViews); } allViews.add(getChildAt(i)); } Collections.reverse(allViews); removeAllViews(); for (int i = 0; i < count; i++) { addView(allViews.get(i)); } } private int getArrowLeft() { if (mIsLeftAligned) { return mArrowOffsetHorizontal; } return getMeasuredWidth() - mArrowOffsetHorizontal - mArrowWidth; } /** * @param show If true, shows arrow (when applicable), otherwise hides arrow. */ public void showArrow(boolean show) { mArrow.setVisibility(show && shouldAddArrow() ? VISIBLE : INVISIBLE); } protected void addArrow() { getPopupContainer().addView(mArrow); mArrow.setX(getX() + getArrowLeft()); if (Gravity.isVertical(mGravity)) { // This is only true if there wasn't room for the container next to the icon, // so we centered it instead. In that case we don't want to showDefaultOptions the arrow. mArrow.setVisibility(INVISIBLE); } else { updateArrowColor(); } mArrow.setPivotX(mArrowWidth / 2.0f); mArrow.setPivotY(mIsAboveIcon ? mArrowHeight : 0); } protected void updateArrowColor() { if (!Gravity.isVertical(mGravity)) { mArrow.setBackground(new RoundedArrowDrawable( mArrowWidth, mArrowHeight, mArrowPointRadius, mOutlineRadius, getMeasuredWidth(), getMeasuredHeight(), mArrowOffsetHorizontal, -mArrowOffsetVertical, !mIsAboveIcon, mIsLeftAligned, mArrowColor)); setElevation(mElevation); mArrow.setElevation(mElevation); } } /** * Returns whether or not we should add the arrow. */ protected boolean shouldAddArrow() { return true; } /** * Provide the location of the target object relative to the dragLayer. */ protected abstract void getTargetObjectLocation(Rect outPos); /** * Orients this container above or below the given icon, aligning with the left or right. * * These are the preferred orientations, in order (RTL prefers right-aligned over left): * - Above and left-aligned * - Above and right-aligned * - Below and left-aligned * - Below and right-aligned * * So we always align left if there is enough horizontal space * and align above if there is enough vertical space. */ protected void orientAboutObject() { orientAboutObject(true /* allowAlignLeft */, true /* allowAlignRight */); } /** * @see #orientAboutObject() * * @param allowAlignLeft Set to false if we already tried aligning left and didn't have room. * @param allowAlignRight Set to false if we already tried aligning right and didn't have room. * TODO: Can we test this with all permutations of widths/heights and icon locations + RTL? */ private void orientAboutObject(boolean allowAlignLeft, boolean allowAlignRight) { measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); int extraVerticalSpace = mArrowHeight + mArrowOffsetVertical + getResources().getDimensionPixelSize(R.dimen.popup_vertical_padding); // The margins are added after we call this method, so we need to account for them here. int numVisibleChildren = 0; for (int i = getChildCount() - 1; i >= 0; --i) { if (getChildAt(i).getVisibility() == VISIBLE) { numVisibleChildren++; } } int childMargins = (numVisibleChildren - 1) * mMargin; int height = getMeasuredHeight() + extraVerticalSpace + childMargins; int width = getMeasuredWidth() + getPaddingLeft() + getPaddingRight(); getTargetObjectLocation(mTempRect); InsettableFrameLayout dragLayer = getPopupContainer(); Rect insets = dragLayer.getInsets(); // Align left (right in RTL) if there is room. int leftAlignedX = mTempRect.left; int rightAlignedX = mTempRect.right - width; mIsLeftAligned = !mIsRtl ? allowAlignLeft : !allowAlignRight; int x = mIsLeftAligned ? leftAlignedX : rightAlignedX; // Offset x so that the arrow and shortcut icons are center-aligned with the original icon. int iconWidth = mTempRect.width(); int xOffset = iconWidth / 2 - mArrowOffsetHorizontal - mArrowWidth / 2; x += mIsLeftAligned ? xOffset : -xOffset; // Check whether we can still align as we originally wanted, now that we've calculated x. if (!allowAlignLeft && !allowAlignRight) { // We've already tried both ways and couldn't make it fit. onLayout() will set the // gravity to CENTER_HORIZONTAL, but continue below to update y. } else { boolean canBeLeftAligned = x + width + insets.left < dragLayer.getWidth() - insets.right; boolean canBeRightAligned = x > insets.left; boolean alignmentStillValid = mIsLeftAligned && canBeLeftAligned || !mIsLeftAligned && canBeRightAligned; if (!alignmentStillValid) { // Try again, but don't allow this alignment we already know won't work. orientAboutObject(allowAlignLeft && !mIsLeftAligned /* allowAlignLeft */, allowAlignRight && mIsLeftAligned /* allowAlignRight */); return; } } // Open above icon if there is room. int iconHeight = mTempRect.height(); int y = mTempRect.top - height; mIsAboveIcon = y > dragLayer.getTop() + insets.top; if (!mIsAboveIcon) { y = mTempRect.top + iconHeight + extraVerticalSpace; } // Insets are added later, so subtract them now. x -= insets.left; y -= insets.top; mGravity = 0; if (y + height > dragLayer.getBottom() - insets.bottom) { // The container is opening off the screen, so just center it in the drag layer instead. mGravity = Gravity.CENTER_VERTICAL; // Put the container next to the icon, preferring the right side in ltr (left in rtl). int rightSide = leftAlignedX + iconWidth - insets.left; int leftSide = rightAlignedX - iconWidth - insets.left; if (!mIsRtl) { if (rightSide + width < dragLayer.getRight()) { x = rightSide; mIsLeftAligned = true; } else { x = leftSide; mIsLeftAligned = false; } } else { if (leftSide > dragLayer.getLeft()) { x = leftSide; mIsLeftAligned = false; } else { x = rightSide; mIsLeftAligned = true; } } mIsAboveIcon = true; } setX(x); if (Gravity.isVertical(mGravity)) { return; } FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams(); FrameLayout.LayoutParams arrowLp = (FrameLayout.LayoutParams) mArrow.getLayoutParams(); if (mIsAboveIcon) { arrowLp.gravity = lp.gravity = Gravity.BOTTOM; lp.bottomMargin = getPopupContainer().getHeight() - y - getMeasuredHeight() - insets.top; arrowLp.bottomMargin = lp.bottomMargin - arrowLp.height - mArrowOffsetVertical - insets.bottom; } else { arrowLp.gravity = lp.gravity = Gravity.TOP; lp.topMargin = y + insets.top; arrowLp.topMargin = lp.topMargin - insets.top - arrowLp.height - mArrowOffsetVertical; } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); // enforce contained is within screen BaseDragLayer dragLayer = getPopupContainer(); Rect insets = dragLayer.getInsets(); if (getTranslationX() + l < insets.left || getTranslationX() + r > dragLayer.getWidth() - insets.right) { // If we are still off screen, center horizontally too. mGravity |= Gravity.CENTER_HORIZONTAL; } if (Gravity.isHorizontal(mGravity)) { setX(dragLayer.getWidth() / 2 - getMeasuredWidth() / 2); mArrow.setVisibility(INVISIBLE); } if (Gravity.isVertical(mGravity)) { setY(dragLayer.getHeight() / 2 - getMeasuredHeight() / 2); } } @Override protected Pair getAccessibilityTarget() { return Pair.create(this, ""); } @Override protected View getAccessibilityInitialFocusView() { return getChildCount() > 0 ? getChildAt(0) : this; } protected void animateOpen() { setVisibility(View.VISIBLE); mOpenCloseAnimator = getOpenCloseAnimator(true, OPEN_DURATION, OPEN_FADE_START_DELAY, OPEN_FADE_DURATION, OPEN_CHILD_FADE_START_DELAY, OPEN_CHILD_FADE_DURATION, DECELERATED_EASE); onCreateOpenAnimation(mOpenCloseAnimator); mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { setAlpha(1f); announceAccessibilityChanges(); mOpenCloseAnimator = null; } }); mOpenCloseAnimator.start(); } private AnimatorSet getOpenCloseAnimator(boolean isOpening, int totalDuration, int fadeStartDelay, int fadeDuration, int childFadeStartDelay, int childFadeDuration, Interpolator interpolator) { final AnimatorSet animatorSet = new AnimatorSet(); float[] alphaValues = isOpening ? new float[] {0, 1} : new float[] {1, 0}; float[] scaleValues = isOpening ? new float[] {0.5f, 1} : new float[] {1, 0.5f}; ValueAnimator fade = ValueAnimator.ofFloat(alphaValues); fade.setStartDelay(fadeStartDelay); fade.setDuration(fadeDuration); fade.setInterpolator(LINEAR); fade.addUpdateListener(anim -> { float alpha = (float) anim.getAnimatedValue(); mArrow.setAlpha(alpha); setAlpha(alpha); }); animatorSet.play(fade); setPivotX(mIsLeftAligned ? 0 : getMeasuredWidth()); setPivotY(mIsAboveIcon ? getMeasuredHeight() : 0); Animator scale = ObjectAnimator.ofFloat(this, View.SCALE_Y, scaleValues); scale.setDuration(totalDuration); scale.setInterpolator(interpolator); animatorSet.play(scale); if (shouldScaleArrow) { Animator arrowScaleAnimator = ObjectAnimator.ofFloat(mArrow, View.SCALE_Y, scaleValues); arrowScaleAnimator.setDuration(totalDuration); arrowScaleAnimator.setInterpolator(interpolator); animatorSet.play(arrowScaleAnimator); } fadeInChildViews(this, alphaValues, childFadeStartDelay, childFadeDuration, animatorSet); return animatorSet; } private void fadeInChildViews(ViewGroup group, float[] alphaValues, long startDelay, long duration, AnimatorSet out) { for (int i = group.getChildCount() - 1; i >= 0; --i) { View view = group.getChildAt(i); if (view.getVisibility() == VISIBLE && view instanceof ViewGroup) { if (mIterateChildrenTag.equals(view.getTag())) { fadeInChildViews((ViewGroup) view, alphaValues, startDelay, duration, out); continue; } for (int j = ((ViewGroup) view).getChildCount() - 1; j >= 0; --j) { View childView = ((ViewGroup) view).getChildAt(j); childView.setAlpha(alphaValues[0]); ValueAnimator childFade = ObjectAnimator.ofFloat(childView, ALPHA, alphaValues); childFade.setStartDelay(startDelay); childFade.setDuration(duration); childFade.setInterpolator(LINEAR); out.play(childFade); } } } } protected void animateClose() { if (!mIsOpen) { return; } if (mOpenCloseAnimator != null) { mOpenCloseAnimator.cancel(); } mIsOpen = false; mOpenCloseAnimator = getOpenCloseAnimator(false, CLOSE_DURATION, CLOSE_FADE_START_DELAY, CLOSE_FADE_DURATION, CLOSE_CHILD_FADE_START_DELAY, CLOSE_CHILD_FADE_DURATION, ACCELERATED_EASE); onCreateCloseAnimation(mOpenCloseAnimator); mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mOpenCloseAnimator = null; if (mDeferContainerRemoval) { setVisibility(INVISIBLE); } else { closeComplete(); } } }); mOpenCloseAnimator.start(); } /** * Called when creating the open transition allowing subclass can add additional animations. */ protected void onCreateOpenAnimation(AnimatorSet anim) { } /** * Called when creating the close transition allowing subclass can add additional animations. */ protected void onCreateCloseAnimation(AnimatorSet anim) { } /** * Closes the popup without animation. */ protected void closeComplete() { if (mOpenCloseAnimator != null) { mOpenCloseAnimator.cancel(); mOpenCloseAnimator = null; } mIsOpen = false; mDeferContainerRemoval = false; getPopupContainer().removeView(this); getPopupContainer().removeView(mArrow); mOnCloseCallback.run(); if (mColorExtractors != null) { mColorExtractors.forEach(e -> e.setListener(null)); } } /** * Callback to be called when the popup is closed */ public void setOnCloseCallback(@NonNull Runnable callback) { mOnCloseCallback = callback; } protected BaseDragLayer getPopupContainer() { return mActivityContext.getDragLayer(); } }