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