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