1 /* 2 * Copyright (C) 2017 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.folder; 18 19 import static com.android.launcher3.BubbleTextView.DISPLAY_FOLDER; 20 import static com.android.launcher3.LauncherSettings.Favorites.DESKTOP_ICON_FLAG; 21 import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.ENTER_INDEX; 22 import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.EXIT_INDEX; 23 import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW; 24 import static com.android.launcher3.folder.FolderIcon.DROP_IN_ANIMATION_DURATION; 25 import static com.android.launcher3.graphics.PreloadIconDrawable.newPendingIcon; 26 import static com.android.launcher3.icons.BitmapInfo.FLAG_THEMED; 27 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK; 28 29 import android.animation.Animator; 30 import android.animation.AnimatorListenerAdapter; 31 import android.animation.ObjectAnimator; 32 import android.animation.ValueAnimator; 33 import android.content.Context; 34 import android.graphics.Canvas; 35 import android.graphics.Path; 36 import android.graphics.PointF; 37 import android.graphics.Rect; 38 import android.graphics.drawable.Drawable; 39 import android.util.FloatProperty; 40 import android.view.View; 41 42 import androidx.annotation.NonNull; 43 import androidx.annotation.VisibleForTesting; 44 45 import com.android.launcher3.BubbleTextView; 46 import com.android.launcher3.Flags; 47 import com.android.launcher3.LauncherAppState; 48 import com.android.launcher3.Utilities; 49 import com.android.launcher3.apppairs.AppPairIcon; 50 import com.android.launcher3.apppairs.AppPairIconDrawingParams; 51 import com.android.launcher3.apppairs.AppPairIconGraphic; 52 import com.android.launcher3.model.data.AppPairInfo; 53 import com.android.launcher3.model.data.ItemInfo; 54 import com.android.launcher3.model.data.ItemInfoWithIcon; 55 import com.android.launcher3.model.data.WorkspaceItemInfo; 56 import com.android.launcher3.views.ActivityContext; 57 58 import java.util.ArrayList; 59 import java.util.List; 60 import java.util.function.Predicate; 61 62 /** 63 * Manages the drawing and animations of {@link PreviewItemDrawingParams} for a {@link FolderIcon}. 64 */ 65 public class PreviewItemManager { 66 67 private static final FloatProperty<PreviewItemManager> CURRENT_PAGE_ITEMS_TRANS_X = 68 new FloatProperty<PreviewItemManager>("currentPageItemsTransX") { 69 @Override 70 public void setValue(PreviewItemManager manager, float v) { 71 manager.mCurrentPageItemsTransX = v; 72 manager.onParamsChanged(); 73 } 74 75 @Override 76 public Float get(PreviewItemManager manager) { 77 return manager.mCurrentPageItemsTransX; 78 } 79 }; 80 81 private final Context mContext; 82 private final FolderIcon mIcon; 83 @VisibleForTesting 84 public final int mIconSize; 85 86 // These variables are all associated with the drawing of the preview; they are stored 87 // as member variables for shared usage and to avoid computation on each frame 88 private float mIntrinsicIconSize = -1; 89 private int mTotalWidth = -1; 90 private int mPrevTopPadding = -1; 91 private Drawable mReferenceDrawable = null; 92 93 private int mNumOfPrevItems = 0; 94 95 // These hold the first page preview items 96 private ArrayList<PreviewItemDrawingParams> mFirstPageParams = new ArrayList<>(); 97 // These hold the current page preview items. It is empty if the current page is the first page. 98 private ArrayList<PreviewItemDrawingParams> mCurrentPageParams = new ArrayList<>(); 99 100 // We clip the preview items during the middle of the animation, so that it does not go outside 101 // of the visual shape. We stop clipping at this threshold, since the preview items ultimately 102 // do not get cropped in their resting state. 103 private final float mClipThreshold; 104 private float mCurrentPageItemsTransX = 0; 105 private boolean mShouldSlideInFirstPage; 106 107 static final int INITIAL_ITEM_ANIMATION_DURATION = 350; 108 private static final int FINAL_ITEM_ANIMATION_DURATION = 200; 109 110 private static final int SLIDE_IN_FIRST_PAGE_ANIMATION_DURATION_DELAY = 100; 111 private static final int SLIDE_IN_FIRST_PAGE_ANIMATION_DURATION = 300; 112 private static final int ITEM_SLIDE_IN_OUT_DISTANCE_PX = 200; 113 PreviewItemManager(FolderIcon icon)114 public PreviewItemManager(FolderIcon icon) { 115 mContext = icon.getContext(); 116 mIcon = icon; 117 mIconSize = ActivityContext.lookupContext( 118 mContext).getDeviceProfile().folderChildIconSizePx; 119 mClipThreshold = Utilities.dpToPx(1f); 120 } 121 122 /** 123 * @param reverse If true, animates the final item in the preview to be full size. If false, 124 * animates the first item to its position in the preview. 125 */ createFirstItemAnimation(final boolean reverse, final Runnable onCompleteRunnable)126 public FolderPreviewItemAnim createFirstItemAnimation(final boolean reverse, 127 final Runnable onCompleteRunnable) { 128 return reverse 129 ? new FolderPreviewItemAnim(this, mFirstPageParams.get(0), 0, 2, -1, -1, 130 FINAL_ITEM_ANIMATION_DURATION, onCompleteRunnable) 131 : new FolderPreviewItemAnim(this, mFirstPageParams.get(0), -1, -1, 0, 2, 132 INITIAL_ITEM_ANIMATION_DURATION, onCompleteRunnable); 133 } 134 prepareCreateAnimation(final View destView)135 Drawable prepareCreateAnimation(final View destView) { 136 Drawable animateDrawable = destView instanceof AppPairIcon 137 ? ((AppPairIcon) destView).getIconDrawableArea().getDrawable() 138 : ((BubbleTextView) destView).getIcon(); 139 computePreviewDrawingParams(animateDrawable.getIntrinsicWidth(), 140 destView.getMeasuredWidth()); 141 mReferenceDrawable = animateDrawable; 142 return animateDrawable; 143 } 144 recomputePreviewDrawingParams()145 public void recomputePreviewDrawingParams() { 146 if (mReferenceDrawable != null) { 147 computePreviewDrawingParams(mReferenceDrawable.getIntrinsicWidth(), 148 mIcon.getMeasuredWidth()); 149 } 150 } 151 computePreviewDrawingParams(int drawableSize, int totalSize)152 private void computePreviewDrawingParams(int drawableSize, int totalSize) { 153 if (mIntrinsicIconSize != drawableSize || mTotalWidth != totalSize || 154 mPrevTopPadding != mIcon.getPaddingTop()) { 155 mIntrinsicIconSize = drawableSize; 156 mTotalWidth = totalSize; 157 mPrevTopPadding = mIcon.getPaddingTop(); 158 159 mIcon.mBackground.setup(mIcon.getContext(), mIcon.mActivity, mIcon, mTotalWidth, 160 mIcon.getPaddingTop()); 161 mIcon.mPreviewLayoutRule.init(mIcon.mBackground.previewSize, mIntrinsicIconSize, 162 Utilities.isRtl(mIcon.getResources())); 163 164 updatePreviewItems(false); 165 } 166 } 167 computePreviewItemDrawingParams(int index, int curNumItems, PreviewItemDrawingParams params)168 PreviewItemDrawingParams computePreviewItemDrawingParams(int index, int curNumItems, 169 PreviewItemDrawingParams params) { 170 // We use an index of -1 to represent an icon on the workspace for the destroy and 171 // create animations 172 if (index == -1) { 173 return getFinalIconParams(params); 174 } 175 return mIcon.mPreviewLayoutRule.computePreviewItemDrawingParams(index, curNumItems, params); 176 } 177 getFinalIconParams(PreviewItemDrawingParams params)178 private PreviewItemDrawingParams getFinalIconParams(PreviewItemDrawingParams params) { 179 float iconSize = mIcon.mActivity.getDeviceProfile().iconSizePx; 180 181 final float scale = iconSize / mReferenceDrawable.getIntrinsicWidth(); 182 final float trans = (mIcon.mBackground.previewSize - iconSize) / 2; 183 184 params.update(trans, trans, scale); 185 return params; 186 } 187 drawParams(Canvas canvas, ArrayList<PreviewItemDrawingParams> params, PointF offset, boolean shouldClipPath, Path clipPath)188 public void drawParams(Canvas canvas, ArrayList<PreviewItemDrawingParams> params, 189 PointF offset, boolean shouldClipPath, Path clipPath) { 190 // The first item should be drawn last (ie. on top of later items) 191 for (int i = params.size() - 1; i >= 0; i--) { 192 PreviewItemDrawingParams p = params.get(i); 193 if (!p.hidden) { 194 // Exiting param should always be clipped. 195 boolean isExiting = p.index == EXIT_INDEX; 196 drawPreviewItem(canvas, p, offset, isExiting | shouldClipPath, clipPath); 197 } 198 } 199 } 200 201 /** 202 * Draws the preview items on {@param canvas}. 203 */ draw(Canvas canvas)204 public void draw(Canvas canvas) { 205 int saveCount = canvas.getSaveCount(); 206 // The items are drawn in coordinates relative to the preview offset 207 PreviewBackground bg = mIcon.getFolderBackground(); 208 Path clipPath = bg.getClipPath(); 209 float firstPageItemsTransX = 0; 210 if (mShouldSlideInFirstPage) { 211 PointF firstPageOffset = new PointF(bg.basePreviewOffsetX + mCurrentPageItemsTransX, 212 bg.basePreviewOffsetY); 213 boolean shouldClip = mCurrentPageItemsTransX > mClipThreshold; 214 drawParams(canvas, mCurrentPageParams, firstPageOffset, shouldClip, clipPath); 215 firstPageItemsTransX = -ITEM_SLIDE_IN_OUT_DISTANCE_PX + mCurrentPageItemsTransX; 216 } 217 218 PointF firstPageOffset = new PointF(bg.basePreviewOffsetX + firstPageItemsTransX, 219 bg.basePreviewOffsetY); 220 boolean shouldClipFirstPage = firstPageItemsTransX < -mClipThreshold; 221 drawParams(canvas, mFirstPageParams, firstPageOffset, shouldClipFirstPage, clipPath); 222 canvas.restoreToCount(saveCount); 223 } 224 225 public void onParamsChanged() { 226 mIcon.invalidate(); 227 } 228 229 /** 230 * Draws each preview item. 231 * 232 * @param offset The offset needed to draw the preview items. 233 * @param shouldClipPath Iff true, clip path using {@param clipPath}. 234 * @param clipPath The clip path of the folder icon. 235 */ 236 private void drawPreviewItem(Canvas canvas, PreviewItemDrawingParams params, PointF offset, 237 boolean shouldClipPath, Path clipPath) { 238 canvas.save(); 239 if (shouldClipPath) { 240 canvas.clipPath(clipPath); 241 } 242 canvas.translate(offset.x + params.transX, offset.y + params.transY); 243 canvas.scale(params.scale, params.scale); 244 Drawable d = params.drawable; 245 246 if (d != null) { 247 Rect bounds = d.getBounds(); 248 canvas.save(); 249 canvas.translate(-bounds.left, -bounds.top); 250 canvas.scale(mIntrinsicIconSize / bounds.width(), mIntrinsicIconSize / bounds.height()); 251 d.draw(canvas); 252 canvas.restore(); 253 } 254 canvas.restore(); 255 } 256 257 public void hidePreviewItem(int index, boolean hidden) { 258 // If there are more params than visible in the preview, they are used for enter/exit 259 // animation purposes and they were added to the front of the list. 260 // To index the params properly, we need to skip these params. 261 index = index + Math.max(mFirstPageParams.size() - MAX_NUM_ITEMS_IN_PREVIEW, 0); 262 263 PreviewItemDrawingParams params = index < mFirstPageParams.size() ? 264 mFirstPageParams.get(index) : null; 265 if (params != null) { 266 params.hidden = hidden; 267 } 268 } 269 270 void buildParamsForPage(int page, ArrayList<PreviewItemDrawingParams> params, boolean animate) { 271 List<ItemInfo> items = mIcon.getPreviewItemsOnPage(page); 272 273 // We adjust the size of the list to match the number of items in the preview. 274 while (items.size() < params.size()) { 275 params.remove(params.size() - 1); 276 } 277 while (items.size() > params.size()) { 278 params.add(new PreviewItemDrawingParams(0, 0, 0)); 279 } 280 281 int numItemsInFirstPagePreview = page == 0 ? items.size() : MAX_NUM_ITEMS_IN_PREVIEW; 282 for (int i = 0; i < params.size(); i++) { 283 PreviewItemDrawingParams p = params.get(i); 284 setDrawable(p, items.get(i)); 285 286 if (!animate) { 287 if (p.anim != null) { 288 p.anim.cancel(); 289 } 290 computePreviewItemDrawingParams(i, numItemsInFirstPagePreview, p); 291 if (mReferenceDrawable == null) { 292 mReferenceDrawable = p.drawable; 293 } 294 } else { 295 FolderPreviewItemAnim anim = new FolderPreviewItemAnim(this, p, i, 296 mNumOfPrevItems, i, numItemsInFirstPagePreview, DROP_IN_ANIMATION_DURATION, 297 null); 298 299 if (p.anim != null) { 300 if (p.anim.hasEqualFinalState(anim)) { 301 // do nothing, let the current animation finish 302 continue; 303 } 304 p.anim.cancel(); 305 } 306 p.anim = anim; 307 p.anim.start(); 308 } 309 } 310 } 311 312 void onFolderClose(int currentPage) { 313 // If we are not closing on the first page, we animate the current page preview items 314 // out, and animate the first page preview items in. 315 mShouldSlideInFirstPage = currentPage != 0; 316 if (mShouldSlideInFirstPage) { 317 mCurrentPageItemsTransX = 0; 318 buildParamsForPage(currentPage, mCurrentPageParams, false); 319 onParamsChanged(); 320 321 ValueAnimator slideAnimator = ObjectAnimator 322 .ofFloat(this, CURRENT_PAGE_ITEMS_TRANS_X, 0, ITEM_SLIDE_IN_OUT_DISTANCE_PX); 323 slideAnimator.addListener(new AnimatorListenerAdapter() { 324 @Override 325 public void onAnimationEnd(Animator animation) { 326 mCurrentPageParams.clear(); 327 } 328 }); 329 slideAnimator.setStartDelay(SLIDE_IN_FIRST_PAGE_ANIMATION_DURATION_DELAY); 330 slideAnimator.setDuration(SLIDE_IN_FIRST_PAGE_ANIMATION_DURATION); 331 slideAnimator.start(); 332 } 333 } 334 335 void updatePreviewItems(boolean animate) { 336 int numOfPrevItemsAux = mFirstPageParams.size(); 337 buildParamsForPage(0, mFirstPageParams, animate); 338 mNumOfPrevItems = numOfPrevItemsAux; 339 } 340 341 void updatePreviewItems(Predicate<ItemInfo> itemCheck) { 342 boolean modified = false; 343 for (PreviewItemDrawingParams param : mFirstPageParams) { 344 if (itemCheck.test(param.item) 345 || (param.item instanceof AppPairInfo api && api.anyMatch(itemCheck))) { 346 setDrawable(param, param.item); 347 modified = true; 348 } 349 } 350 for (PreviewItemDrawingParams param : mCurrentPageParams) { 351 if (itemCheck.test(param.item) 352 || (param.item instanceof AppPairInfo api && api.anyMatch(itemCheck))) { 353 setDrawable(param, param.item); 354 modified = true; 355 } 356 } 357 if (modified) { 358 mIcon.invalidate(); 359 } 360 } 361 362 boolean verifyDrawable(@NonNull Drawable who) { 363 for (int i = 0; i < mFirstPageParams.size(); i++) { 364 if (mFirstPageParams.get(i).drawable == who) { 365 return true; 366 } 367 } 368 return false; 369 } 370 371 float getIntrinsicIconSize() { 372 return mIntrinsicIconSize; 373 } 374 375 /** 376 * Handles the case where items in the preview are either: 377 * - Moving into the preview 378 * - Moving into a new position 379 * - Moving out of the preview 380 * 381 * @param oldItems The list of items in the old preview. 382 * @param newItems The list of items in the new preview. 383 * @param dropped The item that was dropped onto the FolderIcon. 384 */ 385 public void onDrop(List<ItemInfo> oldItems, List<ItemInfo> newItems, ItemInfo dropped) { 386 int numItems = newItems.size(); 387 final ArrayList<PreviewItemDrawingParams> params = mFirstPageParams; 388 buildParamsForPage(0, params, false); 389 390 // New preview items for items that are moving in (except for the dropped item). 391 List<ItemInfo> moveIn = new ArrayList<>(); 392 for (ItemInfo newItem : newItems) { 393 if (!oldItems.contains(newItem) && !newItem.equals(dropped)) { 394 moveIn.add(newItem); 395 } 396 } 397 for (int i = 0; i < moveIn.size(); ++i) { 398 int prevIndex = newItems.indexOf(moveIn.get(i)); 399 PreviewItemDrawingParams p = params.get(prevIndex); 400 computePreviewItemDrawingParams(prevIndex, numItems, p); 401 updateTransitionParam(p, moveIn.get(i), ENTER_INDEX, newItems.indexOf(moveIn.get(i)), 402 numItems); 403 } 404 405 // Items that are moving into new positions within the preview. 406 for (int newIndex = 0; newIndex < newItems.size(); ++newIndex) { 407 int oldIndex = oldItems.indexOf(newItems.get(newIndex)); 408 if (oldIndex >= 0 && newIndex != oldIndex) { 409 PreviewItemDrawingParams p = params.get(newIndex); 410 updateTransitionParam(p, newItems.get(newIndex), oldIndex, newIndex, numItems); 411 } 412 } 413 414 // Old preview items that need to be moved out. 415 List<ItemInfo> moveOut = new ArrayList<>(oldItems); 416 moveOut.removeAll(newItems); 417 for (int i = 0; i < moveOut.size(); ++i) { 418 ItemInfo item = moveOut.get(i); 419 int oldIndex = oldItems.indexOf(item); 420 PreviewItemDrawingParams p = computePreviewItemDrawingParams(oldIndex, numItems, null); 421 updateTransitionParam(p, item, oldIndex, EXIT_INDEX, numItems); 422 params.add(0, p); // We want these items first so that they are on drawn last. 423 } 424 425 for (int i = 0; i < params.size(); ++i) { 426 if (params.get(i).anim != null) { 427 params.get(i).anim.start(); 428 } 429 } 430 } 431 432 private void updateTransitionParam(final PreviewItemDrawingParams p, ItemInfo item, 433 int prevIndex, int newIndex, int numItems) { 434 setDrawable(p, item); 435 436 FolderPreviewItemAnim anim = new FolderPreviewItemAnim(this, p, prevIndex, numItems, 437 newIndex, numItems, DROP_IN_ANIMATION_DURATION, null); 438 if (p.anim != null && !p.anim.hasEqualFinalState(anim)) { 439 p.anim.cancel(); 440 } 441 p.anim = anim; 442 } 443 444 @VisibleForTesting 445 public void setDrawable(PreviewItemDrawingParams p, ItemInfo item) { 446 if (item instanceof WorkspaceItemInfo wii) { 447 if (isActivePendingIcon(wii)) { 448 p.drawable = newPendingIcon(mContext, wii); 449 } else { 450 p.drawable = wii.newIcon(mContext, FLAG_THEMED); 451 } 452 p.drawable.setBounds(0, 0, mIconSize, mIconSize); 453 } else if (item instanceof AppPairInfo api) { 454 AppPairIconDrawingParams appPairParams = 455 new AppPairIconDrawingParams(mContext, DISPLAY_FOLDER); 456 p.drawable = AppPairIconGraphic.composeDrawable(api, appPairParams); 457 p.drawable.setBounds(0, 0, mIconSize, mIconSize); 458 } 459 460 p.item = item; 461 // Set the callback to FolderIcon as it is responsible to drawing the icon. The 462 // callback will be released when the folder is opened. 463 p.drawable.setCallback(mIcon); 464 465 // Verify high res 466 if (item instanceof ItemInfoWithIcon info 467 && info.getMatchingLookupFlag().isVisuallyLessThan(DESKTOP_ICON_FLAG)) { 468 LauncherAppState.getInstance(mContext).getIconCache().updateIconInBackground( 469 newInfo -> { 470 if (p.item == newInfo) { 471 setDrawable(p, newInfo); 472 mIcon.invalidate(); 473 } 474 }, info); 475 } 476 } 477 478 /** 479 * Returns true if item is a Promise Icon or actively downloading, and the item is not an 480 * inactive archived app. 481 */ 482 private boolean isActivePendingIcon(WorkspaceItemInfo item) { 483 return (item.hasPromiseIconUi() 484 || (item.runtimeStatusFlags & FLAG_SHOW_DOWNLOAD_PROGRESS_MASK) != 0) 485 && !(Flags.useNewIconForArchivedApps() && item.isInactiveArchive()); 486 } 487 } 488