Refactor app open animation so that FloatingIconView tracks window.

Before, the window used to track the FloatingIconView. With this refactor,
both app open and app close use the same update method.

With the refactor, we can now use adaptive icons to morph the icon into the
size of the window.

Movement/interpolators are still the same as the original except for the
alpha and the clip animation. To ensure a perfect tradeoff, we wait for the
icon to match the size and shape of the window before crossfading.
Currently it is set up so that the tradeoff happens when the animation is
15% done, but this can be tuned later.

Bug: 122843905

Change-Id: I7d3edbefffb15fe26958a62ab33cf23dc1203908
This commit is contained in:
Jon Miranda 2019-03-27 10:54:17 -07:00
parent e3e1044aed
commit bba6451b5f
5 changed files with 209 additions and 232 deletions

View File

@ -113,8 +113,7 @@ public final class LauncherActivityControllerHelper implements ActivityControlHe
final Rect iconLocation = new Rect();
final FloatingIconView floatingView = workspaceView == null ? null
: FloatingIconView.getFloatingIconView(activity, workspaceView,
true /* hideOriginal */, false /* useDrawableAsIs */,
activity.getDeviceProfile().getAspectRatioWithInsets(), iconLocation, null);
true /* hideOriginal */, iconLocation, false /* isOpening */, null /* recycle */);
return new HomeAnimationFactory() {
@Nullable

View File

@ -28,6 +28,7 @@ import static com.android.launcher3.config.FeatureFlags.QUICKSTEP_SPRINGS;
import static com.android.launcher3.config.FeatureFlags.SWIPE_HOME;
import static com.android.launcher3.util.RaceConditionTracker.ENTER;
import static com.android.launcher3.util.RaceConditionTracker.EXIT;
import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION;
import static com.android.quickstep.ActivityControlHelper.AnimationFactory.ShelfAnimState.HIDE;
import static com.android.quickstep.ActivityControlHelper.AnimationFactory.ShelfAnimState.PEEK;
import static com.android.quickstep.MultiStateCallback.DEBUG_STATES;
@ -945,7 +946,7 @@ public class WindowTransformSwipeHandler<T extends BaseDraggingActivity>
// We want the window alpha to be 0 once this threshold is met, so that the
// FolderIconView can be seen morphing into the icon shape.
final float windowAlphaThreshold = isFloatingIconView ? 0.75f : 1f;
final float windowAlphaThreshold = isFloatingIconView ? 1f - SHAPE_PROGRESS_DURATION : 1f;
anim.addOnUpdateListener((currentRect, progress) -> {
float interpolatedProgress = Interpolators.ACCEL_1_5.getInterpolation(progress);
@ -959,7 +960,7 @@ public class WindowTransformSwipeHandler<T extends BaseDraggingActivity>
if (isFloatingIconView) {
((FloatingIconView) floatingView).update(currentRect, iconAlpha, progress,
windowAlphaThreshold);
windowAlphaThreshold, mClipAnimationHelper.getCurrentCornerRadius(), false);
}
});

View File

@ -20,15 +20,16 @@ import static com.android.launcher3.BaseActivity.INVISIBLE_ALL;
import static com.android.launcher3.BaseActivity.INVISIBLE_BY_APP_TRANSITIONS;
import static com.android.launcher3.BaseActivity.INVISIBLE_BY_PENDING_FLAGS;
import static com.android.launcher3.BaseActivity.PENDING_INVISIBLE_BY_WALLPAPER_ANIMATION;
import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
import static com.android.launcher3.LauncherState.ALL_APPS;
import static com.android.launcher3.LauncherState.OVERVIEW;
import static com.android.launcher3.Utilities.postAsyncCallback;
import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS;
import static com.android.launcher3.anim.Interpolators.AGGRESSIVE_EASE;
import static com.android.launcher3.anim.Interpolators.DEACCEL_1_7;
import static com.android.launcher3.anim.Interpolators.EXAGGERATED_EASE;
import static com.android.launcher3.anim.Interpolators.LINEAR;
import static com.android.launcher3.dragndrop.DragLayer.ALPHA_INDEX_TRANSITIONS;
import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION;
import static com.android.quickstep.TaskUtils.taskIsATargetWithMode;
import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING;
@ -45,6 +46,7 @@ import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.CancellationSignal;
@ -52,10 +54,8 @@ import android.os.Handler;
import android.os.Looper;
import android.util.Pair;
import android.view.View;
import android.view.ViewGroup;
import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
import com.android.launcher3.InsettableFrameLayout.LayoutParams;
import com.android.launcher3.allapps.AllAppsTransitionController;
import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.dragndrop.DragLayer;
@ -103,13 +103,18 @@ public abstract class QuickstepAppTransitionManagerImpl extends LauncherAppTrans
private static final String CONTROL_REMOTE_APP_TRANSITION_PERMISSION =
"android.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS";
private static final int APP_LAUNCH_DURATION = 500;
private static final long APP_LAUNCH_DURATION = 500;
// Use a shorter duration for x or y translation to create a curve effect
private static final int APP_LAUNCH_CURVED_DURATION = APP_LAUNCH_DURATION / 2;
private static final long APP_LAUNCH_CURVED_DURATION = APP_LAUNCH_DURATION / 2;
private static final long APP_LAUNCH_ALPHA_DURATION = 50;
// We scale the durations for the downward app launch animations (minus the scale animation).
private static final float APP_LAUNCH_DOWN_DUR_SCALE_FACTOR = 0.8f;
private static final int APP_LAUNCH_ALPHA_START_DELAY = 32;
private static final int APP_LAUNCH_ALPHA_DURATION = 50;
private static final long APP_LAUNCH_DOWN_DURATION =
(long) (APP_LAUNCH_DURATION * APP_LAUNCH_DOWN_DUR_SCALE_FACTOR);
private static final long APP_LAUNCH_DOWN_CURVED_DURATION = APP_LAUNCH_DOWN_DURATION / 2;
private static final long APP_LAUNCH_ALPHA_DOWN_DURATION =
(long) (APP_LAUNCH_ALPHA_DURATION * APP_LAUNCH_DOWN_DUR_SCALE_FACTOR);
public static final int RECENTS_LAUNCH_DURATION = 336;
private static final int LAUNCHER_RESUME_START_DELAY = 100;
@ -207,11 +212,11 @@ public abstract class QuickstepAppTransitionManagerImpl extends LauncherAppTrans
// Note that this duration is a guess as we do not know if the animation will be a
// recents launch or not for sure until we know the opening app targets.
int duration = fromRecents
long duration = fromRecents
? RECENTS_LAUNCH_DURATION
: APP_LAUNCH_DURATION;
int statusBarTransitionDelay = duration - STATUS_BAR_TRANSITION_DURATION
long statusBarTransitionDelay = duration - STATUS_BAR_TRANSITION_DURATION
- STATUS_BAR_TRANSITION_PRE_DELAY;
return ActivityOptionsCompat.makeRemoteAnimation(new RemoteAnimationAdapterCompat(
runner, duration, statusBarTransitionDelay));
@ -266,7 +271,8 @@ public abstract class QuickstepAppTransitionManagerImpl extends LauncherAppTrans
}
if (!isAllOpeningTargetTrs) break;
}
playIconAnimators(anim, v, windowTargetBounds, !isAllOpeningTargetTrs);
anim.play(getOpeningWindowAnimators(v, targets, windowTargetBounds,
!isAllOpeningTargetTrs));
if (launcherClosing) {
Pair<AnimatorSet, Runnable> launcherContentAnimator =
getLauncherContentAnimator(true /* isAppOpening */,
@ -279,7 +285,6 @@ public abstract class QuickstepAppTransitionManagerImpl extends LauncherAppTrans
}
});
}
anim.play(getOpeningWindowAnimators(v, targets, windowTargetBounds));
}
/**
@ -397,125 +402,14 @@ public abstract class QuickstepAppTransitionManagerImpl extends LauncherAppTrans
protected abstract Runnable composeViewContentAnimator(@NonNull AnimatorSet anim,
float[] alphas, float[] trans);
/**
* Animators for the "floating view" of the view used to launch the target.
*/
private void playIconAnimators(AnimatorSet appOpenAnimator, View v, Rect windowTargetBounds,
boolean toggleVisibility) {
final boolean isBubbleTextView = v instanceof BubbleTextView;
if (mFloatingView != null) {
mFloatingView.setTranslationX(0);
mFloatingView.setTranslationY(0);
mFloatingView.setScaleX(1);
mFloatingView.setScaleY(1);
mFloatingView.setAlpha(1);
mFloatingView.setBackground(null);
}
Rect rect = new Rect();
mFloatingView = FloatingIconView.getFloatingIconView(mLauncher, v, toggleVisibility,
true /* useDrawableAsIs */, -1 /* aspectRatio */, rect, mFloatingView);
int viewLocationStart = mIsRtl ? windowTargetBounds.width() - rect.right : rect.left;
LayoutParams lp = (LayoutParams) mFloatingView.getLayoutParams();
// Special RTL logic is needed to handle the window target bounds.
lp.leftMargin = mIsRtl ? windowTargetBounds.width() - rect.right : rect.left;
mFloatingView.setLayoutParams(lp);
int[] dragLayerBounds = new int[2];
mDragLayer.getLocationOnScreen(dragLayerBounds);
// Animate the app icon to the center of the window bounds in screen coordinates.
float centerX = windowTargetBounds.centerX() - dragLayerBounds[0];
float centerY = windowTargetBounds.centerY() - dragLayerBounds[1];
float xPosition = mIsRtl
? windowTargetBounds.width() - lp.getMarginStart() - rect.width()
: lp.getMarginStart();
float dX = centerX - xPosition - (lp.width / 2f);
float dY = centerY - lp.topMargin - (lp.height / 2f);
ObjectAnimator x = ObjectAnimator.ofFloat(mFloatingView, View.TRANSLATION_X, 0f, dX);
ObjectAnimator y = ObjectAnimator.ofFloat(mFloatingView, View.TRANSLATION_Y, 0f, dY);
// Use upward animation for apps that are either on the bottom half of the screen, or are
// relatively close to the center.
boolean useUpwardAnimation = lp.topMargin > centerY
|| Math.abs(dY) < mLauncher.getDeviceProfile().cellHeightPx;
if (useUpwardAnimation) {
x.setDuration(APP_LAUNCH_CURVED_DURATION);
y.setDuration(APP_LAUNCH_DURATION);
} else {
x.setDuration((long) (APP_LAUNCH_DOWN_DUR_SCALE_FACTOR * APP_LAUNCH_DURATION));
y.setDuration((long) (APP_LAUNCH_DOWN_DUR_SCALE_FACTOR * APP_LAUNCH_CURVED_DURATION));
}
x.setInterpolator(AGGRESSIVE_EASE);
y.setInterpolator(AGGRESSIVE_EASE);
appOpenAnimator.play(x);
appOpenAnimator.play(y);
// Scale the app icon to take up the entire screen. This simplifies the math when
// animating the app window position / scale.
float maxScaleX = windowTargetBounds.width() / (float) rect.width();
float maxScaleY = windowTargetBounds.height() / (float) rect.height();
float scale = Math.max(maxScaleX, maxScaleY);
float startScale = 1f;
if (isBubbleTextView && !(v.getParent() instanceof DeepShortcutView)) {
Drawable dr = ((BubbleTextView) v).getIcon();
if (dr instanceof FastBitmapDrawable) {
startScale = ((FastBitmapDrawable) dr).getAnimatedScale();
}
}
ObjectAnimator scaleAnim = ObjectAnimator
.ofFloat(mFloatingView, SCALE_PROPERTY, startScale, scale);
scaleAnim.setDuration(APP_LAUNCH_DURATION)
.setInterpolator(Interpolators.EXAGGERATED_EASE);
appOpenAnimator.play(scaleAnim);
// Fade out the app icon.
ObjectAnimator alpha = ObjectAnimator.ofFloat(mFloatingView, View.ALPHA, 1f, 0f);
if (useUpwardAnimation) {
alpha.setStartDelay(APP_LAUNCH_ALPHA_START_DELAY);
alpha.setDuration(APP_LAUNCH_ALPHA_DURATION);
} else {
alpha.setStartDelay((long) (APP_LAUNCH_DOWN_DUR_SCALE_FACTOR
* APP_LAUNCH_ALPHA_START_DELAY));
alpha.setDuration((long) (APP_LAUNCH_DOWN_DUR_SCALE_FACTOR * APP_LAUNCH_ALPHA_DURATION));
}
alpha.setInterpolator(LINEAR);
appOpenAnimator.play(alpha);
appOpenAnimator.addListener(mFloatingView);
appOpenAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
// Reset launcher to normal state
if (isBubbleTextView) {
((BubbleTextView) v).setStayPressed(false);
}
v.setVisibility(View.VISIBLE);
((ViewGroup) mDragLayer.getParent()).getOverlay().remove(mFloatingView);
}
});
}
/**
* @return Animator that controls the window of the opening targets.
*/
private ValueAnimator getOpeningWindowAnimators(View v, RemoteAnimationTargetCompat[] targets,
Rect windowTargetBounds) {
Rect windowTargetBounds, boolean toggleVisibility) {
Rect bounds = new Rect();
if (v.getParent() instanceof DeepShortcutView) {
// Deep shortcut views have their icon drawn in a separate view.
DeepShortcutView view = (DeepShortcutView) v.getParent();
mDragLayer.getDescendantRectRelativeToSelf(view.getIconView(), bounds);
} else if (v instanceof BubbleTextView) {
((BubbleTextView) v).getIconBounds(bounds);
} else {
mDragLayer.getDescendantRectRelativeToSelf(v, bounds);
}
int[] floatingViewBounds = new int[2];
mFloatingView = FloatingIconView.getFloatingIconView(mLauncher, v, toggleVisibility,
bounds, true /* isOpening */, mFloatingView);
Rect crop = new Rect();
Matrix matrix = new Matrix();
@ -526,37 +420,99 @@ public abstract class QuickstepAppTransitionManagerImpl extends LauncherAppTrans
SyncRtSurfaceTransactionApplierCompat surfaceApplier =
new SyncRtSurfaceTransactionApplierCompat(mFloatingView);
// Scale the app icon to take up the entire screen. This simplifies the math when
// animating the app window position / scale.
float maxScaleX = windowTargetBounds.width() / (float) bounds.width();
// We use windowTargetBounds.width for scaleY too since we start off the animation where the
// window is clipped to a square.
float maxScaleY = windowTargetBounds.width() / (float) bounds.height();
float scale = Math.max(maxScaleX, maxScaleY);
float startScale = 1f;
if (v instanceof BubbleTextView && !(v.getParent() instanceof DeepShortcutView)) {
Drawable dr = ((BubbleTextView) v).getIcon();
if (dr instanceof FastBitmapDrawable) {
startScale = ((FastBitmapDrawable) dr).getAnimatedScale();
}
}
final float initialStartScale = startScale;
int[] dragLayerBounds = new int[2];
mDragLayer.getLocationOnScreen(dragLayerBounds);
// Animate the app icon to the center of the window bounds in screen coordinates.
float centerX = windowTargetBounds.centerX() - dragLayerBounds[0];
float centerY = windowTargetBounds.centerY() - dragLayerBounds[1];
float dX = centerX - bounds.centerX();
float dY = centerY - bounds.centerY();
boolean useUpwardAnimation = bounds.top > centerY
|| Math.abs(dY) < mLauncher.getDeviceProfile().cellHeightPx;
final long xDuration = useUpwardAnimation ? APP_LAUNCH_CURVED_DURATION
: APP_LAUNCH_DOWN_DURATION;
final long yDuration = useUpwardAnimation ? APP_LAUNCH_DURATION
: APP_LAUNCH_DOWN_CURVED_DURATION;
final long alphaDuration = useUpwardAnimation ? APP_LAUNCH_ALPHA_DURATION
: APP_LAUNCH_ALPHA_DOWN_DURATION;
RectF targetBounds = new RectF(windowTargetBounds);
RectF currentBounds = new RectF();
RectF temp = new RectF();
ValueAnimator appAnimator = ValueAnimator.ofFloat(0, 1);
appAnimator.setDuration(APP_LAUNCH_DURATION);
appAnimator.setInterpolator(LINEAR);
appAnimator.addListener(mFloatingView);
appAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
if (v instanceof BubbleTextView) {
((BubbleTextView) v).setStayPressed(false);
}
}
});
float shapeRevealDuration = APP_LAUNCH_DURATION * SHAPE_PROGRESS_DURATION;
appAnimator.addUpdateListener(new MultiValueUpdateListener() {
// Fade alpha for the app window.
FloatProp mAlpha = new FloatProp(0f, 1f, 0, 60, LINEAR);
FloatProp mDx = new FloatProp(0, dX, 0, xDuration, AGGRESSIVE_EASE);
FloatProp mDy = new FloatProp(0, dY, 0, yDuration, AGGRESSIVE_EASE);
FloatProp mIconScale = new FloatProp(initialStartScale, scale, 0, APP_LAUNCH_DURATION,
EXAGGERATED_EASE);
FloatProp mIconAlpha = new FloatProp(1f, 0f, shapeRevealDuration, alphaDuration,
LINEAR);
FloatProp mCropHeight = new FloatProp(windowTargetBounds.width(),
windowTargetBounds.height(), 0, shapeRevealDuration, AGGRESSIVE_EASE);
@Override
public void onUpdate(float percent) {
final float easePercent = AGGRESSIVE_EASE.getInterpolation(percent);
// Calculate app icon size.
float iconWidth = bounds.width() * mFloatingView.getScaleX();
float iconHeight = bounds.height() * mFloatingView.getScaleY();
float iconWidth = bounds.width() * mIconScale.value;
float iconHeight = bounds.height() * mIconScale.value;
// Animate the window crop so that it starts off as a square, and then reveals
// horizontally.
int windowWidth = windowTargetBounds.width();
int windowHeight = (int) mCropHeight.value;
crop.set(0, 0, windowWidth, windowHeight);
// Scale the app window to match the icon size.
float scaleX = iconWidth / windowTargetBounds.width();
float scaleY = iconHeight / windowTargetBounds.height();
float scale = Math.min(1f, Math.min(scaleX, scaleY));
float scaleX = iconWidth / windowWidth;
float scaleY = iconHeight / windowHeight;
float scale = Math.min(1f, Math.max(scaleX, scaleY));
// Position the scaled window on top of the icon
int windowWidth = windowTargetBounds.width();
int windowHeight = windowTargetBounds.height();
float scaledWindowWidth = windowWidth * scale;
float scaledWindowHeight = windowHeight * scale;
float offsetX = (scaledWindowWidth - iconWidth) / 2;
float offsetY = (scaledWindowHeight - iconHeight) / 2;
mFloatingView.getLocationOnScreen(floatingViewBounds);
float transX0 = floatingViewBounds[0] - offsetX;
float transY0 = floatingViewBounds[1] - offsetY;
// Calculate the window position
temp.set(bounds);
temp.offset(dragLayerBounds[0], dragLayerBounds[1]);
temp.offset(mDx.value, mDy.value);
Utilities.scaleRectFAboutCenter(temp, mIconScale.value);
float transX0 = temp.left - offsetX;
float transY0 = temp.top - offsetY;
float windowRadius = 0;
if (!mDeviceProfile.isMultiWindowMode &&
@ -565,19 +521,9 @@ public abstract class QuickstepAppTransitionManagerImpl extends LauncherAppTrans
.getWindowCornerRadius();
}
// Animate the window crop so that it starts off as a square, and then reveals
// horizontally.
float cropHeight = windowHeight * easePercent + windowWidth * (1 - easePercent);
float initialTop = (windowHeight - windowWidth) / 2f;
crop.left = 0;
crop.top = (int) (initialTop * (1 - easePercent));
crop.right = windowWidth;
crop.bottom = (int) (crop.top + cropHeight);
SurfaceParams[] params = new SurfaceParams[targets.length];
for (int i = targets.length - 1; i >= 0; i--) {
RemoteAnimationTargetCompat target = targets[i];
Rect targetCrop;
final float alpha;
final float cornerRadius;
@ -585,12 +531,15 @@ public abstract class QuickstepAppTransitionManagerImpl extends LauncherAppTrans
matrix.setScale(scale, scale);
matrix.postTranslate(transX0, transY0);
targetCrop = crop;
alpha = mAlpha.value;
alpha = 1f - mIconAlpha.value;
cornerRadius = windowRadius;
matrix.mapRect(currentBounds, targetBounds);
mFloatingView.update(currentBounds, mIconAlpha.value, percent, 0f,
cornerRadius * scale, true /* isOpening */);
} else {
matrix.setTranslate(target.position.x, target.position.y);
alpha = 1f;
targetCrop = target.sourceContainerBounds;
alpha = 1f;
cornerRadius = 0;
}

View File

@ -51,6 +51,9 @@ public class DeviceProfile {
public final int heightPx;
public final int availableWidthPx;
public final int availableHeightPx;
public final float aspectRatio;
/**
* The maximum amount of left/right workspace padding as a percentage of the screen width.
* To be clear, this means that up to 7% of the screen width can be used as left padding, and
@ -160,7 +163,7 @@ public class DeviceProfile {
isTablet = res.getBoolean(R.bool.is_tablet);
isLargeTablet = res.getBoolean(R.bool.is_large_tablet);
isPhone = !isTablet && !isLargeTablet;
float aspectRatio = ((float) Math.max(widthPx, heightPx)) / Math.min(widthPx, heightPx);
aspectRatio = ((float) Math.max(widthPx, heightPx)) / Math.min(widthPx, heightPx);
boolean isTallDevice = Float.compare(aspectRatio, TALL_DEVICE_ASPECT_RATIO_THRESHOLD) >= 0;
// Some more constants
@ -618,12 +621,6 @@ public class DeviceProfile {
}
}
public float getAspectRatioWithInsets() {
int w = widthPx - mInsets.left - mInsets.right;
int h = heightPx - mInsets.top - mInsets.bottom;
return ((float) Math.max(w, h)) / Math.min(w, h);
}
private static Context getContext(Context c, int orientation) {
Configuration context = new Configuration(c.getResources().getConfiguration());
context.orientation = orientation;

View File

@ -15,6 +15,7 @@
*/
package com.android.launcher3.views;
import static com.android.launcher3.anim.Interpolators.LINEAR;
import static com.android.launcher3.config.FeatureFlags.ADAPTIVE_ICON_WINDOW_ANIM;
import android.animation.Animator;
@ -47,7 +48,6 @@ import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherModel;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.dragndrop.DragLayer;
import com.android.launcher3.dragndrop.FolderAdaptiveIcon;
import com.android.launcher3.folder.FolderIcon;
@ -60,18 +60,20 @@ import com.android.launcher3.shortcuts.DeepShortcutView;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import static com.android.launcher3.Utilities.mapToRange;
/**
* A view that is created to look like another view with the purpose of creating fluid animations.
*/
public class FloatingIconView extends View implements Animator.AnimatorListener, ClipPathView {
public static final float SHAPE_PROGRESS_DURATION = 0.15f;
private static final Rect sTmpRect = new Rect();
private Runnable mStartRunnable;
private Runnable mEndRunnable;
private int mOriginalHeight;
private final int mBlurSizeOutline;
private boolean mIsAdaptiveIcon = false;
@ -82,30 +84,28 @@ public class FloatingIconView extends View implements Animator.AnimatorListener,
private final Rect mStartRevealRect = new Rect();
private final Rect mEndRevealRect = new Rect();
private Path mClipPath;
protected final Rect mOutline = new Rect();
private final float mTaskCornerRadius;
private float mTaskCornerRadius;
private final Rect mFinalDrawableBounds = new Rect();
private final Rect mBgDrawableBounds = new Rect();
private float mBgDrawableStartScale = 1f;
private float mBgDrawableEndScale = 1f;
private FloatingIconView(Context context) {
super(context);
mBlurSizeOutline = context.getResources().getDimensionPixelSize(
R.dimen.blur_size_medium_outline);
mTaskCornerRadius = 0; // TODO
}
/**
* Positions this view to match the size and location of {@param rect}.
*
* @param alpha The alpha to set this view.
* @param progress A value from [0, 1] that represents the animation progress.
* @param windowAlphaThreshold The value at which the window alpha is 0.
* @param shapeProgressStart The progress value at which to start the shape reveal.
* @param cornerRadius The corner radius of {@param rect}.
*/
public void update(RectF rect, float alpha, float progress, float windowAlphaThreshold) {
public void update(RectF rect, float alpha, float progress, float shapeProgressStart,
float cornerRadius, boolean isOpening) {
setAlpha(alpha);
LayoutParams lp = (LayoutParams) getLayoutParams();
@ -116,48 +116,41 @@ public class FloatingIconView extends View implements Animator.AnimatorListener,
float scaleX = rect.width() / (float) lp.width;
float scaleY = rect.height() / (float) lp.height;
float scale = mIsAdaptiveIcon ? Math.max(scaleX, scaleY) : Math.min(scaleX, scaleY);
float scale = mIsAdaptiveIcon && !isOpening ? Math.max(scaleX, scaleY)
: Math.min(scaleX, scaleY);
scale = Math.max(1f, scale);
setPivotX(0);
setPivotY(0);
setScaleX(scale);
setScaleY(scale);
// Wait until the window is no longer visible before morphing the icon into its final shape.
float shapeRevealProgress = Utilities.mapToRange(Math.max(windowAlphaThreshold, progress),
windowAlphaThreshold, 1f, 0f, 1, Interpolators.LINEAR);
if (mIsAdaptiveIcon && shapeRevealProgress > 0) {
// shapeRevealProgress = 1 when progress = shapeProgressStart + SHAPE_PROGRESS_DURATION
float toMax = isOpening ? 1 / SHAPE_PROGRESS_DURATION : 1f;
float shapeRevealProgress = Utilities.boundToRange(mapToRange(
Math.max(shapeProgressStart, progress), shapeProgressStart, 1f, 0, toMax,
LINEAR), 0, 1);
mTaskCornerRadius = cornerRadius;
if (mIsAdaptiveIcon && shapeRevealProgress >= 0) {
if (mRevealAnimator == null) {
mEndRevealRect.set(mOutline);
// We play the reveal animation in reverse so that we end with the icon shape.
mRevealAnimator = (ValueAnimator) FolderShape.getShape().createRevealAnimator(this,
mStartRevealRect, mEndRevealRect, mTaskCornerRadius / scale, true);
mRevealAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mRevealAnimator = null;
}
});
mStartRevealRect, mEndRevealRect, mTaskCornerRadius / scale, !isOpening);
mRevealAnimator.start();
// We pause here so we can set the current fraction ourselves.
mRevealAnimator.pause();
}
float bgScale = shapeRevealProgress + mBgDrawableStartScale * (1 - shapeRevealProgress);
setBackgroundDrawableBounds(bgScale);
mRevealAnimator.setCurrentFraction(shapeRevealProgress);
float bgScale = (mBgDrawableEndScale * shapeRevealProgress) + mBgDrawableStartScale
* (1 - shapeRevealProgress);
setBackgroundDrawableBounds(bgScale);
}
invalidate();
invalidateOutline();
}
@Override
public void onAnimationStart(Animator animator) {
if (mStartRunnable != null) {
mStartRunnable.run();
}
}
@Override
public void onAnimationEnd(Animator animator) {
if (mEndRunnable != null) {
@ -180,7 +173,6 @@ public class FloatingIconView extends View implements Animator.AnimatorListener,
Utilities.getLocationBoundsForView(launcher, v, positionOut);
final LayoutParams lp = new LayoutParams(positionOut.width(), positionOut.height());
lp.ignoreInsets = true;
mOriginalHeight = lp.height;
// Position the floating view exactly on top of the original
lp.leftMargin = positionOut.left;
@ -193,11 +185,11 @@ public class FloatingIconView extends View implements Animator.AnimatorListener,
}
@WorkerThread
private void getIcon(Launcher launcher, View v, ItemInfo info, boolean useDrawableAsIs,
float aspectRatio) {
private void getIcon(Launcher launcher, View v, ItemInfo info, boolean isOpening,
Runnable onIconLoadedRunnable) {
final LayoutParams lp = (LayoutParams) getLayoutParams();
Drawable drawable = null;
boolean supportsAdaptiveIcons = ADAPTIVE_ICON_WINDOW_ANIM.get() && !useDrawableAsIs
boolean supportsAdaptiveIcons = ADAPTIVE_ICON_WINDOW_ANIM.get()
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
if (!supportsAdaptiveIcons && v instanceof BubbleTextView) {
// Similar to DragView, we simply use the BubbleTextView icon here.
@ -214,7 +206,7 @@ public class FloatingIconView extends View implements Animator.AnimatorListener,
}
if (drawable == null) {
drawable = Utilities.getFullDrawable(launcher, info, lp.width, lp.height,
useDrawableAsIs, new Object[1]);
false, new Object[1]);
}
Drawable finalDrawable = drawable == null ? null
@ -247,35 +239,50 @@ public class FloatingIconView extends View implements Animator.AnimatorListener,
sbd.setShiftY(sbd.getShiftY() - sTmpRect.top);
}
final int originalHeight = lp.height;
final int originalWidth = lp.width;
int blurMargin = mBlurSizeOutline / 2;
mFinalDrawableBounds.set(0, 0, lp.width, mOriginalHeight);
mFinalDrawableBounds.set(0, 0, originalWidth, originalHeight);
if (!isFolderIcon) {
mFinalDrawableBounds.inset(iconOffset - blurMargin, iconOffset - blurMargin);
}
mForeground.setBounds(mFinalDrawableBounds);
mBackground.setBounds(mFinalDrawableBounds);
if (isFolderIcon) {
mStartRevealRect.set(0, 0, lp.width, mOriginalHeight);
mStartRevealRect.set(0, 0, originalWidth, originalHeight);
if (!isFolderIcon) {
mStartRevealRect.inset(mBlurSizeOutline, mBlurSizeOutline);
}
float aspectRatio = launcher.getDeviceProfile().aspectRatio;
if (launcher.getDeviceProfile().isVerticalBarLayout()) {
lp.width = (int) Math.max(lp.width, lp.height * aspectRatio);
} else {
mStartRevealRect.set(mBlurSizeOutline, mBlurSizeOutline,
lp.width - mBlurSizeOutline, mOriginalHeight - mBlurSizeOutline);
}
if (aspectRatio > 0) {
lp.height = (int) Math.max(lp.height, lp.width * aspectRatio);
layout(lp.leftMargin, lp.topMargin, lp.leftMargin + lp.width, lp.topMargin
+ lp.height);
}
mBgDrawableStartScale = (float) lp.height / mOriginalHeight;
setBackgroundDrawableBounds(mBgDrawableStartScale);
layout(lp.leftMargin, lp.topMargin, lp.leftMargin + lp.width, lp.topMargin
+ lp.height);
// Set up outline
mOutline.set(0, 0, lp.width, lp.height);
Rect rectOutline = new Rect();
float scale = Math.max((float) lp.height / originalHeight,
(float) lp.width / originalWidth);
if (isOpening) {
mBgDrawableStartScale = 1f;
mBgDrawableEndScale = scale;
rectOutline.set(0, 0, originalWidth, originalHeight);
} else {
mBgDrawableStartScale = scale;
mBgDrawableEndScale = 1f;
rectOutline.set(0, 0, lp.width, lp.height);
}
mEndRevealRect.set(0, 0, lp.width, lp.height);
setBackgroundDrawableBounds(mBgDrawableStartScale);
setOutlineProvider(new ViewOutlineProvider() {
@Override
public void getOutline(View view, Outline outline) {
outline.setRoundRect(mOutline, mTaskCornerRadius);
outline.setRoundRect(rectOutline, mTaskCornerRadius);
}
});
setClipToOutline(true);
@ -283,6 +290,7 @@ public class FloatingIconView extends View implements Animator.AnimatorListener,
setBackground(finalDrawable);
}
onIconLoadedRunnable.run();
invalidate();
invalidateOutline();
});
@ -349,6 +357,9 @@ public class FloatingIconView extends View implements Animator.AnimatorListener,
}
}
@Override
public void onAnimationStart(Animator animator) {}
@Override
public void onAnimationCancel(Animator animator) {}
@ -357,17 +368,16 @@ public class FloatingIconView extends View implements Animator.AnimatorListener,
/**
* Creates a floating icon view for {@param originalView}.
*
* @param originalView The view to copy
* @param hideOriginal If true, it will hide {@param originalView} while this view is visible.
* @param useDrawableAsIs If true, we do not separate the foreground/background of adaptive
* icons. TODO(b/122843905): We can remove this once app opening uses new animation.
* @param aspectRatio If >= 0, we will use this aspect ratio for the initial adaptive icon size.
* @param positionOut Rect that will hold the size and position of v.
* @param isOpening True if this view replaces the icon for app open animation.
*/
public static FloatingIconView getFloatingIconView(Launcher launcher, View originalView,
boolean hideOriginal, boolean useDrawableAsIs, float aspectRatio, Rect positionOut,
FloatingIconView recycle) {
boolean hideOriginal, Rect positionOut, boolean isOpening, FloatingIconView recycle) {
if (recycle != null) {
recycle.recycle();
}
FloatingIconView view = recycle != null ? recycle : new FloatingIconView(launcher);
// Match the position of the original view.
@ -376,9 +386,16 @@ public class FloatingIconView extends View implements Animator.AnimatorListener,
// Get the drawable on the background thread
// Must be called after matchPositionOf so that we know what size to load.
if (originalView.getTag() instanceof ItemInfo) {
Runnable onIconLoaded = () -> {
// Delay swapping views until the icon is loaded to prevent a flash.
view.setVisibility(VISIBLE);
if (hideOriginal) {
originalView.setVisibility(INVISIBLE);
}
};
new Handler(LauncherModel.getWorkerLooper()).postAtFrontOfQueue(() -> {
view.getIcon(launcher, originalView, (ItemInfo) originalView.getTag(),
useDrawableAsIs, aspectRatio);
view.getIcon(launcher, originalView, (ItemInfo) originalView.getTag(), isOpening,
onIconLoaded);
});
}
@ -387,12 +404,6 @@ public class FloatingIconView extends View implements Animator.AnimatorListener,
view.setVisibility(INVISIBLE);
((ViewGroup) dragLayer.getParent()).getOverlay().add(view);
view.mStartRunnable = () -> {
view.setVisibility(VISIBLE);
if (hideOriginal) {
originalView.setVisibility(INVISIBLE);
}
};
if (hideOriginal) {
view.mEndRunnable = () -> {
AnimatorSet fade = new AnimatorSet();
@ -442,4 +453,24 @@ public class FloatingIconView extends View implements Animator.AnimatorListener,
}
return view;
}
private void recycle() {
setTranslationX(0);
setTranslationY(0);
setScaleX(1);
setScaleY(1);
setAlpha(1);
setBackground(null);
mEndRunnable = null;
mIsAdaptiveIcon = false;
mForeground = null;
mBackground = null;
mClipPath = null;
mFinalDrawableBounds.setEmpty();
mBgDrawableBounds.setEmpty();;
if (mRevealAnimator != null) {
mRevealAnimator.cancel();
}
mRevealAnimator = null;
}
}