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