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