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