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