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