1 /* 2 * Copyright (C) 2019 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 package com.android.launcher3.uioverrides; 17 18 import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL; 19 import static com.android.launcher3.icons.BitmapInfo.FLAG_THEMED; 20 import static com.android.launcher3.icons.FastBitmapDrawable.getDisabledColorFilter; 21 22 import android.animation.Animator; 23 import android.animation.AnimatorSet; 24 import android.animation.ArgbEvaluator; 25 import android.animation.Keyframe; 26 import android.animation.ObjectAnimator; 27 import android.animation.PropertyValuesHolder; 28 import android.animation.ValueAnimator; 29 import android.annotation.Nullable; 30 import android.content.Context; 31 import android.graphics.BlurMaskFilter; 32 import android.graphics.Canvas; 33 import android.graphics.Color; 34 import android.graphics.Matrix; 35 import android.graphics.Paint; 36 import android.graphics.Path; 37 import android.graphics.Rect; 38 import android.graphics.drawable.Drawable; 39 import android.os.Process; 40 import android.util.AttributeSet; 41 import android.util.FloatProperty; 42 import android.view.LayoutInflater; 43 import android.view.ViewGroup; 44 45 import androidx.core.graphics.ColorUtils; 46 47 import com.android.launcher3.CellLayout; 48 import com.android.launcher3.DeviceProfile; 49 import com.android.launcher3.Launcher; 50 import com.android.launcher3.LauncherSettings; 51 import com.android.launcher3.R; 52 import com.android.launcher3.anim.AnimatorListeners; 53 import com.android.launcher3.celllayout.CellLayoutLayoutParams; 54 import com.android.launcher3.icons.BitmapInfo; 55 import com.android.launcher3.icons.GraphicsUtils; 56 import com.android.launcher3.icons.IconNormalizer; 57 import com.android.launcher3.icons.LauncherIcons; 58 import com.android.launcher3.model.data.ItemInfoWithIcon; 59 import com.android.launcher3.model.data.WorkspaceItemInfo; 60 import com.android.launcher3.touch.ItemClickHandler; 61 import com.android.launcher3.touch.ItemLongClickListener; 62 import com.android.launcher3.util.SafeCloseable; 63 import com.android.launcher3.views.ActivityContext; 64 import com.android.launcher3.views.DoubleShadowBubbleTextView; 65 66 import java.util.ArrayList; 67 import java.util.Collections; 68 import java.util.List; 69 70 /** 71 * A BubbleTextView with a ring around it's drawable 72 */ 73 public class PredictedAppIcon extends DoubleShadowBubbleTextView { 74 75 private static final int RING_SHADOW_COLOR = 0x99000000; 76 private static final float RING_EFFECT_RATIO = 0.095f; 77 78 private static final long ICON_CHANGE_ANIM_DURATION = 360; 79 private static final long ICON_CHANGE_ANIM_STAGGER = 50; 80 81 boolean mIsDrawingDot = false; 82 private final DeviceProfile mDeviceProfile; 83 private final Paint mIconRingPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 84 private final Path mRingPath = new Path(); 85 private final int mNormalizedIconSize; 86 private final Path mShapePath; 87 private final Matrix mTmpMatrix = new Matrix(); 88 89 private final BlurMaskFilter mShadowFilter; 90 91 private boolean mIsPinned = false; 92 private int mPlateColor; 93 boolean mDrawForDrag = false; 94 95 // Used for the "slot-machine" education animation. 96 private List<Drawable> mSlotMachineIcons; 97 private Animator mSlotMachineAnim; 98 private float mSlotMachineIconTranslationY; 99 100 private static final FloatProperty<PredictedAppIcon> SLOT_MACHINE_TRANSLATION_Y = 101 new FloatProperty<PredictedAppIcon>("slotMachineTranslationY") { 102 @Override 103 public void setValue(PredictedAppIcon predictedAppIcon, float transY) { 104 predictedAppIcon.mSlotMachineIconTranslationY = transY; 105 predictedAppIcon.invalidate(); 106 } 107 108 @Override 109 public Float get(PredictedAppIcon predictedAppIcon) { 110 return predictedAppIcon.mSlotMachineIconTranslationY; 111 } 112 }; 113 PredictedAppIcon(Context context)114 public PredictedAppIcon(Context context) { 115 this(context, null, 0); 116 } 117 PredictedAppIcon(Context context, AttributeSet attrs)118 public PredictedAppIcon(Context context, AttributeSet attrs) { 119 this(context, attrs, 0); 120 } 121 PredictedAppIcon(Context context, AttributeSet attrs, int defStyle)122 public PredictedAppIcon(Context context, AttributeSet attrs, int defStyle) { 123 super(context, attrs, defStyle); 124 mDeviceProfile = ActivityContext.lookupContext(context).getDeviceProfile(); 125 mNormalizedIconSize = IconNormalizer.getNormalizedCircleSize(getIconSize()); 126 int shadowSize = context.getResources().getDimensionPixelSize( 127 R.dimen.blur_size_thin_outline); 128 mShadowFilter = new BlurMaskFilter(shadowSize, BlurMaskFilter.Blur.OUTER); 129 mShapePath = GraphicsUtils.getShapePath(context, mNormalizedIconSize); 130 } 131 132 @Override onDraw(Canvas canvas)133 public void onDraw(Canvas canvas) { 134 int count = canvas.save(); 135 boolean isSlotMachineAnimRunning = mSlotMachineAnim != null; 136 if (!mIsPinned) { 137 drawEffect(canvas); 138 if (isSlotMachineAnimRunning) { 139 // Clip to to outside of the ring during the slot machine animation. 140 canvas.clipPath(mRingPath); 141 } 142 canvas.translate(getWidth() * RING_EFFECT_RATIO, getHeight() * RING_EFFECT_RATIO); 143 canvas.scale(1 - 2 * RING_EFFECT_RATIO, 1 - 2 * RING_EFFECT_RATIO); 144 } 145 if (isSlotMachineAnimRunning) { 146 drawSlotMachineIcons(canvas); 147 } else { 148 super.onDraw(canvas); 149 } 150 canvas.restoreToCount(count); 151 } 152 drawSlotMachineIcons(Canvas canvas)153 private void drawSlotMachineIcons(Canvas canvas) { 154 canvas.translate((getWidth() - getIconSize()) / 2f, 155 (getHeight() - getIconSize()) / 2f + mSlotMachineIconTranslationY); 156 for (Drawable icon : mSlotMachineIcons) { 157 icon.setBounds(0, 0, getIconSize(), getIconSize()); 158 icon.draw(canvas); 159 canvas.translate(0, getSlotMachineIconPlusSpacingSize()); 160 } 161 } 162 getSlotMachineIconPlusSpacingSize()163 private float getSlotMachineIconPlusSpacingSize() { 164 return getIconSize() + getOutlineOffsetY(); 165 } 166 167 @Override drawDotIfNecessary(Canvas canvas)168 protected void drawDotIfNecessary(Canvas canvas) { 169 mIsDrawingDot = true; 170 int count = canvas.save(); 171 canvas.translate(-getWidth() * RING_EFFECT_RATIO, -getHeight() * RING_EFFECT_RATIO); 172 canvas.scale(1 + 2 * RING_EFFECT_RATIO, 1 + 2 * RING_EFFECT_RATIO); 173 super.drawDotIfNecessary(canvas); 174 canvas.restoreToCount(count); 175 mIsDrawingDot = false; 176 } 177 178 @Override applyFromWorkspaceItem(WorkspaceItemInfo info, boolean animate, int staggerIndex)179 public void applyFromWorkspaceItem(WorkspaceItemInfo info, boolean animate, int staggerIndex) { 180 // Create the slot machine animation first, since it uses the current icon to start. 181 Animator slotMachineAnim = animate 182 ? createSlotMachineAnim(Collections.singletonList(info.bitmap), false) 183 : null; 184 super.applyFromWorkspaceItem(info, animate, staggerIndex); 185 int oldPlateColor = mPlateColor; 186 int newPlateColor = ColorUtils.setAlphaComponent(mDotParams.appColor, 200); 187 if (!animate) { 188 mPlateColor = newPlateColor; 189 } 190 if (mIsPinned) { 191 setContentDescription(info.contentDescription); 192 } else { 193 setContentDescription( 194 getContext().getString(R.string.hotseat_prediction_content_description, 195 info.contentDescription)); 196 } 197 198 if (animate) { 199 ValueAnimator plateColorAnim = ValueAnimator.ofObject(new ArgbEvaluator(), 200 oldPlateColor, newPlateColor); 201 plateColorAnim.addUpdateListener(valueAnimator -> { 202 mPlateColor = (int) valueAnimator.getAnimatedValue(); 203 invalidate(); 204 }); 205 AnimatorSet changeIconAnim = new AnimatorSet(); 206 if (slotMachineAnim != null) { 207 changeIconAnim.play(slotMachineAnim); 208 } 209 changeIconAnim.play(plateColorAnim); 210 changeIconAnim.setStartDelay(staggerIndex * ICON_CHANGE_ANIM_STAGGER); 211 changeIconAnim.setDuration(ICON_CHANGE_ANIM_DURATION).start(); 212 } 213 } 214 215 /** 216 * Returns an Animator that translates the given icons in a "slot-machine" fashion, beginning 217 * and ending with the original icon. 218 */ createSlotMachineAnim(List<BitmapInfo> iconsToAnimate)219 public @Nullable Animator createSlotMachineAnim(List<BitmapInfo> iconsToAnimate) { 220 return createSlotMachineAnim(iconsToAnimate, true); 221 } 222 223 /** 224 * Returns an Animator that translates the given icons in a "slot-machine" fashion, beginning 225 * with the original icon, then cycling through the given icons, optionally ending back with 226 * the original icon. 227 * @param endWithOriginalIcon Whether we should land back on the icon we started with, rather 228 * than the last item in iconsToAnimate. 229 */ createSlotMachineAnim(List<BitmapInfo> iconsToAnimate, boolean endWithOriginalIcon)230 public @Nullable Animator createSlotMachineAnim(List<BitmapInfo> iconsToAnimate, 231 boolean endWithOriginalIcon) { 232 if (mIsPinned || iconsToAnimate == null || iconsToAnimate.isEmpty()) { 233 return null; 234 } 235 if (mSlotMachineAnim != null) { 236 mSlotMachineAnim.end(); 237 } 238 239 // Bookend the other animating icons with the original icon on both ends. 240 mSlotMachineIcons = new ArrayList<>(iconsToAnimate.size() + 2); 241 mSlotMachineIcons.add(getIcon()); 242 iconsToAnimate.stream() 243 .map(iconInfo -> iconInfo.newIcon(mContext, FLAG_THEMED)) 244 .forEach(mSlotMachineIcons::add); 245 if (endWithOriginalIcon) { 246 mSlotMachineIcons.add(getIcon()); 247 } 248 249 float finalTrans = -getSlotMachineIconPlusSpacingSize() * (mSlotMachineIcons.size() - 1); 250 Keyframe[] keyframes = new Keyframe[] { 251 Keyframe.ofFloat(0f, 0f), 252 Keyframe.ofFloat(0.82f, finalTrans - getOutlineOffsetY() / 2f), // Overshoot 253 Keyframe.ofFloat(1f, finalTrans) // Ease back into the final position 254 }; 255 keyframes[1].setInterpolator(ACCEL_DEACCEL); 256 keyframes[2].setInterpolator(ACCEL_DEACCEL); 257 258 mSlotMachineAnim = ObjectAnimator.ofPropertyValuesHolder(this, 259 PropertyValuesHolder.ofKeyframe(SLOT_MACHINE_TRANSLATION_Y, keyframes)); 260 mSlotMachineAnim.addListener(AnimatorListeners.forEndCallback(() -> { 261 mSlotMachineIcons = null; 262 mSlotMachineAnim = null; 263 mSlotMachineIconTranslationY = 0; 264 invalidate(); 265 })); 266 return mSlotMachineAnim; 267 } 268 269 /** 270 * Removes prediction ring from app icon 271 */ pin(WorkspaceItemInfo info)272 public void pin(WorkspaceItemInfo info) { 273 if (mIsPinned) return; 274 mIsPinned = true; 275 applyFromWorkspaceItem(info); 276 setOnLongClickListener(ItemLongClickListener.INSTANCE_WORKSPACE); 277 ((CellLayoutLayoutParams) getLayoutParams()).canReorder = true; 278 invalidate(); 279 } 280 281 /** 282 * prepares prediction icon for usage after bind 283 */ finishBinding(OnLongClickListener longClickListener)284 public void finishBinding(OnLongClickListener longClickListener) { 285 setOnLongClickListener(longClickListener); 286 ((CellLayoutLayoutParams) getLayoutParams()).canReorder = false; 287 setTextVisibility(false); 288 verifyHighRes(); 289 } 290 291 @Override getIconBounds(Rect outBounds)292 public void getIconBounds(Rect outBounds) { 293 super.getIconBounds(outBounds); 294 if (!mIsPinned && !mIsDrawingDot) { 295 int predictionInset = (int) (getIconSize() * RING_EFFECT_RATIO); 296 outBounds.inset(predictionInset, predictionInset); 297 } 298 } 299 isPinned()300 public boolean isPinned() { 301 return mIsPinned; 302 } 303 getOutlineOffsetX()304 private int getOutlineOffsetX() { 305 return (getMeasuredWidth() - mNormalizedIconSize) / 2; 306 } 307 getOutlineOffsetY()308 private int getOutlineOffsetY() { 309 if (mDisplay != DISPLAY_TASKBAR) { 310 return getPaddingTop() + mDeviceProfile.folderIconOffsetYPx; 311 } 312 return (getMeasuredHeight() - mNormalizedIconSize) / 2; 313 } 314 315 @Override onSizeChanged(int w, int h, int oldw, int oldh)316 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 317 super.onSizeChanged(w, h, oldw, oldh); 318 updateRingPath(); 319 } 320 321 @Override setTag(Object tag)322 public void setTag(Object tag) { 323 super.setTag(tag); 324 updateRingPath(); 325 } 326 updateRingPath()327 private void updateRingPath() { 328 boolean isBadged = false; 329 if (getTag() instanceof WorkspaceItemInfo) { 330 WorkspaceItemInfo info = (WorkspaceItemInfo) getTag(); 331 isBadged = !Process.myUserHandle().equals(info.user) 332 || info.itemType == LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT 333 || info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT; 334 } 335 336 mRingPath.reset(); 337 mTmpMatrix.setTranslate(getOutlineOffsetX(), getOutlineOffsetY()); 338 339 mRingPath.addPath(mShapePath, mTmpMatrix); 340 if (isBadged) { 341 float outlineSize = mNormalizedIconSize * RING_EFFECT_RATIO; 342 float iconSize = getIconSize() * (1 - 2 * RING_EFFECT_RATIO); 343 float badgeSize = LauncherIcons.getBadgeSizeForIconSize((int) iconSize) + outlineSize; 344 float scale = badgeSize / mNormalizedIconSize; 345 mTmpMatrix.postTranslate(mNormalizedIconSize, mNormalizedIconSize); 346 mTmpMatrix.preScale(scale, scale); 347 mTmpMatrix.preTranslate(-mNormalizedIconSize, -mNormalizedIconSize); 348 mRingPath.addPath(mShapePath, mTmpMatrix); 349 } 350 } 351 drawEffect(Canvas canvas)352 private void drawEffect(Canvas canvas) { 353 // Don't draw ring effect if item is about to be dragged. 354 if (mDrawForDrag) { 355 return; 356 } 357 mIconRingPaint.setColor(RING_SHADOW_COLOR); 358 mIconRingPaint.setMaskFilter(mShadowFilter); 359 canvas.drawPath(mRingPath, mIconRingPaint); 360 mIconRingPaint.setColor(mPlateColor); 361 mIconRingPaint.setMaskFilter(null); 362 canvas.drawPath(mRingPath, mIconRingPaint); 363 } 364 365 @Override setIconDisabled(boolean isDisabled)366 public void setIconDisabled(boolean isDisabled) { 367 super.setIconDisabled(isDisabled); 368 mIconRingPaint.setColorFilter(isDisabled ? getDisabledColorFilter() : null); 369 invalidate(); 370 } 371 372 @Override setItemInfo(ItemInfoWithIcon itemInfo)373 protected void setItemInfo(ItemInfoWithIcon itemInfo) { 374 super.setItemInfo(itemInfo); 375 setIconDisabled(itemInfo.isDisabled()); 376 } 377 378 @Override getSourceVisualDragBounds(Rect bounds)379 public void getSourceVisualDragBounds(Rect bounds) { 380 super.getSourceVisualDragBounds(bounds); 381 if (!mIsPinned) { 382 int internalSize = (int) (bounds.width() * RING_EFFECT_RATIO); 383 bounds.inset(internalSize, internalSize); 384 } 385 } 386 387 @Override prepareDrawDragView()388 public SafeCloseable prepareDrawDragView() { 389 mDrawForDrag = true; 390 invalidate(); 391 SafeCloseable r = super.prepareDrawDragView(); 392 return () -> { 393 r.close(); 394 mDrawForDrag = false; 395 }; 396 } 397 398 /** 399 * Creates and returns a new instance of PredictedAppIcon from WorkspaceItemInfo 400 */ createIcon(ViewGroup parent, WorkspaceItemInfo info)401 public static PredictedAppIcon createIcon(ViewGroup parent, WorkspaceItemInfo info) { 402 PredictedAppIcon icon = (PredictedAppIcon) LayoutInflater.from(parent.getContext()) 403 .inflate(R.layout.predicted_app_icon, parent, false); 404 icon.applyFromWorkspaceItem(info); 405 icon.setOnClickListener(ItemClickHandler.INSTANCE); 406 icon.setOnFocusChangeListener(Launcher.getLauncher(parent.getContext()).getFocusHandler()); 407 return icon; 408 } 409 410 /** 411 * Draws Predicted Icon outline on cell layout 412 */ 413 public static class PredictedIconOutlineDrawing extends CellLayout.DelegatedCellDrawing { 414 415 private final PredictedAppIcon mIcon; 416 private final Paint mOutlinePaint = new Paint(Paint.ANTI_ALIAS_FLAG); 417 PredictedIconOutlineDrawing(int cellX, int cellY, PredictedAppIcon icon)418 public PredictedIconOutlineDrawing(int cellX, int cellY, PredictedAppIcon icon) { 419 mDelegateCellX = cellX; 420 mDelegateCellY = cellY; 421 mIcon = icon; 422 mOutlinePaint.setStyle(Paint.Style.FILL); 423 mOutlinePaint.setColor(Color.argb(24, 245, 245, 245)); 424 } 425 426 /** 427 * Draws predicted app icon outline under CellLayout 428 */ 429 @Override drawUnderItem(Canvas canvas)430 public void drawUnderItem(Canvas canvas) { 431 canvas.save(); 432 canvas.translate(mIcon.getOutlineOffsetX(), mIcon.getOutlineOffsetY()); 433 canvas.drawPath(mIcon.mShapePath, mOutlinePaint); 434 canvas.restore(); 435 } 436 437 /** 438 * Draws PredictedAppIcon outline over CellLayout 439 */ 440 @Override drawOverItem(Canvas canvas)441 public void drawOverItem(Canvas canvas) { 442 // Does nothing 443 } 444 } 445 } 446