Files
lawnchair/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java
Tony Wickham 02e1875926 Set remote animations duration based on animation being run
Previously we always set the duration to 500, the app launch duration,
but now the animation can resolve to launching a recent task instead,
which uses a shorter duration. This led to effects such as the status
bar transitioning a bit late on those transitions.

Since we don't techinically know whether we are launching an app vs a
recent task until the animation starts (since we need to check the
opening target and corresponding task id), for now we just make an
educated guess based on the view type and launched component.

Change-Id: I8ebf10d24081d474a48a1eea55419651e2214545
2018-03-13 16:57:10 +00:00

957 lines
44 KiB
Java

/*
* Copyright (C) 2018 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;
import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
import static com.android.launcher3.LauncherState.NORMAL;
import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS;
import static com.android.systemui.shared.recents.utilities.Utilities.getNextFrameNumber;
import static com.android.systemui.shared.recents.utilities.Utilities.getSurface;
import static com.android.systemui.shared.recents.utilities.Utilities.postAtFrontOfQueueAsynchronously;
import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.annotation.TargetApi;
import android.app.ActivityOptions;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Handler;
import android.util.Log;
import android.view.Surface;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Interpolator;
import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener;
import com.android.launcher3.InsettableFrameLayout.LayoutParams;
import com.android.launcher3.allapps.AllAppsTransitionController;
import com.android.launcher3.anim.AnimatorPlaybackController;
import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.anim.PropertyListBuilder;
import com.android.launcher3.dragndrop.DragLayer;
import com.android.launcher3.graphics.DrawableFactory;
import com.android.launcher3.shortcuts.DeepShortcutTextView;
import com.android.launcher3.shortcuts.DeepShortcutView;
import com.android.quickstep.RecentsAnimationInterpolator;
import com.android.quickstep.RecentsAnimationInterpolator.TaskWindowBounds;
import com.android.quickstep.RecentsView;
import com.android.quickstep.TaskView;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.system.ActivityCompat;
import com.android.systemui.shared.system.ActivityOptionsCompat;
import com.android.systemui.shared.system.RemoteAnimationAdapterCompat;
import com.android.systemui.shared.system.RemoteAnimationDefinitionCompat;
import com.android.systemui.shared.system.RemoteAnimationRunnerCompat;
import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
import com.android.systemui.shared.system.TransactionCompat;
import com.android.systemui.shared.system.WindowManagerWrapper;
/**
* Manages the opening and closing app transitions from Launcher.
*/
@TargetApi(Build.VERSION_CODES.O)
@SuppressWarnings("unused")
public class LauncherAppTransitionManagerImpl extends LauncherAppTransitionManager
implements OnDeviceProfileChangeListener {
private static final String TAG = "LauncherTransition";
private static final int REFRESH_RATE_MS = 16;
private static final int STATUS_BAR_TRANSITION_DURATION = 120;
private static final String CONTROL_REMOTE_APP_TRANSITION_PERMISSION =
"android.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS";
private static final int 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 = 233;
private static final int RECENTS_LAUNCH_DURATION = 336;
private static final int LAUNCHER_RESUME_START_DELAY = 100;
private static final int CLOSING_TRANSITION_DURATION_MS = 350;
// Progress = 0: All apps is fully pulled up, Progress = 1: All apps is fully pulled down.
private static final float ALL_APPS_PROGRESS_OFF_SCREEN = 1.3059858f;
private static final float ALL_APPS_PROGRESS_OVERSHOOT = 0.99581414f;
private final DragLayer mDragLayer;
private final Launcher mLauncher;
private DeviceProfile mDeviceProfile;
private final float mContentTransY;
private final float mWorkspaceTransY;
private final float mRecentsTransX;
private final float mRecentsTransY;
private final float mRecentsScale;
private View mFloatingView;
private boolean mIsRtl;
private LauncherTransitionAnimator mCurrentAnimator;
public LauncherAppTransitionManagerImpl(Context context) {
mLauncher = Launcher.getLauncher(context);
mDragLayer = mLauncher.getDragLayer();
mDeviceProfile = mLauncher.getDeviceProfile();
mIsRtl = Utilities.isRtl(mLauncher.getResources());
Resources res = mLauncher.getResources();
mContentTransY = res.getDimensionPixelSize(R.dimen.content_trans_y);
mWorkspaceTransY = res.getDimensionPixelSize(R.dimen.workspace_trans_y);
mRecentsTransX = res.getDimensionPixelSize(R.dimen.recents_adjacent_trans_x);
mRecentsTransY = res.getDimensionPixelSize(R.dimen.recents_adjacent_trans_y);
mRecentsScale = res.getFraction(R.fraction.recents_adjacent_scale, 1, 1);
mLauncher.addOnDeviceProfileChangeListener(this);
registerRemoteAnimations();
}
@Override
public void onDeviceProfileChanged(DeviceProfile dp) {
mDeviceProfile = dp;
}
private void setCurrentAnimator(LauncherTransitionAnimator animator) {
if (isAnimating()) {
mCurrentAnimator.cancel();
}
mCurrentAnimator = animator;
}
@Override
public void finishLauncherAnimation() {
if (isAnimating()) {
mCurrentAnimator.finishLauncherAnimation();
}
mCurrentAnimator = null;
}
@Override
public boolean isAnimating() {
return mCurrentAnimator != null && mCurrentAnimator.isRunning();
}
/**
* @return ActivityOptions with remote animations that controls how the window of the opening
* targets are displayed.
*/
@Override
public ActivityOptions getActivityLaunchOptions(Launcher launcher, View v) {
if (hasControlRemoteAppTransitionPermission()) {
TaskView taskView = findTaskViewToLaunch(launcher, v);
try {
RemoteAnimationRunnerCompat runner = new LauncherAnimationRunner(mLauncher) {
@Override
public void onAnimationStart(RemoteAnimationTargetCompat[] targets,
Runnable finishedCallback) {
// Post at front of queue ignoring sync barriers to make sure it gets
// processed before the next frame.
postAtFrontOfQueueAsynchronously(v.getHandler(), () -> {
final boolean removeTrackingView;
LauncherTransitionAnimator animator = composeRecentsLaunchAnimator(
taskView == null ? v : taskView, targets);
if (animator != null) {
// We are animating the task view directly, do not remove it after
removeTrackingView = false;
} else {
animator = composeAppLaunchAnimator(v, targets);
// A new floating view is created for the animation, remove it after
removeTrackingView = true;
}
setCurrentAnimator(animator);
mAnimator = animator.getAnimatorSet();
mAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
// Reset launcher to normal state
v.setVisibility(View.VISIBLE);
if (removeTrackingView) {
((ViewGroup) mDragLayer.getParent()).removeView(
mFloatingView);
}
mDragLayer.setAlpha(1f);
mDragLayer.setTranslationY(0f);
View appsView = mLauncher.getAppsView();
appsView.setAlpha(1f);
appsView.setTranslationY(0f);
finishedCallback.run();
}
});
mAnimator.start();
// Because t=0 has the app icon in its original spot, we can skip the
// first frame and have the same movement one frame earlier.
mAnimator.setCurrentPlayTime(REFRESH_RATE_MS);
});
}
};
int duration = taskView != null ? RECENTS_LAUNCH_DURATION : APP_LAUNCH_DURATION;
int statusBarTransitionDelay = duration - STATUS_BAR_TRANSITION_DURATION;
return ActivityOptionsCompat.makeRemoteAnimation(new RemoteAnimationAdapterCompat(
runner, duration, statusBarTransitionDelay));
} catch (NoClassDefFoundError e) {
// Gracefully fall back to default launch options if the user's platform doesn't
// have the latest changes.
}
}
return getDefaultActivityLaunchOptions(launcher, v);
}
/**
* Try to find a TaskView that corresponds with the component of the launched view.
*
* If this method returns a non-null TaskView, it will be used in composeRecentsLaunchAnimation.
* Otherwise, we will assume we are using a normal app transition, but it's possible that the
* opening remote target (which we don't get until onAnimationStart) will resolve to a TaskView.
*/
private TaskView findTaskViewToLaunch(Launcher launcher, View v) {
if (v instanceof TaskView) {
return (TaskView) v;
}
if (!launcher.isInState(LauncherState.OVERVIEW)) {
return null;
}
if (v.getTag() instanceof ItemInfo) {
ItemInfo itemInfo = (ItemInfo) v.getTag();
ComponentName componentName = itemInfo.getTargetComponent();
if (componentName != null) {
RecentsView recentsView = launcher.getOverviewPanel();
for (int i = 0; i < recentsView.getChildCount(); i++) {
TaskView taskView = (TaskView) recentsView.getPageAt(i);
if (recentsView.isTaskViewVisible(taskView)) {
Task task = taskView.getTask();
if (componentName.equals(task.key.getComponent())) {
return taskView;
}
}
}
}
}
return null;
}
/**
* Composes the animations for a launch from the recents list if possible.
*/
private LauncherTransitionAnimator composeRecentsLaunchAnimator(View v,
RemoteAnimationTargetCompat[] targets) {
RecentsView recentsView = mLauncher.getOverviewPanel();
boolean launcherClosing = launcherIsATargetWithMode(targets, MODE_CLOSING);
MutableBoolean skipLauncherChanges = new MutableBoolean(!launcherClosing);
if (v instanceof TaskView) {
// We already found a task view to launch, so use that for the animation.
TaskView taskView = (TaskView) v;
return new LauncherTransitionAnimator(getRecentsLauncherAnimator(recentsView, taskView),
getRecentsWindowAnimator(taskView, skipLauncherChanges, targets));
}
// It's possible that the launched view can still be resolved to a visible task view, check
// the task id of the opening task and see if we can find a match.
// Ensure recents is actually visible
if (!mLauncher.getStateManager().getState().overviewUi) {
return null;
}
// Resolve the opening task id
int openingTaskId = -1;
for (RemoteAnimationTargetCompat target : targets) {
if (target.mode == MODE_OPENING) {
openingTaskId = target.taskId;
break;
}
}
// If there is no opening task id, fall back to the normal app icon launch animation
if (openingTaskId == -1) {
return null;
}
// If the opening task id is not currently visible in overview, then fall back to normal app
// icon launch animation
TaskView taskView = recentsView.getTaskView(openingTaskId);
if (taskView == null || !recentsView.isTaskViewVisible(taskView)) {
return null;
}
// Found a visible recents task that matches the opening app, lets launch the app from there
Animator launcherAnim;
AnimatorListenerAdapter windowAnimEndListener;
if (launcherClosing) {
launcherAnim = getRecentsLauncherAnimator(recentsView, taskView);
windowAnimEndListener = new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
// Make sure recents gets fixed up by resetting task alphas and scales, etc.
mLauncher.getStateManager().reapplyState();
}
};
} else {
AnimatorPlaybackController controller =
mLauncher.getStateManager()
.createAnimationToNewWorkspace(NORMAL, RECENTS_LAUNCH_DURATION);
controller.dispatchOnStart();
launcherAnim = controller.getAnimationPlayer().setDuration(RECENTS_LAUNCH_DURATION);
windowAnimEndListener = new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mLauncher.getStateManager().goToState(NORMAL, false);
}
};
}
Animator windowAnim = getRecentsWindowAnimator(taskView, skipLauncherChanges, targets);
windowAnim.addListener(windowAnimEndListener);
return new LauncherTransitionAnimator(launcherAnim, windowAnim, skipLauncherChanges);
}
/**
* Animate adjacent tasks off screen while scaling up, and translate hotseat off screen as well.
*
* If launching one of the adjacent tasks, parallax the center task and other adjacent task
* to the right.
*/
private Animator getRecentsLauncherAnimator(RecentsView recentsView, TaskView v) {
AnimatorSet launcherAnimator = new AnimatorSet();
int launchedTaskIndex = recentsView.indexOfChild(v);
int centerTaskIndex = recentsView.getCurrentPage();
boolean launchingCenterTask = launchedTaskIndex == centerTaskIndex;
boolean isRtl = recentsView.isRtl();
if (launchingCenterTask) {
if (launchedTaskIndex - 1 >= 0) {
TaskView adjacentPage1 = (TaskView) recentsView.getPageAt(launchedTaskIndex - 1);
ObjectAnimator adjacentTask1ScaleAndTranslate =
LauncherAnimUtils.ofPropertyValuesHolder(adjacentPage1,
new PropertyListBuilder()
.scale(adjacentPage1.getScaleX() * mRecentsScale)
.translationY(mRecentsTransY)
.translationX(isRtl ? mRecentsTransX : -mRecentsTransX)
.build());
launcherAnimator.play(adjacentTask1ScaleAndTranslate);
}
if (launchedTaskIndex + 1 < recentsView.getPageCount()) {
TaskView adjacentTask2 = (TaskView) recentsView.getPageAt(launchedTaskIndex + 1);
ObjectAnimator adjacentTask2ScaleAndTranslate =
LauncherAnimUtils.ofPropertyValuesHolder(adjacentTask2,
new PropertyListBuilder()
.scale(adjacentTask2.getScaleX() * mRecentsScale)
.translationY(mRecentsTransY)
.translationX(isRtl ? -mRecentsTransX : mRecentsTransX)
.build());
launcherAnimator.play(adjacentTask2ScaleAndTranslate);
}
} else {
// We are launching an adjacent task, so parallax the center and other adjacent task.
TaskView centerTask = (TaskView) recentsView.getPageAt(centerTaskIndex);
float translationX = Math.abs(v.getTranslationX());
ObjectAnimator centerTaskParallaxToRight =
LauncherAnimUtils.ofPropertyValuesHolder(centerTask,
new PropertyListBuilder()
.scale(v.getScaleX())
.translationX(isRtl ? -translationX : translationX)
.build());
launcherAnimator.play(centerTaskParallaxToRight);
int otherAdjacentTaskIndex = centerTaskIndex + (centerTaskIndex - launchedTaskIndex);
if (otherAdjacentTaskIndex >= 0
&& otherAdjacentTaskIndex < recentsView.getPageCount()) {
TaskView otherAdjacentTask = (TaskView) recentsView.getPageAt(
otherAdjacentTaskIndex);
ObjectAnimator otherAdjacentTaskParallaxToRight =
LauncherAnimUtils.ofPropertyValuesHolder(otherAdjacentTask,
new PropertyListBuilder()
.translationX(otherAdjacentTask.getTranslationX()
+ (isRtl ? -translationX : translationX))
.build());
launcherAnimator.play(otherAdjacentTaskParallaxToRight);
}
}
Animator allAppsSlideOut = ObjectAnimator.ofFloat(mLauncher.getAllAppsController(),
ALL_APPS_PROGRESS, ALL_APPS_PROGRESS_OFF_SCREEN);
launcherAnimator.play(allAppsSlideOut);
Workspace workspace = mLauncher.getWorkspace();
float[] workspaceScaleAndTranslation = NORMAL
.getWorkspaceScaleAndTranslation(mLauncher);
Animator recenterWorkspace = LauncherAnimUtils.ofPropertyValuesHolder(
workspace, new PropertyListBuilder()
.translationX(workspaceScaleAndTranslation[1])
.translationY(workspaceScaleAndTranslation[2])
.build());
launcherAnimator.play(recenterWorkspace);
CellLayout currentWorkspacePage = (CellLayout) workspace.getPageAt(
workspace.getCurrentPage());
launcherAnimator.setInterpolator(Interpolators.TOUCH_RESPONSE_INTERPOLATOR);
launcherAnimator.setDuration(RECENTS_LAUNCH_DURATION);
return launcherAnimator;
}
/**
* @return Animator that controls the window of the opening targets for the recents launch
* animation.
*/
private ValueAnimator getRecentsWindowAnimator(TaskView v, MutableBoolean skipLauncherChanges,
RemoteAnimationTargetCompat[] targets) {
Rect taskViewBounds = new Rect();
mDragLayer.getDescendantRectRelativeToSelf(v, taskViewBounds);
// TODO: Use the actual target insets instead of the current thumbnail insets in case the
// device state has changed
RecentsAnimationInterpolator recentsInterpolator = new RecentsAnimationInterpolator(
new Rect(0, 0, mDeviceProfile.widthPx, mDeviceProfile.heightPx),
v.getThumbnail().getInsets(),
taskViewBounds,
new Rect(0, v.getThumbnail().getTop(), 0, 0),
v.getScaleX(),
v.getTranslationX());
Rect crop = new Rect();
Matrix matrix = new Matrix();
ValueAnimator appAnimator = ValueAnimator.ofFloat(0, 1);
appAnimator.setDuration(RECENTS_LAUNCH_DURATION);
appAnimator.setInterpolator(Interpolators.TOUCH_RESPONSE_INTERPOLATOR);
appAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
boolean isFirstFrame = true;
@Override
public void onAnimationUpdate(ValueAnimator animation) {
final Surface surface = getSurface(v);
final long frameNumber = surface != null ? getNextFrameNumber(surface) : -1;
if (frameNumber == -1) {
// Booo, not cool! Our surface got destroyed, so no reason to animate anything.
Log.w(TAG, "Failed to animate, surface got destroyed.");
return;
}
final float percent = animation.getAnimatedFraction();
TaskWindowBounds tw = recentsInterpolator.interpolate(percent);
if (!skipLauncherChanges.value) {
v.setScaleX(tw.taskScale);
v.setScaleY(tw.taskScale);
v.setTranslationX(tw.taskX);
v.setTranslationY(tw.taskY);
// Defer fading out the view until after the app window gets faded in
v.setAlpha(getValue(1f, 0f, 75, 75,
appAnimator.getDuration() * percent, Interpolators.LINEAR));
}
matrix.setScale(tw.winScale, tw.winScale);
matrix.postTranslate(tw.winX, tw.winY);
crop.set(tw.winCrop);
// Fade in the app window.
float alphaDelay = 0;
float alphaDuration = 75;
float alpha = getValue(0f, 1f, alphaDelay, alphaDuration,
appAnimator.getDuration() * percent, Interpolators.LINEAR);
TransactionCompat t = new TransactionCompat();
for (RemoteAnimationTargetCompat target : targets) {
if (target.mode == RemoteAnimationTargetCompat.MODE_OPENING) {
t.setAlpha(target.leash, alpha);
// TODO: This isn't correct at the beginning of the animation, but better
// than nothing.
matrix.postTranslate(target.position.x, target.position.y);
t.setMatrix(target.leash, matrix);
t.setWindowCrop(target.leash, crop);
if (!skipLauncherChanges.value) {
t.deferTransactionUntil(target.leash, surface, frameNumber);
}
}
if (isFirstFrame) {
t.show(target.leash);
}
}
t.apply();
matrix.reset();
isFirstFrame = false;
}
});
return appAnimator;
}
/**
* Composes the animations for a launch from an app icon.
*/
private LauncherTransitionAnimator composeAppLaunchAnimator(View v,
RemoteAnimationTargetCompat[] targets) {
return new LauncherTransitionAnimator(getLauncherAnimators(v, targets),
getWindowAnimators(v, targets));
}
/**
* @return Animators that control the movements of the Launcher and icon of the opening target.
*/
private AnimatorSet getLauncherAnimators(View v, RemoteAnimationTargetCompat[] targets) {
AnimatorSet launcherAnimators = new AnimatorSet();
launcherAnimators.play(getIconAnimator(v));
if (launcherIsATargetWithMode(targets, MODE_CLOSING)) {
launcherAnimators.play(getLauncherContentAnimator(false /* show */));
}
return launcherAnimators;
}
/**
* Content is everything on screen except the background and the floating view (if any).
*
* @param show If true: Animate the content so that it moves upwards and fades in.
* Else: Animate the content so that it moves downwards and fades out.
*/
private AnimatorSet getLauncherContentAnimator(boolean show) {
AnimatorSet launcherAnimator = new AnimatorSet();
float[] alphas = show
? new float[] {0, 1}
: new float[] {1, 0};
float[] trans = show
? new float[] {mContentTransY, 0,}
: new float[] {0, mContentTransY};
if (mLauncher.isInState(LauncherState.ALL_APPS) && !mDeviceProfile.isVerticalBarLayout()) {
// All Apps in portrait mode is full screen, so we only animate AllAppsContainerView.
View appsView = mLauncher.getAppsView();
appsView.setAlpha(alphas[0]);
appsView.setTranslationY(trans[0]);
ObjectAnimator alpha = ObjectAnimator.ofFloat(appsView, View.ALPHA, alphas);
alpha.setDuration(217);
alpha.setInterpolator(Interpolators.LINEAR);
ObjectAnimator transY = ObjectAnimator.ofFloat(appsView, View.TRANSLATION_Y, trans);
transY.setInterpolator(Interpolators.AGGRESSIVE_EASE);
transY.setDuration(350);
launcherAnimator.play(alpha);
launcherAnimator.play(transY);
} else {
mDragLayer.setAlpha(alphas[0]);
mDragLayer.setTranslationY(trans[0]);
ObjectAnimator dragLayerAlpha = ObjectAnimator.ofFloat(mDragLayer, View.ALPHA, alphas);
dragLayerAlpha.setDuration(217);
dragLayerAlpha.setInterpolator(Interpolators.LINEAR);
ObjectAnimator dragLayerTransY = ObjectAnimator.ofFloat(mDragLayer, View.TRANSLATION_Y,
trans);
dragLayerTransY.setInterpolator(Interpolators.AGGRESSIVE_EASE);
dragLayerTransY.setDuration(350);
launcherAnimator.play(dragLayerAlpha);
launcherAnimator.play(dragLayerTransY);
}
return launcherAnimator;
}
/**
* @return Animator that controls the icon used to launch the target.
*/
private AnimatorSet getIconAnimator(View v) {
final boolean isBubbleTextView = v instanceof BubbleTextView;
mFloatingView = new View(mLauncher);
if (isBubbleTextView && v.getTag() instanceof ItemInfoWithIcon ) {
// Create a copy of the app icon
mFloatingView.setBackground(
DrawableFactory.get(mLauncher).newIcon((ItemInfoWithIcon) v.getTag()));
}
// Position the floating view exactly on top of the original
Rect rect = new Rect();
final boolean isDeepShortcutTextView = v instanceof DeepShortcutTextView
&& v.getParent() != null && v.getParent() instanceof DeepShortcutView;
if (isDeepShortcutTextView) {
// Deep shortcut views have their icon drawn in a sibling view.
DeepShortcutView view = (DeepShortcutView) v.getParent();
mDragLayer.getDescendantRectRelativeToSelf(view.getIconView(), rect);
} else {
mDragLayer.getDescendantRectRelativeToSelf(v, rect);
}
final int viewLocationStart = mIsRtl
? mDeviceProfile.widthPx - rect.right
: rect.left;
final int viewLocationTop = rect.top;
float startScale = 1f;
if (isBubbleTextView && !isDeepShortcutTextView) {
BubbleTextView btv = (BubbleTextView) v;
btv.getIconBounds(rect);
Drawable dr = btv.getIcon();
if (dr instanceof FastBitmapDrawable) {
startScale = ((FastBitmapDrawable) dr).getAnimatedScale();
}
} else {
rect.set(0, 0, rect.width(), rect.height());
}
LayoutParams lp = new LayoutParams(rect.width(), rect.height());
lp.ignoreInsets = true;
lp.setMarginStart(viewLocationStart + rect.left);
lp.topMargin = viewLocationTop + rect.top;
mFloatingView.setLayoutParams(lp);
// Swap the two views in place.
((ViewGroup) mDragLayer.getParent()).addView(mFloatingView);
v.setVisibility(View.INVISIBLE);
AnimatorSet appIconAnimatorSet = new AnimatorSet();
// Animate the app icon to the center
float centerX = mDeviceProfile.widthPx / 2;
float centerY = mDeviceProfile.heightPx / 2;
float xPosition = mIsRtl
? mDeviceProfile.widthPx - lp.getMarginStart() - rect.width()
: lp.getMarginStart();
float dX = centerX - xPosition - (lp.width / 2);
float dY = centerY - lp.topMargin - (lp.height / 2);
ObjectAnimator x = ObjectAnimator.ofFloat(mFloatingView, View.TRANSLATION_X, 0f, dX);
ObjectAnimator y = ObjectAnimator.ofFloat(mFloatingView, View.TRANSLATION_Y, 0f, dY);
// Adjust the duration to change the "curve" of the app icon to the center.
boolean isBelowCenterY = lp.topMargin < centerY;
x.setDuration(isBelowCenterY ? APP_LAUNCH_DURATION : APP_LAUNCH_CURVED_DURATION);
y.setDuration(isBelowCenterY ? APP_LAUNCH_CURVED_DURATION : APP_LAUNCH_DURATION);
x.setInterpolator(Interpolators.AGGRESSIVE_EASE);
y.setInterpolator(Interpolators.AGGRESSIVE_EASE);
appIconAnimatorSet.play(x);
appIconAnimatorSet.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 = mDeviceProfile.widthPx / (float) rect.width();
float maxScaleY = mDeviceProfile.heightPx / (float) rect.height();
float scale = Math.max(maxScaleX, maxScaleY);
ObjectAnimator scaleAnim = ObjectAnimator
.ofFloat(mFloatingView, SCALE_PROPERTY, startScale, scale);
scaleAnim.setDuration(APP_LAUNCH_DURATION).setInterpolator(Interpolators.EXAGGERATED_EASE);
appIconAnimatorSet.play(scaleAnim);
// Fade out the app icon.
ObjectAnimator alpha = ObjectAnimator.ofFloat(mFloatingView, View.ALPHA, 1f, 0f);
alpha.setStartDelay(32);
alpha.setDuration(50);
alpha.setInterpolator(Interpolators.LINEAR);
appIconAnimatorSet.play(alpha);
return appIconAnimatorSet;
}
/**
* @return Animator that controls the window of the opening targets.
*/
private ValueAnimator getWindowAnimators(View v, RemoteAnimationTargetCompat[] targets) {
Rect bounds = new Rect();
if (v instanceof BubbleTextView) {
((BubbleTextView) v).getIconBounds(bounds);
} else {
mDragLayer.getDescendantRectRelativeToSelf(v, bounds);
}
int[] floatingViewBounds = new int[2];
Rect crop = new Rect();
Matrix matrix = new Matrix();
ValueAnimator appAnimator = ValueAnimator.ofFloat(0, 1);
appAnimator.setDuration(APP_LAUNCH_DURATION);
appAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
boolean isFirstFrame = true;
@Override
public void onAnimationUpdate(ValueAnimator animation) {
final Surface surface = getSurface(mFloatingView);
final long frameNumber = surface != null ? getNextFrameNumber(surface) : -1;
if (frameNumber == -1) {
// Booo, not cool! Our surface got destroyed, so no reason to animate anything.
Log.w(TAG, "Failed to animate, surface got destroyed.");
return;
}
final float percent = animation.getAnimatedFraction();
final float easePercent = Interpolators.AGGRESSIVE_EASE.getInterpolation(percent);
// Calculate app icon size.
float iconWidth = bounds.width() * mFloatingView.getScaleX();
float iconHeight = bounds.height() * mFloatingView.getScaleY();
// Scale the app window to match the icon size.
float scaleX = iconWidth / mDeviceProfile.widthPx;
float scaleY = iconHeight / mDeviceProfile.heightPx;
float scale = Math.min(1f, Math.min(scaleX, scaleY));
matrix.setScale(scale, scale);
// Position the scaled window on top of the icon
int deviceWidth = mDeviceProfile.widthPx;
int deviceHeight = mDeviceProfile.heightPx;
float scaledWindowWidth = deviceWidth * scale;
float scaledWindowHeight = deviceHeight * scale;
float offsetX = (scaledWindowWidth - iconWidth) / 2;
float offsetY = (scaledWindowHeight - iconHeight) / 2;
mFloatingView.getLocationInWindow(floatingViewBounds);
float transX0 = floatingViewBounds[0] - offsetX;
float transY0 = floatingViewBounds[1] - offsetY;
matrix.postTranslate(transX0, transY0);
// Fade in the app window.
float alphaDelay = 0;
float alphaDuration = 60;
float alpha = getValue(0f, 1f, alphaDelay, alphaDuration,
appAnimator.getDuration() * percent, Interpolators.LINEAR);
// Animate the window crop so that it starts off as a square, and then reveals
// horizontally.
float cropHeight = deviceHeight * easePercent + deviceWidth * (1 - easePercent);
float initialTop = (deviceHeight - deviceWidth) / 2f;
crop.left = 0;
crop.top = (int) (initialTop * (1 - easePercent));
crop.right = deviceWidth;
crop.bottom = (int) (crop.top + cropHeight);
TransactionCompat t = new TransactionCompat();
for (RemoteAnimationTargetCompat target : targets) {
if (target.mode == MODE_OPENING) {
t.setAlpha(target.leash, alpha);
// TODO: This isn't correct at the beginning of the animation, but better
// than nothing.
matrix.postTranslate(target.position.x, target.position.y);
t.setMatrix(target.leash, matrix);
t.setWindowCrop(target.leash, crop);
t.deferTransactionUntil(target.leash, surface, getNextFrameNumber(surface));
}
if (isFirstFrame) {
t.show(target.leash);
}
}
t.apply();
matrix.reset();
isFirstFrame = false;
}
});
return appAnimator;
}
/**
* Registers remote animations used when closing apps to home screen.
*/
private void registerRemoteAnimations() {
if (hasControlRemoteAppTransitionPermission()) {
try {
RemoteAnimationDefinitionCompat definition = new RemoteAnimationDefinitionCompat();
definition.addRemoteAnimation(WindowManagerWrapper.TRANSIT_WALLPAPER_OPEN,
new RemoteAnimationAdapterCompat(getWallpaperOpenRunner(),
CLOSING_TRANSITION_DURATION_MS, 0 /* statusBarTransitionDelay */));
// TODO: App controlled transition for unlock to home TRANSIT_KEYGUARD_GOING_AWAY_ON_WALLPAPER
new ActivityCompat(mLauncher).registerRemoteAnimations(definition);
} catch (NoClassDefFoundError e) {
// Gracefully fall back if the user's platform doesn't have the latest changes
}
}
}
private boolean launcherIsATargetWithMode(RemoteAnimationTargetCompat[] targets, int mode) {
int launcherTaskId = mLauncher.getTaskId();
for (RemoteAnimationTargetCompat target : targets) {
if (target.mode == mode && target.taskId == launcherTaskId) {
return true;
}
}
return false;
}
/**
* @return Runner that plays when user goes to Launcher
* ie. pressing home, swiping up from nav bar.
*/
private RemoteAnimationRunnerCompat getWallpaperOpenRunner() {
return new LauncherAnimationRunner(mLauncher) {
@Override
public void onAnimationStart(RemoteAnimationTargetCompat[] targets,
Runnable finishedCallback) {
Handler handler = mLauncher.getWindow().getDecorView().getHandler();
postAtFrontOfQueueAsynchronously(handler, () -> {
if ((Utilities.getPrefs(mLauncher)
.getBoolean("pref_use_screenshot_for_swipe_up", false)
&& mLauncher.getStateManager().getState().overviewUi)
|| !launcherIsATargetWithMode(targets, MODE_OPENING)) {
// We use a separate transition for Overview mode. And we can skip the
// animation in cases where Launcher is not in the set of opening targets.
// This can happen when Launcher is already visible. ie. Closing a dialog.
setCurrentAnimator(null);
finishedCallback.run();
return;
}
LauncherTransitionAnimator animator = new LauncherTransitionAnimator(
getLauncherResumeAnimation(), getClosingWindowAnimators(targets));
setCurrentAnimator(animator);
mAnimator = animator.getAnimatorSet();
mAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
finishedCallback.run();
}
});
mAnimator.start();
// Because t=0 has the app icon in its original spot, we can skip the
// first frame and have the same movement one frame earlier.
mAnimator.setCurrentPlayTime(REFRESH_RATE_MS);
});
}
};
}
/**
* Animator that controls the transformations of the windows the targets that are closing.
*/
private Animator getClosingWindowAnimators(RemoteAnimationTargetCompat[] targets) {
Matrix matrix = new Matrix();
float height = mLauncher.getDeviceProfile().heightPx;
float width = mLauncher.getDeviceProfile().widthPx;
float endX = (mLauncher.<RecentsView>getOverviewPanel().isRtl() ? -width : width) * 1.16f;
ValueAnimator closingAnimator = ValueAnimator.ofFloat(0, 1);
closingAnimator.setDuration(CLOSING_TRANSITION_DURATION_MS);
closingAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
boolean isFirstFrame = true;
@Override
public void onAnimationUpdate(ValueAnimator animation) {
final float percent = animation.getAnimatedFraction();
float currentPlayTime = percent * closingAnimator.getDuration();
float scale = getValue(1f, 0.8f, 0, 267, currentPlayTime,
Interpolators.AGGRESSIVE_EASE);
float dX = getValue(0, endX, 0, 350, currentPlayTime,
Interpolators.AGGRESSIVE_EASE_IN_OUT);
TransactionCompat t = new TransactionCompat();
for (RemoteAnimationTargetCompat app : targets) {
if (app.mode == RemoteAnimationTargetCompat.MODE_CLOSING) {
t.setAlpha(app.leash, getValue(1f, 0f, 0, 350, currentPlayTime,
Interpolators.APP_CLOSE_ALPHA));
matrix.setScale(scale, scale,
app.sourceContainerBounds.centerX(),
app.sourceContainerBounds.centerY());
matrix.postTranslate(dX, 0);
matrix.postTranslate(app.position.x, app.position.y);
t.setMatrix(app.leash, matrix);
}
if (isFirstFrame) {
int layer = app.mode == RemoteAnimationTargetCompat.MODE_CLOSING
? Integer.MAX_VALUE
: app.prefixOrderIndex;
t.setLayer(app.leash, layer);
t.show(app.leash);
}
}
t.apply();
matrix.reset();
isFirstFrame = false;
}
});
return closingAnimator;
}
/**
* @return Animator that modifies Launcher as a result from {@link #getWallpaperOpenRunner}.
*/
private AnimatorSet getLauncherResumeAnimation() {
if (mLauncher.isInState(LauncherState.ALL_APPS)
|| mLauncher.getDeviceProfile().isVerticalBarLayout()) {
AnimatorSet contentAnimator = getLauncherContentAnimator(true /* show */);
contentAnimator.setStartDelay(LAUNCHER_RESUME_START_DELAY);
return contentAnimator;
} else {
AnimatorSet workspaceAnimator = new AnimatorSet();
mLauncher.getWorkspace().setTranslationY(mWorkspaceTransY);
mLauncher.getWorkspace().setAlpha(0f);
workspaceAnimator.play(ObjectAnimator.ofFloat(mLauncher.getWorkspace(),
View.TRANSLATION_Y, mWorkspaceTransY, 0));
workspaceAnimator.play(ObjectAnimator.ofFloat(mLauncher.getWorkspace(), View.ALPHA,
0, 1f));
workspaceAnimator.setStartDelay(LAUNCHER_RESUME_START_DELAY);
workspaceAnimator.setDuration(333);
workspaceAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
// Animate the shelf in two parts: slide in, and overeshoot.
AllAppsTransitionController allAppsController = mLauncher.getAllAppsController();
// The shelf will start offscreen
final float startY = ALL_APPS_PROGRESS_OFF_SCREEN;
// And will end slightly pulled up, so that there is something to overshoot back to 1f.
final float slideEnd = ALL_APPS_PROGRESS_OVERSHOOT;
allAppsController.setProgress(startY);
Animator allAppsSlideIn =
ObjectAnimator.ofFloat(allAppsController, ALL_APPS_PROGRESS, startY, slideEnd);
allAppsSlideIn.setStartDelay(LAUNCHER_RESUME_START_DELAY);
allAppsSlideIn.setDuration(317);
allAppsSlideIn.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
Animator allAppsOvershoot =
ObjectAnimator.ofFloat(allAppsController, ALL_APPS_PROGRESS, slideEnd, 1f);
allAppsOvershoot.setDuration(153);
allAppsOvershoot.setInterpolator(Interpolators.OVERSHOOT_0);
AnimatorSet resumeLauncherAnimation = new AnimatorSet();
resumeLauncherAnimation.play(workspaceAnimator);
resumeLauncherAnimation.playSequentially(allAppsSlideIn, allAppsOvershoot);
return resumeLauncherAnimation;
}
}
private boolean hasControlRemoteAppTransitionPermission() {
return mLauncher.checkSelfPermission(CONTROL_REMOTE_APP_TRANSITION_PERMISSION)
== PackageManager.PERMISSION_GRANTED;
}
/**
* Helper method that allows us to get interpolated values for embedded
* animations with a delay and/or different duration.
*/
private static float getValue(float start, float end, float delay, float duration,
float currentPlayTime, Interpolator i) {
float time = Math.max(0, currentPlayTime - delay);
float newPercent = Math.min(1f, time / duration);
newPercent = i.getInterpolation(newPercent);
return end * newPercent + start * (1 - newPercent);
}
}