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