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