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