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 Folder mFolder; 63 private FolderPagedView mContent; 64 private GradientDrawable mFolderBackground; 65 66 private FolderIcon mFolderIcon; 67 private PreviewBackground mPreviewBackground; 68 69 private Context mContext; 70 private Launcher mLauncher; 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, 0); 82 83 FolderAnimationManager(Folder folder, boolean isOpening)84 public FolderAnimationManager(Folder folder, boolean isOpening) { 85 mFolder = folder; 86 mContent = folder.mContent; 87 mFolderBackground = (GradientDrawable) mFolder.getBackground(); 88 89 mFolderIcon = folder.mFolderIcon; 90 mPreviewBackground = mFolderIcon.mBackground; 91 92 mContext = folder.getContext(); 93 mLauncher = folder.mLauncher; 94 95 mIsOpening = isOpening; 96 97 Resources res = mContent.getResources(); 98 mDuration = res.getInteger(R.integer.config_materialFolderExpandDuration); 99 mDelay = res.getInteger(R.integer.config_folderDelay); 100 101 mFolderInterpolator = AnimationUtils.loadInterpolator(mContext, 102 R.interpolator.folder_interpolator); 103 mLargeFolderPreviewItemOpenInterpolator = AnimationUtils.loadInterpolator(mContext, 104 R.interpolator.large_folder_preview_item_open_interpolator); 105 mLargeFolderPreviewItemCloseInterpolator = AnimationUtils.loadInterpolator(mContext, 106 R.interpolator.large_folder_preview_item_close_interpolator); 107 } 108 109 110 /** 111 * Prepares the Folder for animating between open / closed states. 112 */ getAnimator()113 public AnimatorSet getAnimator() { 114 final DragLayer.LayoutParams lp = (DragLayer.LayoutParams) mFolder.getLayoutParams(); 115 ClippedFolderIconLayoutRule rule = mFolderIcon.getLayoutRule(); 116 final List<BubbleTextView> itemsInPreview = mFolderIcon.getPreviewItems(); 117 118 // Match position of the FolderIcon 119 final Rect folderIconPos = new Rect(); 120 float scaleRelativeToDragLayer = mLauncher.getDragLayer() 121 .getDescendantRectRelativeToSelf(mFolderIcon, folderIconPos); 122 int scaledRadius = mPreviewBackground.getScaledRadius(); 123 float initialSize = (scaledRadius * 2) * scaleRelativeToDragLayer; 124 125 // Match size/scale of icons in the preview 126 float previewScale = rule.scaleForItem(itemsInPreview.size()); 127 float previewSize = rule.getIconSize() * previewScale; 128 float initialScale = previewSize / itemsInPreview.get(0).getIconSize() 129 * scaleRelativeToDragLayer; 130 final float finalScale = 1f; 131 float scale = mIsOpening ? initialScale : finalScale; 132 mFolder.setScaleX(scale); 133 mFolder.setScaleY(scale); 134 mFolder.setPivotX(0); 135 mFolder.setPivotY(0); 136 137 // We want to create a small X offset for the preview items, so that they follow their 138 // expected path to their final locations. ie. an icon should not move right, if it's final 139 // location is to its left. This value is arbitrarily defined. 140 int previewItemOffsetX = (int) (previewSize / 2); 141 if (Utilities.isRtl(mContext.getResources())) { 142 previewItemOffsetX = (int) (lp.width * initialScale - initialSize - previewItemOffsetX); 143 } 144 145 final int paddingOffsetX = (int) ((mFolder.getPaddingLeft() + mContent.getPaddingLeft()) 146 * initialScale); 147 final int paddingOffsetY = (int) ((mFolder.getPaddingTop() + mContent.getPaddingTop()) 148 * initialScale); 149 150 int initialX = folderIconPos.left + mPreviewBackground.getOffsetX() - paddingOffsetX 151 - previewItemOffsetX; 152 int initialY = folderIconPos.top + mPreviewBackground.getOffsetY() - paddingOffsetY; 153 final float xDistance = initialX - lp.x; 154 final float yDistance = initialY - lp.y; 155 156 // Set up the Folder background. 157 final int finalColor = ColorUtils.setAlphaComponent( 158 Themes.getAttrColor(mContext, R.attr.folderFillColor), 255); 159 final int initialColor = setColorAlphaBound( 160 finalColor, mPreviewBackground.getBackgroundAlpha()); 161 mFolderBackground.mutate(); 162 mFolderBackground.setColor(mIsOpening ? initialColor : finalColor); 163 164 // Set up the reveal animation that clips the Folder. 165 int totalOffsetX = paddingOffsetX + previewItemOffsetX; 166 Rect startRect = new Rect( 167 Math.round(totalOffsetX / initialScale), 168 Math.round(paddingOffsetY / initialScale), 169 Math.round((totalOffsetX + initialSize) / initialScale), 170 Math.round((paddingOffsetY + initialSize) / initialScale)); 171 Rect endRect = new Rect(0, 0, lp.width, lp.height); 172 float finalRadius = ResourceUtils.pxFromDp(2, mContext.getResources().getDisplayMetrics()); 173 174 // Create the animators. 175 AnimatorSet a = new AnimatorSet(); 176 177 // Initialize the Folder items' text. 178 PropertyResetListener colorResetListener = 179 new PropertyResetListener<>(TEXT_ALPHA_PROPERTY, 1f); 180 for (BubbleTextView icon : mFolder.getItemsOnPage(mFolder.mContent.getCurrentPage())) { 181 if (mIsOpening) { 182 icon.setTextVisibility(false); 183 } 184 ObjectAnimator anim = icon.createTextAlphaAnimator(mIsOpening); 185 anim.addListener(colorResetListener); 186 play(a, anim); 187 } 188 189 play(a, getAnimator(mFolder, View.TRANSLATION_X, xDistance, 0f)); 190 play(a, getAnimator(mFolder, View.TRANSLATION_Y, yDistance, 0f)); 191 play(a, getAnimator(mFolder, SCALE_PROPERTY, initialScale, finalScale)); 192 play(a, getAnimator(mFolderBackground, "color", initialColor, finalColor)); 193 play(a, mFolderIcon.mFolderName.createTextAlphaAnimator(!mIsOpening)); 194 play(a, getShape().createRevealAnimator( 195 mFolder, startRect, endRect, finalRadius, !mIsOpening)); 196 197 // Animate the elevation midway so that the shadow is not noticeable in the background. 198 int midDuration = mDuration / 2; 199 Animator z = getAnimator(mFolder, View.TRANSLATION_Z, -mFolder.getElevation(), 0); 200 play(a, z, mIsOpening ? midDuration : 0, midDuration); 201 202 a.addListener(new AnimatorListenerAdapter() { 203 @Override 204 public void onAnimationEnd(Animator animation) { 205 super.onAnimationEnd(animation); 206 mFolder.setTranslationX(0.0f); 207 mFolder.setTranslationY(0.0f); 208 mFolder.setTranslationZ(0.0f); 209 mFolder.setScaleX(1f); 210 mFolder.setScaleY(1f); 211 } 212 }); 213 214 // We set the interpolator on all current child animators here, because the preview item 215 // animators may use a different interpolator. 216 for (Animator animator : a.getChildAnimations()) { 217 animator.setInterpolator(mFolderInterpolator); 218 } 219 220 int radiusDiff = scaledRadius - mPreviewBackground.getRadius(); 221 addPreviewItemAnimators(a, initialScale / scaleRelativeToDragLayer, 222 // Background can have a scaled radius in drag and drop mode, so we need to add the 223 // difference to keep the preview items centered. 224 previewItemOffsetX + radiusDiff, radiusDiff); 225 return a; 226 } 227 228 /** 229 * Animate the items on the current page. 230 */ addPreviewItemAnimators(AnimatorSet animatorSet, final float folderScale, int previewItemOffsetX, int previewItemOffsetY)231 private void addPreviewItemAnimators(AnimatorSet animatorSet, final float folderScale, 232 int previewItemOffsetX, int previewItemOffsetY) { 233 ClippedFolderIconLayoutRule rule = mFolderIcon.getLayoutRule(); 234 boolean isOnFirstPage = mFolder.mContent.getCurrentPage() == 0; 235 final List<BubbleTextView> itemsInPreview = isOnFirstPage 236 ? mFolderIcon.getPreviewItems() 237 : mFolderIcon.getPreviewItemsOnPage(mFolder.mContent.getCurrentPage()); 238 final int numItemsInPreview = itemsInPreview.size(); 239 final int numItemsInFirstPagePreview = isOnFirstPage 240 ? numItemsInPreview : MAX_NUM_ITEMS_IN_PREVIEW; 241 242 TimeInterpolator previewItemInterpolator = getPreviewItemInterpolator(); 243 244 ShortcutAndWidgetContainer cwc = mContent.getPageAt(0).getShortcutsAndWidgets(); 245 for (int i = 0; i < numItemsInPreview; ++i) { 246 final BubbleTextView btv = itemsInPreview.get(i); 247 CellLayout.LayoutParams btvLp = (CellLayout.LayoutParams) btv.getLayoutParams(); 248 249 // Calculate the final values in the LayoutParams. 250 btvLp.isLockedToGrid = true; 251 cwc.setupLp(btv); 252 253 // Match scale of icons in the preview of the items on the first page. 254 float previewScale = rule.scaleForItem(numItemsInFirstPagePreview); 255 float previewSize = rule.getIconSize() * previewScale; 256 float iconScale = previewSize / itemsInPreview.get(i).getIconSize(); 257 258 final float initialScale = iconScale / folderScale; 259 final float finalScale = 1f; 260 float scale = mIsOpening ? initialScale : finalScale; 261 btv.setScaleX(scale); 262 btv.setScaleY(scale); 263 264 // Match positions of the icons in the folder with their positions in the preview 265 rule.computePreviewItemDrawingParams(i, numItemsInFirstPagePreview, mTmpParams); 266 // The PreviewLayoutRule assumes that the icon size takes up the entire width so we 267 // offset by the actual size. 268 int iconOffsetX = (int) ((btvLp.width - btv.getIconSize()) * iconScale) / 2; 269 270 final int previewPosX = 271 (int) ((mTmpParams.transX - iconOffsetX + previewItemOffsetX) / folderScale); 272 final int previewPosY = (int) ((mTmpParams.transY + previewItemOffsetY) / folderScale); 273 274 final float xDistance = previewPosX - btvLp.x; 275 final float yDistance = previewPosY - btvLp.y; 276 277 Animator translationX = getAnimator(btv, View.TRANSLATION_X, xDistance, 0f); 278 translationX.setInterpolator(previewItemInterpolator); 279 play(animatorSet, translationX); 280 281 Animator translationY = getAnimator(btv, View.TRANSLATION_Y, yDistance, 0f); 282 translationY.setInterpolator(previewItemInterpolator); 283 play(animatorSet, translationY); 284 285 Animator scaleAnimator = getAnimator(btv, SCALE_PROPERTY, initialScale, finalScale); 286 scaleAnimator.setInterpolator(previewItemInterpolator); 287 play(animatorSet, scaleAnimator); 288 289 if (mFolder.getItemCount() > MAX_NUM_ITEMS_IN_PREVIEW) { 290 // These delays allows the preview items to move as part of the Folder's motion, 291 // and its only necessary for large folders because of differing interpolators. 292 int delay = mIsOpening ? mDelay : mDelay * 2; 293 if (mIsOpening) { 294 translationX.setStartDelay(delay); 295 translationY.setStartDelay(delay); 296 scaleAnimator.setStartDelay(delay); 297 } 298 translationX.setDuration(translationX.getDuration() - delay); 299 translationY.setDuration(translationY.getDuration() - delay); 300 scaleAnimator.setDuration(scaleAnimator.getDuration() - delay); 301 } 302 303 animatorSet.addListener(new AnimatorListenerAdapter() { 304 @Override 305 public void onAnimationStart(Animator animation) { 306 super.onAnimationStart(animation); 307 // Necessary to initialize values here because of the start delay. 308 if (mIsOpening) { 309 btv.setTranslationX(xDistance); 310 btv.setTranslationY(yDistance); 311 btv.setScaleX(initialScale); 312 btv.setScaleY(initialScale); 313 } 314 } 315 316 @Override 317 public void onAnimationEnd(Animator animation) { 318 super.onAnimationEnd(animation); 319 btv.setTranslationX(0.0f); 320 btv.setTranslationY(0.0f); 321 btv.setScaleX(1f); 322 btv.setScaleY(1f); 323 } 324 }); 325 } 326 } 327 play(AnimatorSet as, Animator a)328 private void play(AnimatorSet as, Animator a) { 329 play(as, a, a.getStartDelay(), mDuration); 330 } 331 play(AnimatorSet as, Animator a, long startDelay, int duration)332 private void play(AnimatorSet as, Animator a, long startDelay, int duration) { 333 a.setStartDelay(startDelay); 334 a.setDuration(duration); 335 as.play(a); 336 } 337 getPreviewItemInterpolator()338 private TimeInterpolator getPreviewItemInterpolator() { 339 if (mFolder.getItemCount() > MAX_NUM_ITEMS_IN_PREVIEW) { 340 // With larger folders, we want the preview items to reach their final positions faster 341 // (when opening) and later (when closing) so that they appear aligned with the rest of 342 // the folder items when they are both visible. 343 return mIsOpening 344 ? mLargeFolderPreviewItemOpenInterpolator 345 : mLargeFolderPreviewItemCloseInterpolator; 346 } 347 return mFolderInterpolator; 348 } 349 getAnimator(View view, Property property, float v1, float v2)350 private Animator getAnimator(View view, Property property, float v1, float v2) { 351 return mIsOpening 352 ? ObjectAnimator.ofFloat(view, property, v1, v2) 353 : ObjectAnimator.ofFloat(view, property, v2, v1); 354 } 355 getAnimator(GradientDrawable drawable, String property, int v1, int v2)356 private Animator getAnimator(GradientDrawable drawable, String property, int v1, int v2) { 357 return mIsOpening 358 ? ObjectAnimator.ofArgb(drawable, property, v1, v2) 359 : ObjectAnimator.ofArgb(drawable, property, v2, v1); 360 } 361 } 362