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 android.view.View.ALPHA; 20 21 import static com.android.launcher3.BubbleTextView.TEXT_ALPHA_PROPERTY; 22 import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY; 23 import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW; 24 import static com.android.launcher3.graphics.IconShape.getShape; 25 26 import android.animation.Animator; 27 import android.animation.AnimatorListenerAdapter; 28 import android.animation.AnimatorSet; 29 import android.animation.ObjectAnimator; 30 import android.animation.TimeInterpolator; 31 import android.content.Context; 32 import android.content.res.Resources; 33 import android.graphics.Rect; 34 import android.graphics.drawable.GradientDrawable; 35 import android.util.Property; 36 import android.view.View; 37 import android.view.animation.AnimationUtils; 38 39 import com.android.launcher3.BubbleTextView; 40 import com.android.launcher3.CellLayout; 41 import com.android.launcher3.DeviceProfile; 42 import com.android.launcher3.R; 43 import com.android.launcher3.ShortcutAndWidgetContainer; 44 import com.android.launcher3.Utilities; 45 import com.android.launcher3.anim.PropertyResetListener; 46 import com.android.launcher3.celllayout.CellLayoutLayoutParams; 47 import com.android.launcher3.util.Themes; 48 import com.android.launcher3.views.BaseDragLayer; 49 50 import java.util.List; 51 52 /** 53 * Manages the opening and closing animations for a {@link Folder}. 54 * 55 * All of the animations are done in the Folder. 56 * ie. When the user taps on the FolderIcon, we immediately hide the FolderIcon and show the Folder 57 * in its place before starting the animation. 58 */ 59 public class FolderAnimationManager { 60 61 private static final int FOLDER_NAME_ALPHA_DURATION = 32; 62 private static final int LARGE_FOLDER_FOOTER_DURATION = 128; 63 64 private Folder mFolder; 65 private FolderPagedView mContent; 66 private GradientDrawable mFolderBackground; 67 68 private FolderIcon mFolderIcon; 69 private PreviewBackground mPreviewBackground; 70 71 private Context mContext; 72 73 private final boolean mIsOpening; 74 75 private final int mDuration; 76 private final int mDelay; 77 78 private final TimeInterpolator mFolderInterpolator; 79 private final TimeInterpolator mLargeFolderPreviewItemOpenInterpolator; 80 private final TimeInterpolator mLargeFolderPreviewItemCloseInterpolator; 81 82 private final PreviewItemDrawingParams mTmpParams = new PreviewItemDrawingParams(0, 0, 0); 83 private final FolderGridOrganizer mPreviewVerifier; 84 85 private ObjectAnimator mBgColorAnimator; 86 87 private DeviceProfile mDeviceProfile; 88 FolderAnimationManager(Folder folder, boolean isOpening)89 public FolderAnimationManager(Folder folder, boolean isOpening) { 90 mFolder = folder; 91 mContent = folder.mContent; 92 mFolderBackground = (GradientDrawable) mFolder.getBackground(); 93 94 mFolderIcon = folder.mFolderIcon; 95 mPreviewBackground = mFolderIcon.mBackground; 96 97 mContext = folder.getContext(); 98 mDeviceProfile = folder.mActivityContext.getDeviceProfile(); 99 mPreviewVerifier = new FolderGridOrganizer(mDeviceProfile.inv); 100 101 mIsOpening = isOpening; 102 103 Resources res = mContent.getResources(); 104 mDuration = res.getInteger(R.integer.config_materialFolderExpandDuration); 105 mDelay = res.getInteger(R.integer.config_folderDelay); 106 107 mFolderInterpolator = AnimationUtils.loadInterpolator(mContext, 108 R.interpolator.standard_interpolator); 109 mLargeFolderPreviewItemOpenInterpolator = AnimationUtils.loadInterpolator(mContext, 110 R.interpolator.large_folder_preview_item_open_interpolator); 111 mLargeFolderPreviewItemCloseInterpolator = AnimationUtils.loadInterpolator(mContext, 112 R.interpolator.standard_accelerate_interpolator); 113 } 114 115 /** 116 * Returns the animator that changes the background color. 117 */ getBgColorAnimator()118 public ObjectAnimator getBgColorAnimator() { 119 return mBgColorAnimator; 120 } 121 122 /** 123 * Prepares the Folder for animating between open / closed states. 124 */ getAnimator()125 public AnimatorSet getAnimator() { 126 final BaseDragLayer.LayoutParams lp = 127 (BaseDragLayer.LayoutParams) mFolder.getLayoutParams(); 128 mFolderIcon.getPreviewItemManager().recomputePreviewDrawingParams(); 129 ClippedFolderIconLayoutRule rule = mFolderIcon.getLayoutRule(); 130 final List<BubbleTextView> itemsInPreview = getPreviewIconsOnPage(0); 131 132 // Match position of the FolderIcon 133 final Rect folderIconPos = new Rect(); 134 float scaleRelativeToDragLayer = mFolder.mActivityContext.getDragLayer() 135 .getDescendantRectRelativeToSelf(mFolderIcon, folderIconPos); 136 int scaledRadius = mPreviewBackground.getScaledRadius(); 137 float initialSize = (scaledRadius * 2) * scaleRelativeToDragLayer; 138 139 // Match size/scale of icons in the preview 140 float previewScale = rule.scaleForItem(itemsInPreview.size()); 141 float previewSize = rule.getIconSize() * previewScale; 142 float initialScale = previewSize / itemsInPreview.get(0).getIconSize() 143 * scaleRelativeToDragLayer; 144 final float finalScale = 1f; 145 float scale = mIsOpening ? initialScale : finalScale; 146 mFolder.setPivotX(0); 147 mFolder.setPivotY(0); 148 149 // Scale the contents of the folder. 150 mFolder.mContent.setScaleX(scale); 151 mFolder.mContent.setScaleY(scale); 152 mFolder.mContent.setPivotX(0); 153 mFolder.mContent.setPivotY(0); 154 mFolder.mFooter.setScaleX(scale); 155 mFolder.mFooter.setScaleY(scale); 156 mFolder.mFooter.setPivotX(0); 157 mFolder.mFooter.setPivotY(0); 158 159 // We want to create a small X offset for the preview items, so that they follow their 160 // expected path to their final locations. ie. an icon should not move right, if it's final 161 // location is to its left. This value is arbitrarily defined. 162 int previewItemOffsetX = (int) (previewSize / 2); 163 if (Utilities.isRtl(mContext.getResources())) { 164 previewItemOffsetX = (int) (lp.width * initialScale - initialSize - previewItemOffsetX); 165 } 166 167 final int paddingOffsetX = (int) (mContent.getPaddingLeft() * initialScale); 168 final int paddingOffsetY = (int) (mContent.getPaddingTop() * initialScale); 169 170 int initialX = folderIconPos.left + mFolder.getPaddingLeft() 171 + Math.round(mPreviewBackground.getOffsetX() * scaleRelativeToDragLayer) 172 - paddingOffsetX - previewItemOffsetX; 173 int initialY = folderIconPos.top + mFolder.getPaddingTop() 174 + Math.round(mPreviewBackground.getOffsetY() * scaleRelativeToDragLayer) 175 - paddingOffsetY; 176 final float xDistance = initialX - lp.x; 177 final float yDistance = initialY - lp.y; 178 179 // Set up the Folder background. 180 final int initialColor = Themes.getAttrColor(mContext, R.attr.folderPreviewColor); 181 final int finalColor = Themes.getAttrColor(mContext, R.attr.folderBackgroundColor); 182 183 mFolderBackground.mutate(); 184 mFolderBackground.setColor(mIsOpening ? initialColor : finalColor); 185 186 // Set up the reveal animation that clips the Folder. 187 int totalOffsetX = paddingOffsetX + previewItemOffsetX; 188 Rect startRect = new Rect(totalOffsetX, 189 paddingOffsetY, 190 Math.round((totalOffsetX + initialSize)), 191 Math.round((paddingOffsetY + initialSize))); 192 Rect endRect = new Rect(0, 0, lp.width, lp.height); 193 float finalRadius = mFolderBackground.getCornerRadius(); 194 195 // Create the animators. 196 AnimatorSet a = new AnimatorSet(); 197 198 // Initialize the Folder items' text. 199 PropertyResetListener colorResetListener = 200 new PropertyResetListener<>(TEXT_ALPHA_PROPERTY, 1f); 201 for (BubbleTextView icon : mFolder.getItemsOnPage(mFolder.mContent.getCurrentPage())) { 202 if (mIsOpening) { 203 icon.setTextVisibility(false); 204 } 205 ObjectAnimator anim = icon.createTextAlphaAnimator(mIsOpening); 206 anim.addListener(colorResetListener); 207 play(a, anim); 208 } 209 210 mBgColorAnimator = getAnimator(mFolderBackground, "color", initialColor, finalColor); 211 play(a, mBgColorAnimator); 212 play(a, getAnimator(mFolder, View.TRANSLATION_X, xDistance, 0f)); 213 play(a, getAnimator(mFolder, View.TRANSLATION_Y, yDistance, 0f)); 214 play(a, getAnimator(mFolder.mContent, SCALE_PROPERTY, initialScale, finalScale)); 215 play(a, getAnimator(mFolder.mFooter, SCALE_PROPERTY, initialScale, finalScale)); 216 217 final int footerAlphaDuration; 218 final int footerStartDelay; 219 if (isLargeFolder()) { 220 if (mIsOpening) { 221 mFolder.mFooter.setAlpha(0); 222 footerAlphaDuration = LARGE_FOLDER_FOOTER_DURATION; 223 footerStartDelay = mDuration - footerAlphaDuration; 224 } else { 225 footerAlphaDuration = 0; 226 footerStartDelay = 0; 227 } 228 } else { 229 footerStartDelay = 0; 230 footerAlphaDuration = mDuration; 231 } 232 play(a, getAnimator(mFolder.mFooter, ALPHA, 0, 1f), footerStartDelay, footerAlphaDuration); 233 234 // Create reveal animator for the folder background 235 play(a, getShape().createRevealAnimator( 236 mFolder, startRect, endRect, finalRadius, !mIsOpening)); 237 238 // Create reveal animator for the folder content (capture the top 4 icons 2x2) 239 int width = mDeviceProfile.folderCellLayoutBorderSpacePx.x 240 + mDeviceProfile.folderCellWidthPx * 2; 241 int height = mDeviceProfile.folderCellLayoutBorderSpacePx.y 242 + mDeviceProfile.folderCellHeightPx * 2; 243 int page = mIsOpening ? mContent.getCurrentPage() : mContent.getDestinationPage(); 244 int left = mContent.getPaddingLeft() + page * lp.width; 245 Rect contentStart = new Rect(left, 0, left + width, height); 246 Rect contentEnd = new Rect(left, 0, left + lp.width, lp.height); 247 play(a, getShape().createRevealAnimator( 248 mFolder.getContent(), contentStart, contentEnd, finalRadius, !mIsOpening)); 249 250 251 // Fade in the folder name, as the text can overlap the icons when grid size is small. 252 mFolder.mFolderName.setAlpha(mIsOpening ? 0f : 1f); 253 play(a, getAnimator(mFolder.mFolderName, View.ALPHA, 0, 1), 254 mIsOpening ? FOLDER_NAME_ALPHA_DURATION : 0, 255 mIsOpening ? mDuration - FOLDER_NAME_ALPHA_DURATION : FOLDER_NAME_ALPHA_DURATION); 256 257 // Translate the footer so that it tracks the bottom of the content. 258 float normalHeight = mFolder.getContentAreaHeight(); 259 float scaledHeight = normalHeight * initialScale; 260 float diff = normalHeight - scaledHeight; 261 play(a, getAnimator(mFolder.mFooter, View.TRANSLATION_Y, -diff, 0f)); 262 263 // Animate the elevation midway so that the shadow is not noticeable in the background. 264 int midDuration = mDuration / 2; 265 Animator z = getAnimator(mFolder, View.TRANSLATION_Z, -mFolder.getElevation(), 0); 266 play(a, z, mIsOpening ? midDuration : 0, midDuration); 267 268 // Store clip variables. 269 // Because {@link #onAnimationStart} and {@link #onAnimationEnd} callbacks are sent to 270 // message queue and executed on separate frame, we should save states in 271 // {@link #onAnimationStart} instead of before creating animator, so that cancelling 272 // animation A and restarting animation B allows A to reset states in 273 // {@link #onAnimationEnd} before B reads new UI state from {@link #onAnimationStart}. 274 a.addListener(new AnimatorListenerAdapter() { 275 private CellLayout mCellLayout; 276 277 private boolean mFolderClipChildren; 278 private boolean mFolderClipToPadding; 279 private boolean mContentClipChildren; 280 private boolean mContentClipToPadding; 281 private boolean mCellLayoutClipChildren; 282 private boolean mCellLayoutClipPadding; 283 284 @Override 285 public void onAnimationStart(Animator animator) { 286 super.onAnimationStart(animator); 287 mCellLayout = mContent.getCurrentCellLayout(); 288 mFolderClipChildren = mFolder.getClipChildren(); 289 mFolderClipToPadding = mFolder.getClipToPadding(); 290 mContentClipChildren = mContent.getClipChildren(); 291 mContentClipToPadding = mContent.getClipToPadding(); 292 mCellLayoutClipChildren = mCellLayout.getClipChildren(); 293 mCellLayoutClipPadding = mCellLayout.getClipToPadding(); 294 295 mFolder.setClipChildren(false); 296 mFolder.setClipToPadding(false); 297 mContent.setClipChildren(false); 298 mContent.setClipToPadding(false); 299 mCellLayout.setClipChildren(false); 300 mCellLayout.setClipToPadding(false); 301 } 302 303 @Override 304 public void onAnimationEnd(Animator animation) { 305 super.onAnimationEnd(animation); 306 mFolder.setTranslationX(0.0f); 307 mFolder.setTranslationY(0.0f); 308 mFolder.setTranslationZ(0.0f); 309 mFolder.mContent.setScaleX(1f); 310 mFolder.mContent.setScaleY(1f); 311 mFolder.mFooter.setScaleX(1f); 312 mFolder.mFooter.setScaleY(1f); 313 mFolder.mFooter.setTranslationX(0f); 314 mFolder.mFolderName.setAlpha(1f); 315 316 mFolder.setClipChildren(mFolderClipChildren); 317 mFolder.setClipToPadding(mFolderClipToPadding); 318 mContent.setClipChildren(mContentClipChildren); 319 mContent.setClipToPadding(mContentClipToPadding); 320 mCellLayout.setClipChildren(mCellLayoutClipChildren); 321 mCellLayout.setClipToPadding(mCellLayoutClipPadding); 322 } 323 }); 324 325 // We set the interpolator on all current child animators here, because the preview item 326 // animators may use a different interpolator. 327 for (Animator animator : a.getChildAnimations()) { 328 animator.setInterpolator(mFolderInterpolator); 329 } 330 331 int radiusDiff = scaledRadius - mPreviewBackground.getRadius(); 332 addPreviewItemAnimators(a, initialScale / scaleRelativeToDragLayer, 333 // Background can have a scaled radius in drag and drop mode, so we need to add the 334 // difference to keep the preview items centered. 335 (int) (previewItemOffsetX / scaleRelativeToDragLayer) + radiusDiff, radiusDiff); 336 return a; 337 } 338 339 /** 340 * Returns the list of "preview items" on {@param page}. 341 */ getPreviewIconsOnPage(int page)342 private List<BubbleTextView> getPreviewIconsOnPage(int page) { 343 return mPreviewVerifier.setFolderInfo(mFolder.mInfo) 344 .previewItemsForPage(page, mFolder.getIconsInReadingOrder()); 345 } 346 347 /** 348 * Animate the items on the current page. 349 */ addPreviewItemAnimators(AnimatorSet animatorSet, final float folderScale, int previewItemOffsetX, int previewItemOffsetY)350 private void addPreviewItemAnimators(AnimatorSet animatorSet, final float folderScale, 351 int previewItemOffsetX, int previewItemOffsetY) { 352 ClippedFolderIconLayoutRule rule = mFolderIcon.getLayoutRule(); 353 boolean isOnFirstPage = mFolder.mContent.getCurrentPage() == 0; 354 final List<BubbleTextView> itemsInPreview = getPreviewIconsOnPage( 355 isOnFirstPage ? 0 : mFolder.mContent.getCurrentPage()); 356 final int numItemsInPreview = itemsInPreview.size(); 357 final int numItemsInFirstPagePreview = isOnFirstPage 358 ? numItemsInPreview : MAX_NUM_ITEMS_IN_PREVIEW; 359 360 TimeInterpolator previewItemInterpolator = getPreviewItemInterpolator(); 361 362 ShortcutAndWidgetContainer cwc = mContent.getPageAt(0).getShortcutsAndWidgets(); 363 for (int i = 0; i < numItemsInPreview; ++i) { 364 final BubbleTextView btv = itemsInPreview.get(i); 365 CellLayoutLayoutParams btvLp = (CellLayoutLayoutParams) btv.getLayoutParams(); 366 367 // Calculate the final values in the LayoutParams. 368 btvLp.isLockedToGrid = true; 369 cwc.setupLp(btv); 370 371 // Match scale of icons in the preview of the items on the first page. 372 float previewScale = rule.scaleForItem(numItemsInFirstPagePreview); 373 float previewSize = rule.getIconSize() * previewScale; 374 float iconScale = previewSize / itemsInPreview.get(i).getIconSize(); 375 376 final float initialScale = iconScale / folderScale; 377 final float finalScale = 1f; 378 float scale = mIsOpening ? initialScale : finalScale; 379 btv.setScaleX(scale); 380 btv.setScaleY(scale); 381 382 // Match positions of the icons in the folder with their positions in the preview 383 rule.computePreviewItemDrawingParams(i, numItemsInFirstPagePreview, mTmpParams); 384 // The PreviewLayoutRule assumes that the icon size takes up the entire width so we 385 // offset by the actual size. 386 int iconOffsetX = (int) ((btvLp.width - btv.getIconSize()) * iconScale) / 2; 387 388 final int previewPosX = 389 (int) ((mTmpParams.transX - iconOffsetX + previewItemOffsetX) / folderScale); 390 final float paddingTop = btv.getPaddingTop() * iconScale; 391 final int previewPosY = (int) ((mTmpParams.transY + previewItemOffsetY - paddingTop) 392 / folderScale); 393 394 final float xDistance = previewPosX - btvLp.x; 395 final float yDistance = previewPosY - btvLp.y; 396 397 Animator translationX = getAnimator(btv, View.TRANSLATION_X, xDistance, 0f); 398 translationX.setInterpolator(previewItemInterpolator); 399 play(animatorSet, translationX); 400 401 Animator translationY = getAnimator(btv, View.TRANSLATION_Y, yDistance, 0f); 402 translationY.setInterpolator(previewItemInterpolator); 403 play(animatorSet, translationY); 404 405 Animator scaleAnimator = getAnimator(btv, SCALE_PROPERTY, initialScale, finalScale); 406 scaleAnimator.setInterpolator(previewItemInterpolator); 407 play(animatorSet, scaleAnimator); 408 409 if (mFolder.getItemCount() > MAX_NUM_ITEMS_IN_PREVIEW) { 410 // These delays allows the preview items to move as part of the Folder's motion, 411 // and its only necessary for large folders because of differing interpolators. 412 int delay = mIsOpening ? mDelay : mDelay * 2; 413 if (mIsOpening) { 414 translationX.setStartDelay(delay); 415 translationY.setStartDelay(delay); 416 scaleAnimator.setStartDelay(delay); 417 } 418 translationX.setDuration(translationX.getDuration() - delay); 419 translationY.setDuration(translationY.getDuration() - delay); 420 scaleAnimator.setDuration(scaleAnimator.getDuration() - delay); 421 } 422 423 animatorSet.addListener(new AnimatorListenerAdapter() { 424 @Override 425 public void onAnimationStart(Animator animation) { 426 super.onAnimationStart(animation); 427 // Necessary to initialize values here because of the start delay. 428 if (mIsOpening) { 429 btv.setTranslationX(xDistance); 430 btv.setTranslationY(yDistance); 431 btv.setScaleX(initialScale); 432 btv.setScaleY(initialScale); 433 } 434 } 435 436 @Override 437 public void onAnimationEnd(Animator animation) { 438 super.onAnimationEnd(animation); 439 btv.setTranslationX(0.0f); 440 btv.setTranslationY(0.0f); 441 btv.setScaleX(1f); 442 btv.setScaleY(1f); 443 } 444 }); 445 } 446 } 447 play(AnimatorSet as, Animator a)448 private void play(AnimatorSet as, Animator a) { 449 play(as, a, a.getStartDelay(), mDuration); 450 } 451 play(AnimatorSet as, Animator a, long startDelay, int duration)452 private void play(AnimatorSet as, Animator a, long startDelay, int duration) { 453 a.setStartDelay(startDelay); 454 a.setDuration(duration); 455 as.play(a); 456 } 457 isLargeFolder()458 private boolean isLargeFolder() { 459 return mFolder.getItemCount() > MAX_NUM_ITEMS_IN_PREVIEW; 460 } 461 getPreviewItemInterpolator()462 private TimeInterpolator getPreviewItemInterpolator() { 463 if (isLargeFolder()) { 464 // With larger folders, we want the preview items to reach their final positions faster 465 // (when opening) and later (when closing) so that they appear aligned with the rest of 466 // the folder items when they are both visible. 467 return mIsOpening 468 ? mLargeFolderPreviewItemOpenInterpolator 469 : mLargeFolderPreviewItemCloseInterpolator; 470 } 471 return mFolderInterpolator; 472 } 473 getAnimator(View view, Property property, float v1, float v2)474 private Animator getAnimator(View view, Property property, float v1, float v2) { 475 return mIsOpening 476 ? ObjectAnimator.ofFloat(view, property, v1, v2) 477 : ObjectAnimator.ofFloat(view, property, v2, v1); 478 } 479 getAnimator(GradientDrawable drawable, String property, int v1, int v2)480 private ObjectAnimator getAnimator(GradientDrawable drawable, String property, int v1, int v2) { 481 return mIsOpening 482 ? ObjectAnimator.ofArgb(drawable, property, v1, v2) 483 : ObjectAnimator.ofArgb(drawable, property, v2, v1); 484 } 485 } 486