diff --git a/res/interpolator/folder_closing_interpolator.xml b/res/interpolator/folder_closing_interpolator.xml new file mode 100644 index 0000000000..e6e012eda1 --- /dev/null +++ b/res/interpolator/folder_closing_interpolator.xml @@ -0,0 +1,24 @@ + + + + diff --git a/res/interpolator/folder_opening_interpolator.xml b/res/interpolator/folder_opening_interpolator.xml new file mode 100644 index 0000000000..b95d4548ff --- /dev/null +++ b/res/interpolator/folder_opening_interpolator.xml @@ -0,0 +1,24 @@ + + + + diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java index 3b8fb0ac8d..f9a6742d8f 100644 --- a/src/com/android/launcher3/BubbleTextView.java +++ b/src/com/android/launcher3/BubbleTextView.java @@ -590,6 +590,10 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver { .isEmpty(); } + public int getIconSize() { + return mIconSize; + } + /** * Interface to be implemented by the grand parent to allow click shadow effect. */ diff --git a/src/com/android/launcher3/anim/RevealOutlineAnimation.java b/src/com/android/launcher3/anim/RevealOutlineAnimation.java index 4b270dbecb..51d00d9477 100644 --- a/src/com/android/launcher3/anim/RevealOutlineAnimation.java +++ b/src/com/android/launcher3/anim/RevealOutlineAnimation.java @@ -83,4 +83,8 @@ public abstract class RevealOutlineAnimation extends ViewOutlineProvider { public void getOutline(View v, Outline outline) { outline.setRoundRect(mOutline, mOutlineRadius); } + + public float getRadius() { + return mOutlineRadius; + } } diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java index c63dd5861f..5b0dfdb9fe 100644 --- a/src/com/android/launcher3/folder/Folder.java +++ b/src/com/android/launcher3/folder/Folder.java @@ -46,6 +46,7 @@ import android.widget.TextView; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.Alarm; import com.android.launcher3.AppInfo; +import com.android.launcher3.BubbleTextView; import com.android.launcher3.CellLayout; import com.android.launcher3.DeviceProfile; import com.android.launcher3.DragSource; @@ -82,6 +83,7 @@ import com.android.launcher3.util.Thunk; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; +import java.util.List; /** * Represents a set of icons chosen by the user or generated by the system. @@ -134,8 +136,10 @@ public class Folder extends AbstractFloatingView implements DragSource, View.OnC @Thunk final ArrayList mItemsInReadingOrder = new ArrayList(); + private FolderAnimationManager mFolderAnimationManager; + private final int mExpandDuration; - private final int mMaterialExpandDuration; + public final int mMaterialExpandDuration; private final int mMaterialExpandStagger; protected final Launcher mLauncher; @@ -476,6 +480,8 @@ public class Folder extends AbstractFloatingView implements DragSource, View.OnC } } }); + + mFolderAnimationManager = new FolderAnimationManager(this); } /** @@ -512,6 +518,9 @@ public class Folder extends AbstractFloatingView implements DragSource, View.OnC } private AnimatorSet getOpeningAnimatorSet() { + prepareReveal(); + mFolderIcon.growAndFadeOut(); + AnimatorSet anim = LauncherAnimUtils.createAnimatorSet(); int width = getFolderWidth(); @@ -602,12 +611,11 @@ public class Folder extends AbstractFloatingView implements DragSource, View.OnC mDeleteFolderOnDropCompleted = false; final Runnable onCompleteRunnable; - prepareReveal(); centerAboutIcon(); - mFolderIcon.growAndFadeOut(); - - AnimatorSet anim = getOpeningAnimatorSet(); + AnimatorSet anim = FeatureFlags.LAUNCHER3_NEW_FOLDER_ANIMATION + ? mFolderAnimationManager.getOpeningAnimator() + : getOpeningAnimatorSet(); onCompleteRunnable = new Runnable() { @Override public void run() { @@ -705,7 +713,7 @@ public class Folder extends AbstractFloatingView implements DragSource, View.OnC mFolderName.dispatchBackKey(); } - if (mFolderIcon != null) { + if (mFolderIcon != null && !FeatureFlags.LAUNCHER3_NEW_FOLDER_ANIMATION) { mFolderIcon.shrinkAndFadeIn(animate); } @@ -730,12 +738,14 @@ public class Folder extends AbstractFloatingView implements DragSource, View.OnC AnimationLayerSet layerSet = new AnimationLayerSet(); layerSet.addView(this); animatorSet.addListener(layerSet); - + animatorSet.setDuration(mExpandDuration); return animatorSet; } private void animateClosed() { - AnimatorSet a = getClosingAnimatorSet(); + AnimatorSet a = FeatureFlags.LAUNCHER3_NEW_FOLDER_ANIMATION + ? mFolderAnimationManager.getClosingAnimator() + : getClosingAnimatorSet(); a.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { @@ -750,7 +760,6 @@ public class Folder extends AbstractFloatingView implements DragSource, View.OnC mState = STATE_ANIMATING; } }); - a.setDuration(mExpandDuration); a.start(); } @@ -1453,6 +1462,26 @@ public class Folder extends AbstractFloatingView implements DragSource, View.OnC return mItemsInReadingOrder; } + public List getItemsOnCurrentPage() { + ArrayList allItems = getItemsInReadingOrder(); + int currentPage = mContent.getCurrentPage(); + int lastPage = mContent.getPageCount() - 1; + int totalItemsInFolder = allItems.size(); + int itemsPerPage = mContent.itemsPerPage(); + int numItemsOnCurrentPage = currentPage == lastPage + ? totalItemsInFolder - (itemsPerPage * currentPage) + : itemsPerPage; + + int startIndex = currentPage * itemsPerPage; + int endIndex = startIndex + numItemsOnCurrentPage; + + List itemsOnCurrentPage = new ArrayList<>(numItemsOnCurrentPage); + for (int i = startIndex; i < endIndex; ++i) { + itemsOnCurrentPage.add((BubbleTextView) allItems.get(i)); + } + return itemsOnCurrentPage; + } + public void onFocusChange(View v, boolean hasFocus) { if (v == mFolderName) { if (hasFocus) { diff --git a/src/com/android/launcher3/folder/FolderAnimationManager.java b/src/com/android/launcher3/folder/FolderAnimationManager.java new file mode 100644 index 0000000000..28133219f1 --- /dev/null +++ b/src/com/android/launcher3/folder/FolderAnimationManager.java @@ -0,0 +1,333 @@ +/* + * 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 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.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.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 FolderIcon.PreviewBackground mPreviewBackground; + + private Context mContext; + private Launcher mLauncher; + + private Animator mRevealAnimator; + private final TimeInterpolator mOpeningInterpolator; + private final TimeInterpolator mClosingInterpolator; + + private final FolderIcon.PreviewItemDrawingParams mTmpParams = + new FolderIcon.PreviewItemDrawingParams(0, 0, 0, 0); + + private final Property SCALE_PROPERTY = + new Property(Float.class, "scale") { + @Override + public Float get(View view) { + return view.getScaleX(); + } + + @Override + public void set(View view, Float scale) { + view.setScaleX(scale); + view.setScaleY(scale); + } + }; + + private final Property, Integer> ITEMS_TEXT_COLOR_PROPERTY = + new Property, Integer>(Integer.class, "textColor") { + @Override + public Integer get(List items) { + return items.get(0).getCurrentTextColor(); + } + + @Override + public void set(List items, Integer color) { + setItemsTextColor(items, color); + } + }; + + public FolderAnimationManager(Folder folder) { + mFolder = folder; + mContent = folder.mContent; + mFolderBackground = (GradientDrawable) mFolder.getBackground(); + + mFolderIcon = folder.mFolderIcon; + mPreviewBackground = mFolderIcon.mBackground; + + mContext = folder.getContext(); + mLauncher = folder.mLauncher; + + mOpeningInterpolator = AnimationUtils.loadInterpolator(mContext, + R.interpolator.folder_opening_interpolator); + mClosingInterpolator = AnimationUtils.loadInterpolator(mContext, + R.interpolator.folder_closing_interpolator); + } + + public AnimatorSet getOpeningAnimator() { + mFolder.setPivotX(0); + mFolder.setPivotY(0); + + AnimatorSet a = getAnimatorSet(true /* isOpening */); + a.setInterpolator(mOpeningInterpolator); + a.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + mFolderIcon.setVisibility(View.INVISIBLE); + } + }); + return a; + } + + public AnimatorSet getClosingAnimator() { + AnimatorSet a = getAnimatorSet(false /* isOpening */); + a.setInterpolator(mClosingInterpolator); + a.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mFolderIcon.setVisibility(View.VISIBLE); + } + }); + return a; + } + + /** + * Prepares the Folder for animating between open / closed states. + * + * @param isOpening If true, return the animator set for the opening animation. + */ + private AnimatorSet getAnimatorSet(final boolean isOpening) { + final DragLayer.LayoutParams lp = (DragLayer.LayoutParams) mFolder.getLayoutParams(); + FolderIcon.PreviewLayoutRule rule = mFolderIcon.getLayoutRule(); + final List itemsInPreview = mFolderIcon.getItemsToDisplay(); + + // Match size/scale of icons in the preview + float previewScale = rule.scaleForItem(0, itemsInPreview.size()); + float previewSize = rule.getIconSize() * previewScale; + float folderScale = previewSize / itemsInPreview.get(0).getIconSize(); + + final float initialScale = folderScale; + final float finalScale = 1f; + float scale = isOpening ? initialScale : finalScale; + mFolder.setScaleX(scale); + mFolder.setScaleY(scale); + + // Match position of the FolderIcon + final Rect folderIconPos = new Rect(); + float scaleRelativeToDragLayer = mLauncher.getDragLayer() + .getDescendantRectRelativeToSelf(mFolderIcon, folderIconPos); + folderScale *= scaleRelativeToDragLayer; + + // 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. + final int nudgeOffsetX = (int) (previewSize / 2); + + final int paddingOffsetX = (int) ((mFolder.getPaddingLeft() + mContent.getPaddingLeft()) + * folderScale); + final int paddingOffsetY = (int) ((mFolder.getPaddingTop() + mContent.getPaddingTop()) + * folderScale); + + int initialX = folderIconPos.left + mFolderIcon.mBackground.getOffsetX() - paddingOffsetX + - nudgeOffsetX; + int initialY = folderIconPos.top + mFolderIcon.mBackground.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(isOpening ? initialColor : finalColor); + + // Initialize the Folder items' text. + final List itemsOnCurrentPage = mFolder.getItemsOnCurrentPage(); + final int finalTextColor = Themes.getAttrColor(mContext, android.R.attr.textColorSecondary); + setItemsTextColor(itemsOnCurrentPage, isOpening ? Color.TRANSPARENT : finalTextColor); + + // Create the animators. + AnimatorSet a = LauncherAnimUtils.createAnimatorSet(); + a.setDuration(mFolder.mMaterialExpandDuration); + + ObjectAnimator translationX = isOpening + ? ObjectAnimator.ofFloat(mFolder, View.TRANSLATION_X, xDistance, 0) + : ObjectAnimator.ofFloat(mFolder, View.TRANSLATION_X, 0, xDistance); + a.play(translationX); + + ObjectAnimator translationY = isOpening + ? ObjectAnimator.ofFloat(mFolder, View.TRANSLATION_Y, yDistance, 0) + : ObjectAnimator.ofFloat(mFolder, View.TRANSLATION_Y, 0, yDistance); + a.play(translationY); + + ObjectAnimator scaleAnimator = isOpening + ? ObjectAnimator.ofFloat(mFolder, SCALE_PROPERTY, initialScale, finalScale) + : ObjectAnimator.ofFloat(mFolder, SCALE_PROPERTY, finalScale, initialScale); + a.play(scaleAnimator); + + ObjectAnimator itemsTextColor = isOpening + ? ObjectAnimator.ofArgb(itemsOnCurrentPage, ITEMS_TEXT_COLOR_PROPERTY, + Color.TRANSPARENT, finalTextColor) + : ObjectAnimator.ofArgb(itemsOnCurrentPage, ITEMS_TEXT_COLOR_PROPERTY, + finalTextColor, Color.TRANSPARENT); + a.play(itemsTextColor); + + ObjectAnimator backgroundColor = isOpening + ? ObjectAnimator.ofArgb(mFolderBackground, "color", initialColor, finalColor) + : ObjectAnimator.ofArgb(mFolderBackground, "color", finalColor, initialColor); + a.play(backgroundColor); + + // Set up the reveal animation that clips the Folder. + float stroke = mPreviewBackground.getStrokeWidth(); + int initialSize = (int) ((mFolderIcon.mBackground.getRadius() * 2 + stroke) / folderScale); + int totalOffsetX = paddingOffsetX + Math.round(nudgeOffsetX / folderScale); + int unscaledStroke = (int) Math.floor(stroke / folderScale); + Rect startRect = new Rect(totalOffsetX + unscaledStroke, unscaledStroke, + totalOffsetX + initialSize, initialSize); + Rect endRect = new Rect(0, 0, lp.width, lp.height); + a.play(getRevealAnimator(isOpening, initialSize / 2f, startRect, endRect)); + + addPreviewItemAnimatorsToSet(a, isOpening, folderScale, nudgeOffsetX); + return a; + } + + private Animator getRevealAnimator(boolean isOpening, float circleRadius, Rect start, + Rect end) { + boolean revealIsRunning = mRevealAnimator != null && mRevealAnimator.isRunning(); + final float finalRadius = revealIsRunning + ? ((RoundedRectRevealOutlineProvider) mFolder.getOutlineProvider()).getRadius() + : Utilities.pxFromDp(2, mContext.getResources().getDisplayMetrics()); + if (revealIsRunning) { + mRevealAnimator.cancel(); + } + mRevealAnimator = new RoundedRectRevealOutlineProvider(circleRadius, finalRadius, + start, end).createRevealAnimator(mFolder, !isOpening); + return mRevealAnimator; + } + + /** + * Animate the items that are displayed in the preview. + */ + private void addPreviewItemAnimatorsToSet(AnimatorSet animatorSet, boolean isOpening, + final float folderScale, int nudgeOffsetX) { + FolderIcon.PreviewLayoutRule rule = mFolderIcon.getLayoutRule(); + final List itemsInPreview = mFolderIcon.getItemsToDisplay(); + final int numItemsInPreview = itemsInPreview.size(); + + 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. + float previewScale = rule.scaleForItem(i, numItemsInPreview); + float previewSize = rule.getIconSize() * previewScale; + float iconScale = previewSize / itemsInPreview.get(i).getIconSize(); + + final float initialScale = iconScale / folderScale; + final float finalScale = 1f; + float scale = isOpening ? 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, numItemsInPreview, 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 + nudgeOffsetX) / folderScale); + final int previewPosY = (int) (mTmpParams.transY / folderScale); + + final float xDistance = previewPosX - btvLp.x; + final float yDistance = previewPosY - btvLp.y; + + ObjectAnimator translationX = isOpening + ? ObjectAnimator.ofFloat(btv, View.TRANSLATION_X, xDistance, 0) + : ObjectAnimator.ofFloat(btv, View.TRANSLATION_X, 0, xDistance); + animatorSet.play(translationX); + + ObjectAnimator translationY = isOpening + ? ObjectAnimator.ofFloat(btv, View.TRANSLATION_Y, yDistance, 0) + : ObjectAnimator.ofFloat(btv, View.TRANSLATION_Y, 0, yDistance); + animatorSet.play(translationY); + + ObjectAnimator scaleAnimator = isOpening + ? ObjectAnimator.ofFloat(btv, SCALE_PROPERTY, initialScale, finalScale) + : ObjectAnimator.ofFloat(btv, SCALE_PROPERTY, finalScale, initialScale); + animatorSet.play(scaleAnimator); + + animatorSet.addListener(new AnimatorListenerAdapter() { + @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 setItemsTextColor(List items, int color) { + int size = items.size(); + + for (int i = 0; i < size; ++i) { + items.get(i).setTextColor(color); + } + } +} diff --git a/src/com/android/launcher3/folder/FolderIcon.java b/src/com/android/launcher3/folder/FolderIcon.java index 92577b7e3a..9395572bfc 100644 --- a/src/com/android/launcher3/folder/FolderIcon.java +++ b/src/com/android/launcher3/folder/FolderIcon.java @@ -407,6 +407,10 @@ public class FolderIcon extends FrameLayout implements FolderListener { mBadgeInfo = badgeInfo; } + public PreviewLayoutRule getLayoutRule() { + return mPreviewLayoutRule; + } + /** * Sets mBadgeScale to 1 or 0, animating if oldCount or newCount is 0 * (the badge is being added or removed). @@ -824,6 +828,14 @@ public class FolderIcon extends FrameLayout implements FolderListener { }; animateScale(1f, 1f, onStart, onEnd); } + + public int getBackgroundAlpha() { + return (int) Math.min(MAX_BG_OPACITY, BG_OPACITY * mColorMultiplier); + } + + public float getStrokeWidth() { + return mStrokeWidth; + } } public void setFolderBackground(PreviewBackground bg) { @@ -991,15 +1003,15 @@ public class FolderIcon extends FrameLayout implements FolderListener { return mFolderName.getVisibility() == VISIBLE; } - private List getItemsToDisplay() { + public List getItemsToDisplay() { mPreviewVerifier.setFolderInfo(mFolder.getInfo()); - List itemsToDisplay = new ArrayList<>(); + List itemsToDisplay = new ArrayList<>(); List allItems = mFolder.getItemsInReadingOrder(); int numItems = allItems.size(); for (int rank = 0; rank < numItems; ++rank) { if (mPreviewVerifier.isItemInPreview(rank)) { - itemsToDisplay.add(allItems.get(rank)); + itemsToDisplay.add((BubbleTextView) allItems.get(rank)); } if (itemsToDisplay.size() == FolderIcon.NUM_ITEMS_IN_PREVIEW) { @@ -1010,7 +1022,7 @@ public class FolderIcon extends FrameLayout implements FolderListener { } private void updateItemDrawingParams(boolean animate) { - List items = getItemsToDisplay(); + List items = getItemsToDisplay(); int nItemsInPreview = items.size(); int prevNumItems = mDrawingParams.size(); @@ -1025,7 +1037,7 @@ public class FolderIcon extends FrameLayout implements FolderListener { for (int i = 0; i < mDrawingParams.size(); i++) { PreviewItemDrawingParams p = mDrawingParams.get(i); - p.drawable = ((TextView) items.get(i)).getCompoundDrawables()[1]; + p.drawable = items.get(i).getCompoundDrawables()[1]; if (!animate || FeatureFlags.LAUNCHER3_LEGACY_FOLDER_ICON) { computePreviewItemDrawingParams(i, nItemsInPreview, p);