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