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