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