| /* |
| * 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 android.view.View.ALPHA; |
| |
| import static com.android.launcher3.BubbleTextView.TEXT_ALPHA_PROPERTY; |
| import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY; |
| import static com.android.launcher3.folder.ClippedFolderIconLayoutRule.MAX_NUM_ITEMS_IN_PREVIEW; |
| import static com.android.launcher3.graphics.IconShape.getShape; |
| |
| 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.Rect; |
| import android.graphics.drawable.GradientDrawable; |
| 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.DeviceProfile; |
| 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.celllayout.CellLayoutLayoutParams; |
| import com.android.launcher3.util.Themes; |
| import com.android.launcher3.views.BaseDragLayer; |
| |
| 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 static final int FOLDER_NAME_ALPHA_DURATION = 32; |
| private static final int LARGE_FOLDER_FOOTER_DURATION = 128; |
| |
| private Folder mFolder; |
| private FolderPagedView mContent; |
| private GradientDrawable mFolderBackground; |
| |
| private FolderIcon mFolderIcon; |
| private PreviewBackground mPreviewBackground; |
| |
| private Context mContext; |
| |
| 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); |
| private final FolderGridOrganizer mPreviewVerifier; |
| |
| private ObjectAnimator mBgColorAnimator; |
| |
| private DeviceProfile mDeviceProfile; |
| |
| public FolderAnimationManager(Folder folder, boolean isOpening) { |
| mFolder = folder; |
| mContent = folder.mContent; |
| mFolderBackground = (GradientDrawable) mFolder.getBackground(); |
| |
| mFolderIcon = folder.mFolderIcon; |
| mPreviewBackground = mFolderIcon.mBackground; |
| |
| mContext = folder.getContext(); |
| mDeviceProfile = folder.mActivityContext.getDeviceProfile(); |
| mPreviewVerifier = new FolderGridOrganizer(mDeviceProfile); |
| |
| 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.standard_interpolator); |
| mLargeFolderPreviewItemOpenInterpolator = AnimationUtils.loadInterpolator(mContext, |
| R.interpolator.large_folder_preview_item_open_interpolator); |
| mLargeFolderPreviewItemCloseInterpolator = AnimationUtils.loadInterpolator(mContext, |
| R.interpolator.standard_accelerate_interpolator); |
| } |
| |
| /** |
| * Returns the animator that changes the background color. |
| */ |
| public ObjectAnimator getBgColorAnimator() { |
| return mBgColorAnimator; |
| } |
| |
| /** |
| * Prepares the Folder for animating between open / closed states. |
| */ |
| public AnimatorSet getAnimator() { |
| final BaseDragLayer.LayoutParams lp = |
| (BaseDragLayer.LayoutParams) mFolder.getLayoutParams(); |
| mFolderIcon.getPreviewItemManager().recomputePreviewDrawingParams(); |
| ClippedFolderIconLayoutRule rule = mFolderIcon.getLayoutRule(); |
| final List<BubbleTextView> itemsInPreview = getPreviewIconsOnPage(0); |
| |
| // Match position of the FolderIcon |
| final Rect folderIconPos = new Rect(); |
| float scaleRelativeToDragLayer = mFolder.mActivityContext.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.setPivotX(0); |
| mFolder.setPivotY(0); |
| |
| // Scale the contents of the folder. |
| mFolder.mContent.setScaleX(scale); |
| mFolder.mContent.setScaleY(scale); |
| mFolder.mContent.setPivotX(0); |
| mFolder.mContent.setPivotY(0); |
| mFolder.mFooter.setScaleX(scale); |
| mFolder.mFooter.setScaleY(scale); |
| mFolder.mFooter.setPivotX(0); |
| mFolder.mFooter.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) (mContent.getPaddingLeft() * initialScale); |
| final int paddingOffsetY = (int) (mContent.getPaddingTop() * initialScale); |
| |
| int initialX = folderIconPos.left + mFolder.getPaddingLeft() |
| + Math.round(mPreviewBackground.getOffsetX() * scaleRelativeToDragLayer) |
| - paddingOffsetX - previewItemOffsetX; |
| int initialY = folderIconPos.top + mFolder.getPaddingTop() |
| + Math.round(mPreviewBackground.getOffsetY() * scaleRelativeToDragLayer) |
| - paddingOffsetY; |
| final float xDistance = initialX - lp.x; |
| final float yDistance = initialY - lp.y; |
| |
| // Set up the Folder background. |
| final int initialColor = Themes.getAttrColor(mContext, R.attr.folderPreviewColor); |
| final int finalColor = Themes.getAttrColor(mContext, R.attr.folderBackgroundColor); |
| |
| mFolderBackground.mutate(); |
| mFolderBackground.setColor(mIsOpening ? initialColor : finalColor); |
| |
| // Set up the reveal animation that clips the Folder. |
| int totalOffsetX = paddingOffsetX + previewItemOffsetX; |
| Rect startRect = new Rect(totalOffsetX, |
| paddingOffsetY, |
| Math.round((totalOffsetX + initialSize)), |
| Math.round((paddingOffsetY + initialSize))); |
| Rect endRect = new Rect(0, 0, lp.width, lp.height); |
| float finalRadius = mFolderBackground.getCornerRadius(); |
| |
| // Create the animators. |
| AnimatorSet a = new AnimatorSet(); |
| |
| // Initialize the Folder items' text. |
| PropertyResetListener colorResetListener = |
| new PropertyResetListener<>(TEXT_ALPHA_PROPERTY, 1f); |
| for (BubbleTextView icon : mFolder.getItemsOnPage(mFolder.mContent.getCurrentPage())) { |
| if (mIsOpening) { |
| icon.setTextVisibility(false); |
| } |
| ObjectAnimator anim = icon.createTextAlphaAnimator(mIsOpening); |
| anim.addListener(colorResetListener); |
| play(a, anim); |
| } |
| |
| mBgColorAnimator = getAnimator(mFolderBackground, "color", initialColor, finalColor); |
| play(a, mBgColorAnimator); |
| play(a, getAnimator(mFolder, View.TRANSLATION_X, xDistance, 0f)); |
| play(a, getAnimator(mFolder, View.TRANSLATION_Y, yDistance, 0f)); |
| play(a, getAnimator(mFolder.mContent, SCALE_PROPERTY, initialScale, finalScale)); |
| play(a, getAnimator(mFolder.mFooter, SCALE_PROPERTY, initialScale, finalScale)); |
| |
| final int footerAlphaDuration; |
| final int footerStartDelay; |
| if (isLargeFolder()) { |
| if (mIsOpening) { |
| mFolder.mFooter.setAlpha(0); |
| footerAlphaDuration = LARGE_FOLDER_FOOTER_DURATION; |
| footerStartDelay = mDuration - footerAlphaDuration; |
| } else { |
| footerAlphaDuration = 0; |
| footerStartDelay = 0; |
| } |
| } else { |
| footerStartDelay = 0; |
| footerAlphaDuration = mDuration; |
| } |
| play(a, getAnimator(mFolder.mFooter, ALPHA, 0, 1f), footerStartDelay, footerAlphaDuration); |
| |
| // Create reveal animator for the folder background |
| play(a, getShape().createRevealAnimator( |
| mFolder, startRect, endRect, finalRadius, !mIsOpening)); |
| |
| // Create reveal animator for the folder content (capture the top 4 icons 2x2) |
| int width = mDeviceProfile.folderCellLayoutBorderSpacePx.x |
| + mDeviceProfile.folderCellWidthPx * 2; |
| int height = mDeviceProfile.folderCellLayoutBorderSpacePx.y |
| + mDeviceProfile.folderCellHeightPx * 2; |
| int page = mIsOpening ? mContent.getCurrentPage() : mContent.getDestinationPage(); |
| int left = mContent.getPaddingLeft() + page * lp.width; |
| Rect contentStart = new Rect(left, 0, left + width, height); |
| Rect contentEnd = new Rect(left, 0, left + lp.width, lp.height); |
| play(a, getShape().createRevealAnimator( |
| mFolder.getContent(), contentStart, contentEnd, finalRadius, !mIsOpening)); |
| |
| |
| // Fade in the folder name, as the text can overlap the icons when grid size is small. |
| mFolder.mFolderName.setAlpha(mIsOpening ? 0f : 1f); |
| play(a, getAnimator(mFolder.mFolderName, View.ALPHA, 0, 1), |
| mIsOpening ? FOLDER_NAME_ALPHA_DURATION : 0, |
| mIsOpening ? mDuration - FOLDER_NAME_ALPHA_DURATION : FOLDER_NAME_ALPHA_DURATION); |
| |
| // Translate the footer so that it tracks the bottom of the content. |
| float normalHeight = mFolder.getContentAreaHeight(); |
| float scaledHeight = normalHeight * initialScale; |
| float diff = normalHeight - scaledHeight; |
| play(a, getAnimator(mFolder.mFooter, View.TRANSLATION_Y, -diff, 0f)); |
| |
| // 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); |
| |
| // Store clip variables. |
| // Because {@link #onAnimationStart} and {@link #onAnimationEnd} callbacks are sent to |
| // message queue and executed on separate frame, we should save states in |
| // {@link #onAnimationStart} instead of before creating animator, so that cancelling |
| // animation A and restarting animation B allows A to reset states in |
| // {@link #onAnimationEnd} before B reads new UI state from {@link #onAnimationStart}. |
| a.addListener(new AnimatorListenerAdapter() { |
| private CellLayout mCellLayout; |
| |
| private boolean mFolderClipChildren; |
| private boolean mFolderClipToPadding; |
| private boolean mContentClipChildren; |
| private boolean mContentClipToPadding; |
| private boolean mCellLayoutClipChildren; |
| private boolean mCellLayoutClipPadding; |
| |
| @Override |
| public void onAnimationStart(Animator animator) { |
| super.onAnimationStart(animator); |
| mCellLayout = mContent.getCurrentCellLayout(); |
| mFolderClipChildren = mFolder.getClipChildren(); |
| mFolderClipToPadding = mFolder.getClipToPadding(); |
| mContentClipChildren = mContent.getClipChildren(); |
| mContentClipToPadding = mContent.getClipToPadding(); |
| mCellLayoutClipChildren = mCellLayout.getClipChildren(); |
| mCellLayoutClipPadding = mCellLayout.getClipToPadding(); |
| |
| mFolder.setClipChildren(false); |
| mFolder.setClipToPadding(false); |
| mContent.setClipChildren(false); |
| mContent.setClipToPadding(false); |
| mCellLayout.setClipChildren(false); |
| mCellLayout.setClipToPadding(false); |
| } |
| |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| super.onAnimationEnd(animation); |
| mFolder.setTranslationX(0.0f); |
| mFolder.setTranslationY(0.0f); |
| mFolder.setTranslationZ(0.0f); |
| mFolder.mContent.setScaleX(1f); |
| mFolder.mContent.setScaleY(1f); |
| mFolder.mFooter.setScaleX(1f); |
| mFolder.mFooter.setScaleY(1f); |
| mFolder.mFooter.setTranslationX(0f); |
| mFolder.mFolderName.setAlpha(1f); |
| |
| mFolder.setClipChildren(mFolderClipChildren); |
| mFolder.setClipToPadding(mFolderClipToPadding); |
| mContent.setClipChildren(mContentClipChildren); |
| mContent.setClipToPadding(mContentClipToPadding); |
| mCellLayout.setClipChildren(mCellLayoutClipChildren); |
| mCellLayout.setClipToPadding(mCellLayoutClipPadding); |
| } |
| }); |
| |
| // 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. |
| (int) (previewItemOffsetX / scaleRelativeToDragLayer) + radiusDiff, radiusDiff); |
| return a; |
| } |
| |
| /** |
| * Returns the list of "preview items" on {@param page}. |
| */ |
| private List<BubbleTextView> getPreviewIconsOnPage(int page) { |
| return mPreviewVerifier.setFolderInfo(mFolder.mInfo) |
| .previewItemsForPage(page, mFolder.getIconsInReadingOrder()); |
| } |
| |
| /** |
| * 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 = getPreviewIconsOnPage( |
| isOnFirstPage ? 0 : 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); |
| CellLayoutLayoutParams btvLp = (CellLayoutLayoutParams) 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 float paddingTop = btv.getPaddingTop() * iconScale; |
| final int previewPosY = (int) ((mTmpParams.transY + previewItemOffsetY - paddingTop) |
| / 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 boolean isLargeFolder() { |
| return mFolder.getItemCount() > MAX_NUM_ITEMS_IN_PREVIEW; |
| } |
| |
| private TimeInterpolator getPreviewItemInterpolator() { |
| if (isLargeFolder()) { |
| // 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 ObjectAnimator getAnimator(GradientDrawable drawable, String property, int v1, int v2) { |
| return mIsOpening |
| ? ObjectAnimator.ofArgb(drawable, property, v1, v2) |
| : ObjectAnimator.ofArgb(drawable, property, v2, v1); |
| } |
| } |