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.launcher3.anim.Interpolators.ACCEL_DEACCEL; 20 21 import android.animation.Animator; 22 import android.animation.AnimatorListenerAdapter; 23 import android.animation.AnimatorSet; 24 import android.animation.ObjectAnimator; 25 import android.animation.TimeInterpolator; 26 import android.animation.ValueAnimator; 27 import android.content.Context; 28 import android.content.res.Resources; 29 import android.graphics.CornerPathEffect; 30 import android.graphics.Outline; 31 import android.graphics.Paint; 32 import android.graphics.Rect; 33 import android.graphics.drawable.ShapeDrawable; 34 import android.util.AttributeSet; 35 import android.util.Pair; 36 import android.view.Gravity; 37 import android.view.LayoutInflater; 38 import android.view.View; 39 import android.view.ViewGroup; 40 import android.view.ViewOutlineProvider; 41 import android.widget.FrameLayout; 42 43 import com.android.launcher3.AbstractFloatingView; 44 import com.android.launcher3.BaseDraggingActivity; 45 import com.android.launcher3.InsettableFrameLayout; 46 import com.android.launcher3.LauncherAnimUtils; 47 import com.android.launcher3.R; 48 import com.android.launcher3.Utilities; 49 import com.android.launcher3.anim.RevealOutlineAnimation; 50 import com.android.launcher3.anim.RoundedRectRevealOutlineProvider; 51 import com.android.launcher3.dragndrop.DragLayer; 52 import com.android.launcher3.graphics.TriangleShape; 53 import com.android.launcher3.util.Themes; 54 import com.android.launcher3.views.BaseDragLayer; 55 56 import java.util.ArrayList; 57 import java.util.Collections; 58 59 /** 60 * A container for shortcuts to deep links and notifications associated with an app. 61 * 62 * @param <T> The activity on with the popup shows 63 */ 64 public abstract class ArrowPopup<T extends BaseDraggingActivity> extends AbstractFloatingView { 65 66 private final Rect mTempRect = new Rect(); 67 68 protected final LayoutInflater mInflater; 69 private final float mOutlineRadius; 70 protected final T mLauncher; 71 protected final boolean mIsRtl; 72 73 private final int mArrowOffset; 74 private final View mArrow; 75 76 protected boolean mIsLeftAligned; 77 protected boolean mIsAboveIcon; 78 private int mGravity; 79 80 protected Animator mOpenCloseAnimator; 81 protected boolean mDeferContainerRemoval; 82 private final Rect mStartRect = new Rect(); 83 private final Rect mEndRect = new Rect(); 84 ArrowPopup(Context context, AttributeSet attrs, int defStyleAttr)85 public ArrowPopup(Context context, AttributeSet attrs, int defStyleAttr) { 86 super(context, attrs, defStyleAttr); 87 mInflater = LayoutInflater.from(context); 88 mOutlineRadius = Themes.getDialogCornerRadius(context); 89 mLauncher = BaseDraggingActivity.fromContext(context); 90 mIsRtl = Utilities.isRtl(getResources()); 91 92 setClipToOutline(true); 93 setOutlineProvider(new ViewOutlineProvider() { 94 @Override 95 public void getOutline(View view, Outline outline) { 96 outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mOutlineRadius); 97 } 98 }); 99 100 // Initialize arrow view 101 final Resources resources = getResources(); 102 final int arrowWidth = resources.getDimensionPixelSize(R.dimen.popup_arrow_width); 103 final int arrowHeight = resources.getDimensionPixelSize(R.dimen.popup_arrow_height); 104 mArrow = new View(context); 105 mArrow.setLayoutParams(new DragLayer.LayoutParams(arrowWidth, arrowHeight)); 106 mArrowOffset = resources.getDimensionPixelSize(R.dimen.popup_arrow_vertical_offset); 107 } 108 ArrowPopup(Context context, AttributeSet attrs)109 public ArrowPopup(Context context, AttributeSet attrs) { 110 this(context, attrs, 0); 111 } 112 ArrowPopup(Context context)113 public ArrowPopup(Context context) { 114 this(context, null, 0); 115 } 116 117 @Override handleClose(boolean animate)118 protected void handleClose(boolean animate) { 119 if (animate) { 120 animateClose(); 121 } else { 122 closeComplete(); 123 } 124 } 125 126 /** 127 * Utility method for inflating and adding a view 128 */ inflateAndAdd(int resId, ViewGroup container)129 public <R extends View> R inflateAndAdd(int resId, ViewGroup container) { 130 View view = mInflater.inflate(resId, container, false); 131 container.addView(view); 132 return (R) view; 133 } 134 135 /** 136 * Utility method for inflating and adding a view 137 */ inflateAndAdd(int resId, ViewGroup container, int index)138 public <R extends View> R inflateAndAdd(int resId, ViewGroup container, int index) { 139 View view = mInflater.inflate(resId, container, false); 140 container.addView(view, index); 141 return (R) view; 142 } 143 144 /** 145 * Called when all view inflation and reordering in complete. 146 */ onInflationComplete(boolean isReversed)147 protected void onInflationComplete(boolean isReversed) { } 148 149 /** 150 * Shows the popup at the desired location, optionally reversing the children. 151 * @param viewsToFlip number of views from the top to to flip in case of reverse order 152 */ reorderAndShow(int viewsToFlip)153 protected void reorderAndShow(int viewsToFlip) { 154 setVisibility(View.INVISIBLE); 155 mIsOpen = true; 156 getPopupContainer().addView(this); 157 orientAboutObject(); 158 159 boolean reverseOrder = mIsAboveIcon; 160 if (reverseOrder) { 161 int count = getChildCount(); 162 ArrayList<View> allViews = new ArrayList<>(count); 163 for (int i = 0; i < count; i++) { 164 if (i == viewsToFlip) { 165 Collections.reverse(allViews); 166 } 167 allViews.add(getChildAt(i)); 168 } 169 Collections.reverse(allViews); 170 removeAllViews(); 171 for (int i = 0; i < count; i++) { 172 addView(allViews.get(i)); 173 } 174 175 orientAboutObject(); 176 } 177 onInflationComplete(reverseOrder); 178 179 // Add the arrow. 180 final Resources res = getResources(); 181 final int arrowCenterOffset = res.getDimensionPixelSize(isAlignedWithStart() 182 ? R.dimen.popup_arrow_horizontal_center_start 183 : R.dimen.popup_arrow_horizontal_center_end); 184 final int halfArrowWidth = res.getDimensionPixelSize(R.dimen.popup_arrow_width) / 2; 185 getPopupContainer().addView(mArrow); 186 DragLayer.LayoutParams arrowLp = (DragLayer.LayoutParams) mArrow.getLayoutParams(); 187 if (mIsLeftAligned) { 188 mArrow.setX(getX() + arrowCenterOffset - halfArrowWidth); 189 } else { 190 mArrow.setX(getX() + getMeasuredWidth() - arrowCenterOffset - halfArrowWidth); 191 } 192 193 if (Gravity.isVertical(mGravity)) { 194 // This is only true if there wasn't room for the container next to the icon, 195 // so we centered it instead. In that case we don't want to showDefaultOptions the arrow. 196 mArrow.setVisibility(INVISIBLE); 197 } else { 198 ShapeDrawable arrowDrawable = new ShapeDrawable(TriangleShape.create( 199 arrowLp.width, arrowLp.height, !mIsAboveIcon)); 200 Paint arrowPaint = arrowDrawable.getPaint(); 201 arrowPaint.setColor(Themes.getAttrColor(getContext(), R.attr.popupColorPrimary)); 202 // The corner path effect won't be reflected in the shadow, but shouldn't be noticeable. 203 int radius = getResources().getDimensionPixelSize(R.dimen.popup_arrow_corner_radius); 204 arrowPaint.setPathEffect(new CornerPathEffect(radius)); 205 mArrow.setBackground(arrowDrawable); 206 // Clip off the part of the arrow that is underneath the popup. 207 if (mIsAboveIcon) { 208 mArrow.setClipBounds(new Rect(0, -mArrowOffset, arrowLp.width, arrowLp.height)); 209 } else { 210 mArrow.setClipBounds(new Rect(0, 0, arrowLp.width, arrowLp.height + mArrowOffset)); 211 } 212 mArrow.setElevation(getElevation()); 213 } 214 215 mArrow.setPivotX(arrowLp.width / 2); 216 mArrow.setPivotY(mIsAboveIcon ? arrowLp.height : 0); 217 218 animateOpen(); 219 } 220 isAlignedWithStart()221 protected boolean isAlignedWithStart() { 222 return mIsLeftAligned && !mIsRtl || !mIsLeftAligned && mIsRtl; 223 } 224 225 /** 226 * Provide the location of the target object relative to the dragLayer. 227 */ getTargetObjectLocation(Rect outPos)228 protected abstract void getTargetObjectLocation(Rect outPos); 229 230 /** 231 * Orients this container above or below the given icon, aligning with the left or right. 232 * 233 * These are the preferred orientations, in order (RTL prefers right-aligned over left): 234 * - Above and left-aligned 235 * - Above and right-aligned 236 * - Below and left-aligned 237 * - Below and right-aligned 238 * 239 * So we always align left if there is enough horizontal space 240 * and align above if there is enough vertical space. 241 */ orientAboutObject()242 protected void orientAboutObject() { 243 orientAboutObject(true /* allowAlignLeft */, true /* allowAlignRight */); 244 } 245 246 /** 247 * @see #orientAboutObject() 248 * 249 * @param allowAlignLeft Set to false if we already tried aligning left and didn't have room. 250 * @param allowAlignRight Set to false if we already tried aligning right and didn't have room. 251 * TODO: Can we test this with all permutations of widths/heights and icon locations + RTL? 252 */ orientAboutObject(boolean allowAlignLeft, boolean allowAlignRight)253 private void orientAboutObject(boolean allowAlignLeft, boolean allowAlignRight) { 254 measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); 255 int width = getMeasuredWidth(); 256 int extraVerticalSpace = mArrow.getLayoutParams().height + mArrowOffset 257 + getResources().getDimensionPixelSize(R.dimen.popup_vertical_padding); 258 int height = getMeasuredHeight() + extraVerticalSpace; 259 260 getTargetObjectLocation(mTempRect); 261 InsettableFrameLayout dragLayer = getPopupContainer(); 262 Rect insets = dragLayer.getInsets(); 263 264 // Align left (right in RTL) if there is room. 265 int leftAlignedX = mTempRect.left; 266 int rightAlignedX = mTempRect.right - width; 267 mIsLeftAligned = !mIsRtl ? allowAlignLeft : !allowAlignRight; 268 int x = mIsLeftAligned ? leftAlignedX : rightAlignedX; 269 270 // Offset x so that the arrow and shortcut icons are center-aligned with the original icon. 271 int iconWidth = mTempRect.width(); 272 Resources resources = getResources(); 273 int xOffset; 274 if (isAlignedWithStart()) { 275 // Aligning with the shortcut icon. 276 int shortcutIconWidth = resources.getDimensionPixelSize(R.dimen.deep_shortcut_icon_size); 277 int shortcutPaddingStart = resources.getDimensionPixelSize( 278 R.dimen.popup_padding_start); 279 xOffset = iconWidth / 2 - shortcutIconWidth / 2 - shortcutPaddingStart; 280 } else { 281 // Aligning with the drag handle. 282 int shortcutDragHandleWidth = resources.getDimensionPixelSize( 283 R.dimen.deep_shortcut_drag_handle_size); 284 int shortcutPaddingEnd = resources.getDimensionPixelSize( 285 R.dimen.popup_padding_end); 286 xOffset = iconWidth / 2 - shortcutDragHandleWidth / 2 - shortcutPaddingEnd; 287 } 288 x += mIsLeftAligned ? xOffset : -xOffset; 289 290 // Check whether we can still align as we originally wanted, now that we've calculated x. 291 if (!allowAlignLeft && !allowAlignRight) { 292 // We've already tried both ways and couldn't make it fit. onLayout() will set the 293 // gravity to CENTER_HORIZONTAL, but continue below to update y. 294 } else { 295 boolean canBeLeftAligned = x + width + insets.left 296 < dragLayer.getWidth() - insets.right; 297 boolean canBeRightAligned = x > insets.left; 298 boolean alignmentStillValid = mIsLeftAligned && canBeLeftAligned 299 || !mIsLeftAligned && canBeRightAligned; 300 if (!alignmentStillValid) { 301 // Try again, but don't allow this alignment we already know won't work. 302 orientAboutObject(allowAlignLeft && !mIsLeftAligned /* allowAlignLeft */, 303 allowAlignRight && mIsLeftAligned /* allowAlignRight */); 304 return; 305 } 306 } 307 308 // Open above icon if there is room. 309 int iconHeight = mTempRect.height(); 310 int y = mTempRect.top - height; 311 mIsAboveIcon = y > dragLayer.getTop() + insets.top; 312 if (!mIsAboveIcon) { 313 y = mTempRect.top + iconHeight + extraVerticalSpace; 314 } 315 316 // Insets are added later, so subtract them now. 317 x -= insets.left; 318 y -= insets.top; 319 320 mGravity = 0; 321 if (y + height > dragLayer.getBottom() - insets.bottom) { 322 // The container is opening off the screen, so just center it in the drag layer instead. 323 mGravity = Gravity.CENTER_VERTICAL; 324 // Put the container next to the icon, preferring the right side in ltr (left in rtl). 325 int rightSide = leftAlignedX + iconWidth - insets.left; 326 int leftSide = rightAlignedX - iconWidth - insets.left; 327 if (!mIsRtl) { 328 if (rightSide + width < dragLayer.getRight()) { 329 x = rightSide; 330 mIsLeftAligned = true; 331 } else { 332 x = leftSide; 333 mIsLeftAligned = false; 334 } 335 } else { 336 if (leftSide > dragLayer.getLeft()) { 337 x = leftSide; 338 mIsLeftAligned = false; 339 } else { 340 x = rightSide; 341 mIsLeftAligned = true; 342 } 343 } 344 mIsAboveIcon = true; 345 } 346 347 setX(x); 348 if (Gravity.isVertical(mGravity)) { 349 return; 350 } 351 352 FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams(); 353 FrameLayout.LayoutParams arrowLp = (FrameLayout.LayoutParams) mArrow.getLayoutParams(); 354 if (mIsAboveIcon) { 355 arrowLp.gravity = lp.gravity = Gravity.BOTTOM; 356 lp.bottomMargin = getPopupContainer().getHeight() - y - getMeasuredHeight() - insets.top; 357 arrowLp.bottomMargin = lp.bottomMargin - arrowLp.height - mArrowOffset - insets.bottom; 358 } else { 359 arrowLp.gravity = lp.gravity = Gravity.TOP; 360 lp.topMargin = y + insets.top; 361 arrowLp.topMargin = lp.topMargin - insets.top - arrowLp.height - mArrowOffset; 362 } 363 } 364 365 @Override onLayout(boolean changed, int l, int t, int r, int b)366 protected void onLayout(boolean changed, int l, int t, int r, int b) { 367 super.onLayout(changed, l, t, r, b); 368 369 // enforce contained is within screen 370 BaseDragLayer dragLayer = getPopupContainer(); 371 Rect insets = dragLayer.getInsets(); 372 if (getTranslationX() + l < insets.left 373 || getTranslationX() + r > dragLayer.getWidth() - insets.right) { 374 // If we are still off screen, center horizontally too. 375 mGravity |= Gravity.CENTER_HORIZONTAL; 376 } 377 378 if (Gravity.isHorizontal(mGravity)) { 379 setX(dragLayer.getWidth() / 2 - getMeasuredWidth() / 2); 380 mArrow.setVisibility(INVISIBLE); 381 } 382 if (Gravity.isVertical(mGravity)) { 383 setY(dragLayer.getHeight() / 2 - getMeasuredHeight() / 2); 384 } 385 } 386 387 @Override getAccessibilityTarget()388 protected Pair<View, String> getAccessibilityTarget() { 389 return Pair.create(this, ""); 390 } 391 392 @Override getAccessibilityInitialFocusView()393 protected View getAccessibilityInitialFocusView() { 394 return getChildCount() > 0 ? getChildAt(0) : this; 395 } 396 animateOpen()397 private void animateOpen() { 398 setVisibility(View.VISIBLE); 399 400 final AnimatorSet openAnim = new AnimatorSet(); 401 final Resources res = getResources(); 402 final long revealDuration = (long) res.getInteger(R.integer.config_popupOpenCloseDuration); 403 final long arrowDuration = res.getInteger(R.integer.config_popupArrowOpenCloseDuration); 404 final TimeInterpolator revealInterpolator = ACCEL_DEACCEL; 405 406 // Rectangular reveal. 407 mEndRect.set(0, 0, getMeasuredWidth(), getMeasuredHeight()); 408 final ValueAnimator revealAnim = createOpenCloseOutlineProvider() 409 .createRevealAnimator(this, false); 410 revealAnim.setDuration(revealDuration); 411 revealAnim.setInterpolator(revealInterpolator); 412 // Clip the popup to the initial outline while the notification dot and arrow animate. 413 revealAnim.start(); 414 revealAnim.pause(); 415 416 ValueAnimator fadeIn = ValueAnimator.ofFloat(0, 1); 417 fadeIn.setDuration(revealDuration + arrowDuration); 418 fadeIn.setInterpolator(revealInterpolator); 419 fadeIn.addUpdateListener(anim -> { 420 float alpha = (float) anim.getAnimatedValue(); 421 mArrow.setAlpha(alpha); 422 setAlpha(revealAnim.isStarted() ? alpha : 0); 423 }); 424 openAnim.play(fadeIn); 425 426 // Animate the arrow. 427 mArrow.setScaleX(0); 428 mArrow.setScaleY(0); 429 Animator arrowScale = ObjectAnimator.ofFloat(mArrow, LauncherAnimUtils.SCALE_PROPERTY, 1) 430 .setDuration(arrowDuration); 431 432 openAnim.addListener(new AnimatorListenerAdapter() { 433 @Override 434 public void onAnimationEnd(Animator animation) { 435 setAlpha(1f); 436 announceAccessibilityChanges(); 437 mOpenCloseAnimator = null; 438 } 439 }); 440 441 mOpenCloseAnimator = openAnim; 442 openAnim.playSequentially(arrowScale, revealAnim); 443 openAnim.start(); 444 } 445 animateClose()446 protected void animateClose() { 447 if (!mIsOpen) { 448 return; 449 } 450 if (getOutlineProvider() instanceof RevealOutlineAnimation) { 451 ((RevealOutlineAnimation) getOutlineProvider()).getOutline(mEndRect); 452 } else { 453 mEndRect.set(0, 0, getMeasuredWidth(), getMeasuredHeight()); 454 } 455 if (mOpenCloseAnimator != null) { 456 mOpenCloseAnimator.cancel(); 457 } 458 mIsOpen = false; 459 460 461 final AnimatorSet closeAnim = new AnimatorSet(); 462 final Resources res = getResources(); 463 final TimeInterpolator revealInterpolator = ACCEL_DEACCEL; 464 final long revealDuration = res.getInteger(R.integer.config_popupOpenCloseDuration); 465 final long arrowDuration = res.getInteger(R.integer.config_popupArrowOpenCloseDuration); 466 467 // Hide the arrow 468 Animator scaleArrow = ObjectAnimator.ofFloat(mArrow, LauncherAnimUtils.SCALE_PROPERTY, 0) 469 .setDuration(arrowDuration); 470 471 // Rectangular reveal (reversed). 472 final ValueAnimator revealAnim = createOpenCloseOutlineProvider() 473 .createRevealAnimator(this, true); 474 revealAnim.setDuration(revealDuration); 475 revealAnim.setInterpolator(revealInterpolator); 476 closeAnim.playSequentially(revealAnim, scaleArrow); 477 478 ValueAnimator fadeOut = ValueAnimator.ofFloat(getAlpha(), 0); 479 fadeOut.setDuration(revealDuration + arrowDuration); 480 fadeOut.setInterpolator(revealInterpolator); 481 fadeOut.addUpdateListener(anim -> { 482 float alpha = (float) anim.getAnimatedValue(); 483 mArrow.setAlpha(alpha); 484 setAlpha(scaleArrow.isStarted() ? 0 : alpha); 485 }); 486 closeAnim.play(fadeOut); 487 488 onCreateCloseAnimation(closeAnim); 489 closeAnim.addListener(new AnimatorListenerAdapter() { 490 @Override 491 public void onAnimationEnd(Animator animation) { 492 mOpenCloseAnimator = null; 493 if (mDeferContainerRemoval) { 494 setVisibility(INVISIBLE); 495 } else { 496 closeComplete(); 497 } 498 } 499 }); 500 mOpenCloseAnimator = closeAnim; 501 closeAnim.start(); 502 } 503 504 /** 505 * Called when creating the close transition allowing subclass can add additional animations. 506 */ onCreateCloseAnimation(AnimatorSet anim)507 protected void onCreateCloseAnimation(AnimatorSet anim) { } 508 createOpenCloseOutlineProvider()509 private RoundedRectRevealOutlineProvider createOpenCloseOutlineProvider() { 510 Resources res = getResources(); 511 int arrowCenterX = res.getDimensionPixelSize(mIsLeftAligned ^ mIsRtl ? 512 R.dimen.popup_arrow_horizontal_center_start: 513 R.dimen.popup_arrow_horizontal_center_end); 514 int halfArrowWidth = res.getDimensionPixelSize(R.dimen.popup_arrow_width) / 2; 515 float arrowCornerRadius = res.getDimension(R.dimen.popup_arrow_corner_radius); 516 if (!mIsLeftAligned) { 517 arrowCenterX = getMeasuredWidth() - arrowCenterX; 518 } 519 int arrowCenterY = mIsAboveIcon ? getMeasuredHeight() : 0; 520 521 mStartRect.set(arrowCenterX - halfArrowWidth, arrowCenterY, arrowCenterX + halfArrowWidth, 522 arrowCenterY); 523 524 return new RoundedRectRevealOutlineProvider 525 (arrowCornerRadius, mOutlineRadius, mStartRect, mEndRect); 526 } 527 528 /** 529 * Closes the popup without animation. 530 */ closeComplete()531 protected void closeComplete() { 532 if (mOpenCloseAnimator != null) { 533 mOpenCloseAnimator.cancel(); 534 mOpenCloseAnimator = null; 535 } 536 mIsOpen = false; 537 mDeferContainerRemoval = false; 538 getPopupContainer().removeView(this); 539 getPopupContainer().removeView(mArrow); 540 } 541 getPopupContainer()542 protected BaseDragLayer getPopupContainer() { 543 return mLauncher.getDragLayer(); 544 } 545 } 546