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