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