/* * 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.BaseActivity.INVISIBLE_ALL; import static com.android.launcher3.BaseActivity.INVISIBLE_BY_APP_TRANSITIONS; import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY; import static com.android.launcher3.LauncherState.ALL_APPS; import static com.android.launcher3.LauncherState.NORMAL; 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.LINEAR; import static com.android.launcher3.dragndrop.DragLayer.ALPHA_INDEX_TRANSITIONS; import static com.android.quickstep.TaskUtils.findTaskViewToLaunch; import static com.android.quickstep.TaskUtils.getRecentsWindowAnimator; import static com.android.quickstep.TaskUtils.taskIsATargetWithMode; 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.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.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.CancellationSignal; import android.os.Handler; import android.os.Looper; import android.util.Log; import android.util.Pair; import android.view.Surface; 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.AnimatorPlaybackController; import com.android.launcher3.anim.Interpolators; import com.android.launcher3.dragndrop.DragLayer; import com.android.launcher3.graphics.DrawableFactory; import com.android.launcher3.shortcuts.DeepShortcutView; import com.android.launcher3.util.MultiValueAlpha; import com.android.launcher3.util.MultiValueAlpha.AlphaProperty; import com.android.quickstep.util.ClipAnimationHelper; import com.android.quickstep.util.MultiValueUpdateListener; import com.android.quickstep.util.RemoteAnimationProvider; import com.android.quickstep.views.RecentsView; import com.android.quickstep.views.TaskView; 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"; public 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 = APP_LAUNCH_DURATION / 2; // 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; public static final int RECENTS_LAUNCH_DURATION = 336; public static final int RECENTS_QUICKSCRUB_LAUNCH_DURATION = 300; private static final int LAUNCHER_RESUME_START_DELAY = 100; private static final int CLOSING_TRANSITION_DURATION_MS = 250; // Progress = 0: All apps is fully pulled up, Progress = 1: All apps is fully pulled down. public static final float ALL_APPS_PROGRESS_OFF_SCREEN = 1.3059858f; private final Launcher mLauncher; private final DragLayer mDragLayer; private final AlphaProperty mDragLayerAlpha; private final Handler mHandler; private final boolean mIsRtl; private final float mContentTransY; private final float mWorkspaceTransY; private final float mClosingWindowTransY; private DeviceProfile mDeviceProfile; private View mFloatingView; private RemoteAnimationProvider mRemoteAnimationProvider; private final AnimatorListenerAdapter mForceInvisibleListener = new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { mLauncher.addForceInvisibleFlag(INVISIBLE_BY_APP_TRANSITIONS); } @Override public void onAnimationEnd(Animator animation) { mLauncher.clearForceInvisibleFlag(INVISIBLE_BY_APP_TRANSITIONS); } }; public LauncherAppTransitionManagerImpl(Context context) { mLauncher = Launcher.getLauncher(context); mDragLayer = mLauncher.getDragLayer(); mDragLayerAlpha = mDragLayer.getAlphaProperty(ALPHA_INDEX_TRANSITIONS); mHandler = new Handler(Looper.getMainLooper()); mIsRtl = Utilities.isRtl(mLauncher.getResources()); mDeviceProfile = mLauncher.getDeviceProfile(); Resources res = mLauncher.getResources(); mContentTransY = res.getDimensionPixelSize(R.dimen.content_trans_y); mWorkspaceTransY = res.getDimensionPixelSize(R.dimen.workspace_trans_y); mClosingWindowTransY = res.getDimensionPixelSize(R.dimen.closing_window_trans_y); mLauncher.addOnDeviceProfileChangeListener(this); registerRemoteAnimations(); } @Override public void onDeviceProfileChanged(DeviceProfile dp) { mDeviceProfile = dp; } /** * @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()) { RemoteAnimationRunnerCompat runner = new LauncherAnimationRunner(mHandler, true /* startAtFrontOfQueue */) { @Override public void onCreateAnimation(RemoteAnimationTargetCompat[] targetCompats, AnimationResult result) { AnimatorSet anim = new AnimatorSet(); boolean launcherClosing = launcherIsATargetWithMode(targetCompats, MODE_CLOSING); if (!composeRecentsLaunchAnimator(v, targetCompats, anim)) { // Set the state animation first so that any state listeners are called // before our internal listeners. mLauncher.getStateManager().setCurrentAnimation(anim); Rect windowTargetBounds = getWindowTargetBounds(targetCompats); anim.play(getIconAnimator(v, windowTargetBounds)); if (launcherClosing) { Pair launcherContentAnimator = getLauncherContentAnimator(true /* isAppOpening */); anim.play(launcherContentAnimator.first); anim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { launcherContentAnimator.second.run(); } }); } anim.play(getOpeningWindowAnimators(v, targetCompats, windowTargetBounds)); } if (launcherClosing) { anim.addListener(mForceInvisibleListener); } result.setAnimation(anim); } }; int duration = findTaskViewToLaunch(launcher, v, null) != null ? RECENTS_LAUNCH_DURATION : APP_LAUNCH_DURATION; int statusBarTransitionDelay = duration - STATUS_BAR_TRANSITION_DURATION; return ActivityOptionsCompat.makeRemoteAnimation(new RemoteAnimationAdapterCompat( runner, duration, statusBarTransitionDelay)); } return super.getActivityLaunchOptions(launcher, v); } /** * Return the window bounds of the opening target. * In multiwindow mode, we need to get the final size of the opening app window target to help * figure out where the floating view should animate to. */ private Rect getWindowTargetBounds(RemoteAnimationTargetCompat[] targets) { Rect bounds = new Rect(0, 0, mDeviceProfile.widthPx, mDeviceProfile.heightPx); if (mLauncher.isInMultiWindowModeCompat()) { for (RemoteAnimationTargetCompat target : targets) { if (target.mode == MODE_OPENING) { bounds.set(target.sourceContainerBounds); bounds.offsetTo(target.position.x, target.position.y); return bounds; } } } return bounds; } public void setRemoteAnimationProvider(final RemoteAnimationProvider animationProvider, CancellationSignal cancellationSignal) { mRemoteAnimationProvider = animationProvider; cancellationSignal.setOnCancelListener(() -> { if (animationProvider == mRemoteAnimationProvider) { mRemoteAnimationProvider = null; } }); } /** * Composes the animations for a launch from the recents list if possible. */ private boolean composeRecentsLaunchAnimator(View v, RemoteAnimationTargetCompat[] targets, AnimatorSet target) { // Ensure recents is actually visible if (!mLauncher.getStateManager().getState().overviewUi) { return false; } RecentsView recentsView = mLauncher.getOverviewPanel(); boolean launcherClosing = launcherIsATargetWithMode(targets, MODE_CLOSING); boolean skipLauncherChanges = !launcherClosing; boolean isLaunchingFromQuickscrub = recentsView.getQuickScrubController().isWaitingForTaskLaunch(); TaskView taskView = findTaskViewToLaunch(mLauncher, v, targets); if (taskView == null) { return false; } int duration = isLaunchingFromQuickscrub ? RECENTS_QUICKSCRUB_LAUNCH_DURATION : RECENTS_LAUNCH_DURATION; ClipAnimationHelper helper = new ClipAnimationHelper(); target.play(getRecentsWindowAnimator(taskView, skipLauncherChanges, targets, helper) .setDuration(duration)); Animator childStateAnimation = null; // Found a visible recents task that matches the opening app, lets launch the app from there Animator launcherAnim; final AnimatorListenerAdapter windowAnimEndListener; if (launcherClosing) { launcherAnim = recentsView.createAdjacentPageAnimForTaskLaunch(taskView, helper); launcherAnim.setInterpolator(Interpolators.TOUCH_RESPONSE_INTERPOLATOR); launcherAnim.setDuration(duration); // Make sure recents gets fixed up by resetting task alphas and scales, etc. windowAnimEndListener = new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mLauncher.getStateManager().moveToRestState(); mLauncher.getStateManager().reapplyState(); } }; } else { AnimatorPlaybackController controller = mLauncher.getStateManager().createAnimationToNewWorkspace(NORMAL, duration); controller.dispatchOnStart(); childStateAnimation = controller.getTarget(); launcherAnim = controller.getAnimationPlayer().setDuration(duration); windowAnimEndListener = new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mLauncher.getStateManager().goToState(NORMAL, false); } }; } target.play(launcherAnim); // Set the current animation first, before adding windowAnimEndListener. Setting current // animation adds some listeners which need to be called before windowAnimEndListener // (the ordering of listeners matter in this case). mLauncher.getStateManager().setCurrentAnimation(target, childStateAnimation); target.addListener(windowAnimEndListener); return true; } /** * Content is everything on screen except the background and the floating view (if any). * * @param isAppOpening True when this is called when an app is opening. * False when this is called when an app is closing. */ private Pair getLauncherContentAnimator(boolean isAppOpening) { AnimatorSet launcherAnimator = new AnimatorSet(); Runnable endListener; float[] alphas = isAppOpening ? new float[] {1, 0} : new float[] {0, 1}; float[] trans = isAppOpening ? new float[] {0, mContentTransY} : new float[] {-mContentTransY, 0}; if (mLauncher.isInState(ALL_APPS)) { // All Apps in portrait mode is full screen, so we only animate AllAppsContainerView. final View appsView = mLauncher.getAppsView(); final float startAlpha = appsView.getAlpha(); final float startY = appsView.getTranslationY(); appsView.setAlpha(alphas[0]); appsView.setTranslationY(trans[0]); ObjectAnimator alpha = ObjectAnimator.ofFloat(appsView, View.ALPHA, alphas); alpha.setDuration(217); alpha.setInterpolator(LINEAR); appsView.setLayerType(View.LAYER_TYPE_HARDWARE, null); alpha.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { appsView.setLayerType(View.LAYER_TYPE_NONE, null); } }); ObjectAnimator transY = ObjectAnimator.ofFloat(appsView, View.TRANSLATION_Y, trans); transY.setInterpolator(AGGRESSIVE_EASE); transY.setDuration(350); launcherAnimator.play(alpha); launcherAnimator.play(transY); endListener = () -> { appsView.setAlpha(startAlpha); appsView.setTranslationY(startY); appsView.setLayerType(View.LAYER_TYPE_NONE, null); }; } else if (mLauncher.isInState(OVERVIEW)) { AllAppsTransitionController allAppsController = mLauncher.getAllAppsController(); launcherAnimator.play(ObjectAnimator.ofFloat(allAppsController, ALL_APPS_PROGRESS, allAppsController.getProgress(), ALL_APPS_PROGRESS_OFF_SCREEN)); View overview = mLauncher.getOverviewPanelContainer(); ObjectAnimator alpha = ObjectAnimator.ofFloat(overview, View.ALPHA, alphas); alpha.setDuration(217); alpha.setInterpolator(LINEAR); launcherAnimator.play(alpha); ObjectAnimator transY = ObjectAnimator.ofFloat(overview, View.TRANSLATION_Y, trans); transY.setInterpolator(AGGRESSIVE_EASE); transY.setDuration(350); launcherAnimator.play(transY); overview.setLayerType(View.LAYER_TYPE_HARDWARE, null); endListener = () -> { overview.setLayerType(View.LAYER_TYPE_NONE, null); overview.setAlpha(1f); overview.setTranslationY(0f); mLauncher.getStateManager().reapplyState(); }; } else { mDragLayerAlpha.setValue(alphas[0]); ObjectAnimator alpha = ObjectAnimator.ofFloat(mDragLayerAlpha, MultiValueAlpha.VALUE, alphas); alpha.setDuration(217); alpha.setInterpolator(LINEAR); launcherAnimator.play(alpha); mDragLayer.setTranslationY(trans[0]); ObjectAnimator transY = ObjectAnimator.ofFloat(mDragLayer, View.TRANSLATION_Y, trans); transY.setInterpolator(AGGRESSIVE_EASE); transY.setDuration(350); launcherAnimator.play(transY); mDragLayer.getScrim().hideSysUiScrim(true); // Pause page indicator animations as they lead to layer trashing. mLauncher.getWorkspace().getPageIndicator().pauseAnimations(); mDragLayer.setLayerType(View.LAYER_TYPE_HARDWARE, null); endListener = this::resetContentView; } return new Pair<>(launcherAnimator, endListener); } /** * @return Animator that controls the icon used to launch the target. */ private AnimatorSet getIconAnimator(View v, Rect windowTargetBounds) { 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 fromDeepShortcutView = v.getParent() instanceof DeepShortcutView; if (fromDeepShortcutView) { // Deep shortcut views have their icon drawn in a separate view. DeepShortcutView view = (DeepShortcutView) v.getParent(); mDragLayer.getDescendantRectRelativeToSelf(view.getIconView(), rect); } else { mDragLayer.getDescendantRectRelativeToSelf(v, rect); } int viewLocationLeft = rect.left; int viewLocationTop = rect.top; float startScale = 1f; if (isBubbleTextView && !fromDeepShortcutView) { 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()); } viewLocationLeft += rect.left; viewLocationTop += rect.top; int viewLocationStart = mIsRtl ? windowTargetBounds.width() - rect.right : viewLocationLeft; LayoutParams lp = new LayoutParams(rect.width(), rect.height()); lp.ignoreInsets = true; lp.setMarginStart(viewLocationStart); lp.topMargin = viewLocationTop; mFloatingView.setLayoutParams(lp); // Set the properties here already to make sure they'are available when running the first // animation frame. mFloatingView.setLeft(viewLocationLeft); mFloatingView.setTop(viewLocationTop); mFloatingView.setRight(viewLocationLeft + rect.width()); mFloatingView.setBottom(viewLocationTop + rect.height()); // Swap the two views in place. ((ViewGroup) mDragLayer.getParent()).addView(mFloatingView); v.setVisibility(View.INVISIBLE); AnimatorSet appIconAnimatorSet = new AnimatorSet(); 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 / 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); // 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); 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 = windowTargetBounds.width() / (float) rect.width(); float maxScaleY = windowTargetBounds.height() / (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); 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); appIconAnimatorSet.play(alpha); appIconAnimatorSet.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { // Reset launcher to normal state v.setVisibility(View.VISIBLE); ((ViewGroup) mDragLayer.getParent()).removeView(mFloatingView); } }); return appIconAnimatorSet; } /** * @return Animator that controls the window of the opening targets. */ private ValueAnimator getOpeningWindowAnimators(View v, RemoteAnimationTargetCompat[] targets, Rect windowTargetBounds) { 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]; Rect crop = new Rect(); Matrix matrix = new Matrix(); ValueAnimator appAnimator = ValueAnimator.ofFloat(0, 1); appAnimator.setDuration(APP_LAUNCH_DURATION); appAnimator.addUpdateListener(new MultiValueUpdateListener() { // Fade alpha for the app window. FloatProp mAlpha = new FloatProp(0f, 1f, 0, 60, LINEAR); boolean isFirstFrame = true; @Override public void onUpdate(float percent) { 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 easePercent = 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 / windowTargetBounds.width(); float scaleY = iconHeight / windowTargetBounds.height(); float scale = Math.min(1f, Math.min(scaleX, scaleY)); matrix.setScale(scale, scale); // 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; matrix.postTranslate(transX0, transY0); // 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); TransactionCompat t = new TransactionCompat(); if (isFirstFrame) { RemoteAnimationProvider.prepareTargetsForFirstFrame(targets, t, MODE_OPENING); isFirstFrame = false; } for (RemoteAnimationTargetCompat target : targets) { if (target.mode == MODE_OPENING) { t.setAlpha(target.leash, mAlpha.value); t.setMatrix(target.leash, matrix); t.setWindowCrop(target.leash, crop); t.deferTransactionUntil(target.leash, surface, getNextFrameNumber(surface)); } } t.setEarlyWakeup(); t.apply(); matrix.reset(); } }); return appAnimator; } /** * Registers remote animations used when closing apps to home screen. */ private void registerRemoteAnimations() { // Unregister this if (hasControlRemoteAppTransitionPermission()) { RemoteAnimationDefinitionCompat definition = new RemoteAnimationDefinitionCompat(); definition.addRemoteAnimation(WindowManagerWrapper.TRANSIT_WALLPAPER_OPEN, WindowManagerWrapper.ACTIVITY_TYPE_STANDARD, new RemoteAnimationAdapterCompat(getWallpaperOpenRunner(), CLOSING_TRANSITION_DURATION_MS, 0 /* statusBarTransitionDelay */)); // TODO: Transition for unlock to home TRANSIT_KEYGUARD_GOING_AWAY_ON_WALLPAPER new ActivityCompat(mLauncher).registerRemoteAnimations(definition); } } private boolean launcherIsATargetWithMode(RemoteAnimationTargetCompat[] targets, int mode) { return taskIsATargetWithMode(targets, mLauncher.getTaskId(), mode); } /** * @return Runner that plays when user goes to Launcher * ie. pressing home, swiping up from nav bar. */ private RemoteAnimationRunnerCompat getWallpaperOpenRunner() { return new LauncherAnimationRunner(mHandler, false /* startAtFrontOfQueue */) { @Override public void onCreateAnimation(RemoteAnimationTargetCompat[] targetCompats, AnimationResult result) { if (!mLauncher.hasBeenResumed()) { // If launcher is not resumed, wait until new async-frame after resume mLauncher.setOnResumeCallback(() -> postAsyncCallback(mHandler, () -> onCreateAnimation(targetCompats, result))); return; } AnimatorSet anim = null; RemoteAnimationProvider provider = mRemoteAnimationProvider; if (provider != null) { anim = provider.createWindowAnimation(targetCompats); } if (anim == null) { anim = new AnimatorSet(); anim.play(getClosingWindowAnimators(targetCompats)); // Normally, we run the launcher content animation when we are transitioning // home, but if home is already visible, then we don't want to animate the // contents of launcher unless we know that we are animating home as a result // of the home button press with quickstep, which will result in launcher being // started on touch down, prior to the animation home (and won't be in the // targets list because it is already visible). In that case, we force // invisibility on touch down, and only reset it after the animation to home // is initialized. if (launcherIsATargetWithMode(targetCompats, MODE_OPENING) || mLauncher.isForceInvisible()) { // Only register the content animation for cancellation when state changes mLauncher.getStateManager().setCurrentAnimation(anim); createLauncherResumeAnimation(anim); } } mLauncher.clearForceInvisibleFlag(INVISIBLE_ALL); result.setAnimation(anim); } }; } /** * Animator that controls the transformations of the windows the targets that are closing. */ private Animator getClosingWindowAnimators(RemoteAnimationTargetCompat[] targets) { Matrix matrix = new Matrix(); ValueAnimator closingAnimator = ValueAnimator.ofFloat(0, 1); int duration = CLOSING_TRANSITION_DURATION_MS; closingAnimator.setDuration(duration); closingAnimator.addUpdateListener(new MultiValueUpdateListener() { FloatProp mDy = new FloatProp(0, mClosingWindowTransY, 0, duration, DEACCEL_1_7); FloatProp mScale = new FloatProp(1f, 1f, 0, duration, DEACCEL_1_7); FloatProp mAlpha = new FloatProp(1f, 0f, 25, 125, LINEAR); boolean isFirstFrame = true; @Override public void onUpdate(float percent) { TransactionCompat t = new TransactionCompat(); if (isFirstFrame) { RemoteAnimationProvider.prepareTargetsForFirstFrame(targets, t, MODE_CLOSING); isFirstFrame = false; } for (RemoteAnimationTargetCompat app : targets) { if (app.mode == RemoteAnimationTargetCompat.MODE_CLOSING) { t.setAlpha(app.leash, mAlpha.value); matrix.setScale(mScale.value, mScale.value, app.sourceContainerBounds.centerX(), app.sourceContainerBounds.centerY()); matrix.postTranslate(0, mDy.value); matrix.postTranslate(app.position.x, app.position.y); t.setMatrix(app.leash, matrix); } } t.setEarlyWakeup(); t.apply(); matrix.reset(); } }); return closingAnimator; } /** * Creates an animator that modifies Launcher as a result from {@link #getWallpaperOpenRunner}. */ private void createLauncherResumeAnimation(AnimatorSet anim) { if (mLauncher.isInState(LauncherState.ALL_APPS)) { Pair contentAnimator = getLauncherContentAnimator(false /* isAppOpening */); contentAnimator.first.setStartDelay(LAUNCHER_RESUME_START_DELAY); anim.play(contentAnimator.first); anim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { contentAnimator.second.run(); } }); } else { AnimatorSet workspaceAnimator = new AnimatorSet(); mDragLayer.setTranslationY(-mWorkspaceTransY);; workspaceAnimator.play(ObjectAnimator.ofFloat(mDragLayer, View.TRANSLATION_Y, -mWorkspaceTransY, 0)); mDragLayerAlpha.setValue(0); workspaceAnimator.play(ObjectAnimator.ofFloat( mDragLayerAlpha, MultiValueAlpha.VALUE, 0, 1f)); workspaceAnimator.setStartDelay(LAUNCHER_RESUME_START_DELAY); workspaceAnimator.setDuration(333); workspaceAnimator.setInterpolator(Interpolators.DEACCEL_1_7); mDragLayer.getScrim().hideSysUiScrim(true); // Pause page indicator animations as they lead to layer trashing. mLauncher.getWorkspace().getPageIndicator().pauseAnimations(); mDragLayer.setLayerType(View.LAYER_TYPE_HARDWARE, null); workspaceAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { resetContentView(); } }); anim.play(workspaceAnimator); } } private void resetContentView() { mLauncher.getWorkspace().getPageIndicator().skipAnimationsToEnd(); mDragLayerAlpha.setValue(1f); mDragLayer.setLayerType(View.LAYER_TYPE_NONE, null); mDragLayer.setTranslationY(0f); mDragLayer.getScrim().hideSysUiScrim(false); } private boolean hasControlRemoteAppTransitionPermission() { return mLauncher.checkSelfPermission(CONTROL_REMOTE_APP_TRANSITION_PERMISSION) == PackageManager.PERMISSION_GRANTED; } }