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