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