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