| /* |
| * Copyright (C) 2017 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.launcher3.folder; |
| |
| import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY; |
| import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW; |
| |
| import android.animation.Animator; |
| import android.animation.AnimatorListenerAdapter; |
| import android.animation.AnimatorSet; |
| import android.animation.ObjectAnimator; |
| import android.animation.TimeInterpolator; |
| import android.content.Context; |
| import android.content.res.Resources; |
| import android.graphics.Color; |
| import android.graphics.Rect; |
| import android.graphics.drawable.GradientDrawable; |
| import android.support.v4.graphics.ColorUtils; |
| import android.util.Property; |
| import android.view.View; |
| import android.view.animation.AnimationUtils; |
| |
| import com.android.launcher3.BubbleTextView; |
| import com.android.launcher3.CellLayout; |
| import com.android.launcher3.Launcher; |
| import com.android.launcher3.LauncherAnimUtils; |
| import com.android.launcher3.R; |
| import com.android.launcher3.ShortcutAndWidgetContainer; |
| import com.android.launcher3.Utilities; |
| import com.android.launcher3.anim.PropertyResetListener; |
| import com.android.launcher3.anim.RoundedRectRevealOutlineProvider; |
| import com.android.launcher3.dragndrop.DragLayer; |
| import com.android.launcher3.util.Themes; |
| |
| import java.util.List; |
| |
| /** |
| * Manages the opening and closing animations for a {@link Folder}. |
| * |
| * All of the animations are done in the Folder. |
| * ie. When the user taps on the FolderIcon, we immediately hide the FolderIcon and show the Folder |
| * in its place before starting the animation. |
| */ |
| public class FolderAnimationManager { |
| |
| private Folder mFolder; |
| private FolderPagedView mContent; |
| private GradientDrawable mFolderBackground; |
| |
| private FolderIcon mFolderIcon; |
| private PreviewBackground mPreviewBackground; |
| |
| private Context mContext; |
| private Launcher mLauncher; |
| |
| private final boolean mIsOpening; |
| |
| private final int mDuration; |
| private final int mDelay; |
| |
| private final TimeInterpolator mFolderInterpolator; |
| private final TimeInterpolator mLargeFolderPreviewItemOpenInterpolator; |
| private final TimeInterpolator mLargeFolderPreviewItemCloseInterpolator; |
| |
| private final PreviewItemDrawingParams mTmpParams = new PreviewItemDrawingParams(0, 0, 0, 0); |
| |
| |
| public FolderAnimationManager(Folder folder, boolean isOpening) { |
| mFolder = folder; |
| mContent = folder.mContent; |
| mFolderBackground = (GradientDrawable) mFolder.getBackground(); |
| |
| mFolderIcon = folder.mFolderIcon; |
| mPreviewBackground = mFolderIcon.mBackground; |
| |
| mContext = folder.getContext(); |
| mLauncher = folder.mLauncher; |
| |
| mIsOpening = isOpening; |
| |
| Resources res = mContent.getResources(); |
| mDuration = res.getInteger(R.integer.config_materialFolderExpandDuration); |
| mDelay = res.getInteger(R.integer.config_folderDelay); |
| |
| mFolderInterpolator = AnimationUtils.loadInterpolator(mContext, |
| R.interpolator.folder_interpolator); |
| mLargeFolderPreviewItemOpenInterpolator = AnimationUtils.loadInterpolator(mContext, |
| R.interpolator.large_folder_preview_item_open_interpolator); |
| mLargeFolderPreviewItemCloseInterpolator = AnimationUtils.loadInterpolator(mContext, |
| R.interpolator.large_folder_preview_item_close_interpolator); |
| } |
| |
| |
| /** |
| * Prepares the Folder for animating between open / closed states. |
| */ |
| public AnimatorSet getAnimator() { |
| final DragLayer.LayoutParams lp = (DragLayer.LayoutParams) mFolder.getLayoutParams(); |
| ClippedFolderIconLayoutRule rule = mFolderIcon.getLayoutRule(); |
| final List<BubbleTextView> itemsInPreview = mFolderIcon.getPreviewItems(); |
| |
| // Match position of the FolderIcon |
| final Rect folderIconPos = new Rect(); |
| float scaleRelativeToDragLayer = mLauncher.getDragLayer() |
| .getDescendantRectRelativeToSelf(mFolderIcon, folderIconPos); |
| int scaledRadius = mPreviewBackground.getScaledRadius(); |
| float initialSize = (scaledRadius * 2) * scaleRelativeToDragLayer; |
| |
| // Match size/scale of icons in the preview |
| float previewScale = rule.scaleForItem(itemsInPreview.size()); |
| float previewSize = rule.getIconSize() * previewScale; |
| float initialScale = previewSize / itemsInPreview.get(0).getIconSize() |
| * scaleRelativeToDragLayer; |
| final float finalScale = 1f; |
| float scale = mIsOpening ? initialScale : finalScale; |
| mFolder.setScaleX(scale); |
| mFolder.setScaleY(scale); |
| mFolder.setPivotX(0); |
| mFolder.setPivotY(0); |
| |
| // We want to create a small X offset for the preview items, so that they follow their |
| // expected path to their final locations. ie. an icon should not move right, if it's final |
| // location is to its left. This value is arbitrarily defined. |
| int previewItemOffsetX = (int) (previewSize / 2); |
| if (Utilities.isRtl(mContext.getResources())) { |
| previewItemOffsetX = (int) (lp.width * initialScale - initialSize - previewItemOffsetX); |
| } |
| |
| final int paddingOffsetX = (int) ((mFolder.getPaddingLeft() + mContent.getPaddingLeft()) |
| * initialScale); |
| final int paddingOffsetY = (int) ((mFolder.getPaddingTop() + mContent.getPaddingTop()) |
| * initialScale); |
| |
| int initialX = folderIconPos.left + mPreviewBackground.getOffsetX() - paddingOffsetX |
| - previewItemOffsetX; |
| int initialY = folderIconPos.top + mPreviewBackground.getOffsetY() - paddingOffsetY; |
| final float xDistance = initialX - lp.x; |
| final float yDistance = initialY - lp.y; |
| |
| // Set up the Folder background. |
| final int finalColor = Themes.getAttrColor(mContext, android.R.attr.colorPrimary); |
| final int initialColor = |
| ColorUtils.setAlphaComponent(finalColor, mPreviewBackground.getBackgroundAlpha()); |
| mFolderBackground.setColor(mIsOpening ? initialColor : finalColor); |
| |
| // Set up the reveal animation that clips the Folder. |
| int totalOffsetX = paddingOffsetX + previewItemOffsetX; |
| Rect startRect = new Rect( |
| Math.round(totalOffsetX / initialScale), |
| Math.round(paddingOffsetY / initialScale), |
| Math.round((totalOffsetX + initialSize) / initialScale), |
| Math.round((paddingOffsetY + initialSize) / initialScale)); |
| Rect endRect = new Rect(0, 0, lp.width, lp.height); |
| float initialRadius = initialSize / initialScale / 2f; |
| float finalRadius = Utilities.pxFromDp(2, mContext.getResources().getDisplayMetrics()); |
| |
| // Create the animators. |
| AnimatorSet a = LauncherAnimUtils.createAnimatorSet(); |
| |
| // Initialize the Folder items' text. |
| PropertyResetListener colorResetListener = new PropertyResetListener<>( |
| BubbleTextView.TEXT_ALPHA_PROPERTY, |
| Color.alpha(Themes.getAttrColor(mContext, android.R.attr.textColorSecondary))); |
| for (BubbleTextView icon : mFolder.getItemsOnPage(mFolder.mContent.getCurrentPage())) { |
| if (mIsOpening) { |
| icon.setTextVisibility(false); |
| } |
| ObjectAnimator anim = icon.createTextAlphaAnimator(mIsOpening); |
| anim.addListener(colorResetListener); |
| play(a, anim); |
| } |
| |
| play(a, getAnimator(mFolder, View.TRANSLATION_X, xDistance, 0f)); |
| play(a, getAnimator(mFolder, View.TRANSLATION_Y, yDistance, 0f)); |
| play(a, getAnimator(mFolder, SCALE_PROPERTY, initialScale, finalScale)); |
| play(a, getAnimator(mFolderBackground, "color", initialColor, finalColor)); |
| play(a, mFolderIcon.mFolderName.createTextAlphaAnimator(!mIsOpening)); |
| RoundedRectRevealOutlineProvider outlineProvider = new RoundedRectRevealOutlineProvider( |
| initialRadius, finalRadius, startRect, endRect) { |
| @Override |
| public boolean shouldRemoveElevationDuringAnimation() { |
| return true; |
| } |
| }; |
| play(a, outlineProvider.createRevealAnimator(mFolder, !mIsOpening)); |
| |
| // Animate the elevation midway so that the shadow is not noticeable in the background. |
| int midDuration = mDuration / 2; |
| Animator z = getAnimator(mFolder, View.TRANSLATION_Z, -mFolder.getElevation(), 0); |
| play(a, z, mIsOpening ? midDuration : 0, midDuration); |
| |
| a.addListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| super.onAnimationEnd(animation); |
| mFolder.setTranslationX(0.0f); |
| mFolder.setTranslationY(0.0f); |
| mFolder.setTranslationZ(0.0f); |
| mFolder.setScaleX(1f); |
| mFolder.setScaleY(1f); |
| } |
| }); |
| |
| // We set the interpolator on all current child animators here, because the preview item |
| // animators may use a different interpolator. |
| for (Animator animator : a.getChildAnimations()) { |
| animator.setInterpolator(mFolderInterpolator); |
| } |
| |
| int radiusDiff = scaledRadius - mPreviewBackground.getRadius(); |
| addPreviewItemAnimators(a, initialScale / scaleRelativeToDragLayer, |
| // Background can have a scaled radius in drag and drop mode, so we need to add the |
| // difference to keep the preview items centered. |
| previewItemOffsetX + radiusDiff, radiusDiff); |
| return a; |
| } |
| |
| /** |
| * Animate the items on the current page. |
| */ |
| private void addPreviewItemAnimators(AnimatorSet animatorSet, final float folderScale, |
| int previewItemOffsetX, int previewItemOffsetY) { |
| ClippedFolderIconLayoutRule rule = mFolderIcon.getLayoutRule(); |
| boolean isOnFirstPage = mFolder.mContent.getCurrentPage() == 0; |
| final List<BubbleTextView> itemsInPreview = isOnFirstPage |
| ? mFolderIcon.getPreviewItems() |
| : mFolderIcon.getPreviewItemsOnPage(mFolder.mContent.getCurrentPage()); |
| final int numItemsInPreview = itemsInPreview.size(); |
| final int numItemsInFirstPagePreview = isOnFirstPage |
| ? numItemsInPreview : MAX_NUM_ITEMS_IN_PREVIEW; |
| |
| TimeInterpolator previewItemInterpolator = getPreviewItemInterpolator(); |
| |
| ShortcutAndWidgetContainer cwc = mContent.getPageAt(0).getShortcutsAndWidgets(); |
| for (int i = 0; i < numItemsInPreview; ++i) { |
| final BubbleTextView btv = itemsInPreview.get(i); |
| CellLayout.LayoutParams btvLp = (CellLayout.LayoutParams) btv.getLayoutParams(); |
| |
| // Calculate the final values in the LayoutParams. |
| btvLp.isLockedToGrid = true; |
| cwc.setupLp(btv); |
| |
| // Match scale of icons in the preview of the items on the first page. |
| float previewScale = rule.scaleForItem(numItemsInFirstPagePreview); |
| float previewSize = rule.getIconSize() * previewScale; |
| float iconScale = previewSize / itemsInPreview.get(i).getIconSize(); |
| |
| final float initialScale = iconScale / folderScale; |
| final float finalScale = 1f; |
| float scale = mIsOpening ? initialScale : finalScale; |
| btv.setScaleX(scale); |
| btv.setScaleY(scale); |
| |
| // Match positions of the icons in the folder with their positions in the preview |
| rule.computePreviewItemDrawingParams(i, numItemsInFirstPagePreview, mTmpParams); |
| // The PreviewLayoutRule assumes that the icon size takes up the entire width so we |
| // offset by the actual size. |
| int iconOffsetX = (int) ((btvLp.width - btv.getIconSize()) * iconScale) / 2; |
| |
| final int previewPosX = |
| (int) ((mTmpParams.transX - iconOffsetX + previewItemOffsetX) / folderScale); |
| final int previewPosY = (int) ((mTmpParams.transY + previewItemOffsetY) / folderScale); |
| |
| final float xDistance = previewPosX - btvLp.x; |
| final float yDistance = previewPosY - btvLp.y; |
| |
| Animator translationX = getAnimator(btv, View.TRANSLATION_X, xDistance, 0f); |
| translationX.setInterpolator(previewItemInterpolator); |
| play(animatorSet, translationX); |
| |
| Animator translationY = getAnimator(btv, View.TRANSLATION_Y, yDistance, 0f); |
| translationY.setInterpolator(previewItemInterpolator); |
| play(animatorSet, translationY); |
| |
| Animator scaleAnimator = getAnimator(btv, SCALE_PROPERTY, initialScale, finalScale); |
| scaleAnimator.setInterpolator(previewItemInterpolator); |
| play(animatorSet, scaleAnimator); |
| |
| if (mFolder.getItemCount() > MAX_NUM_ITEMS_IN_PREVIEW) { |
| // These delays allows the preview items to move as part of the Folder's motion, |
| // and its only necessary for large folders because of differing interpolators. |
| int delay = mIsOpening ? mDelay : mDelay * 2; |
| if (mIsOpening) { |
| translationX.setStartDelay(delay); |
| translationY.setStartDelay(delay); |
| scaleAnimator.setStartDelay(delay); |
| } |
| translationX.setDuration(translationX.getDuration() - delay); |
| translationY.setDuration(translationY.getDuration() - delay); |
| scaleAnimator.setDuration(scaleAnimator.getDuration() - delay); |
| } |
| |
| animatorSet.addListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationStart(Animator animation) { |
| super.onAnimationStart(animation); |
| // Necessary to initialize values here because of the start delay. |
| if (mIsOpening) { |
| btv.setTranslationX(xDistance); |
| btv.setTranslationY(yDistance); |
| btv.setScaleX(initialScale); |
| btv.setScaleY(initialScale); |
| } |
| } |
| |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| super.onAnimationEnd(animation); |
| btv.setTranslationX(0.0f); |
| btv.setTranslationY(0.0f); |
| btv.setScaleX(1f); |
| btv.setScaleY(1f); |
| } |
| }); |
| } |
| } |
| |
| private void play(AnimatorSet as, Animator a) { |
| play(as, a, a.getStartDelay(), mDuration); |
| } |
| |
| private void play(AnimatorSet as, Animator a, long startDelay, int duration) { |
| a.setStartDelay(startDelay); |
| a.setDuration(duration); |
| as.play(a); |
| } |
| |
| private TimeInterpolator getPreviewItemInterpolator() { |
| if (mFolder.getItemCount() > MAX_NUM_ITEMS_IN_PREVIEW) { |
| // With larger folders, we want the preview items to reach their final positions faster |
| // (when opening) and later (when closing) so that they appear aligned with the rest of |
| // the folder items when they are both visible. |
| return mIsOpening |
| ? mLargeFolderPreviewItemOpenInterpolator |
| : mLargeFolderPreviewItemCloseInterpolator; |
| } |
| return mFolderInterpolator; |
| } |
| |
| private Animator getAnimator(View view, Property property, float v1, float v2) { |
| return mIsOpening |
| ? ObjectAnimator.ofFloat(view, property, v1, v2) |
| : ObjectAnimator.ofFloat(view, property, v2, v1); |
| } |
| |
| private Animator getAnimator(GradientDrawable drawable, String property, int v1, int v2) { |
| return mIsOpening |
| ? ObjectAnimator.ofArgb(drawable, property, v1, v2) |
| : ObjectAnimator.ofArgb(drawable, property, v2, v1); |
| } |
| } |