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);