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