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