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