Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 3e803c7a authored by Winson Chung's avatar Winson Chung
Browse files

Animate the visible task view if launching an app that resolves to the task

Test: Manual, launch app for associated visible task
Change-Id: I7a56553197ad23e1269eb50523eca0ea88898f47
parent 7780fa7b
Loading
Loading
Loading
Loading
+146 −3
Original line number Diff line number Diff line
@@ -47,6 +47,10 @@ import com.android.launcher3.allapps.AllAppsTransitionController;
import com.android.launcher3.anim.Interpolators;
import com.android.launcher3.dragndrop.DragLayer;
import com.android.launcher3.graphics.DrawableFactory;
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.system.ActivityCompat;
import com.android.systemui.shared.system.ActivityOptionsCompat;
import com.android.systemui.shared.system.RemoteAnimationAdapterCompat;
@@ -70,6 +74,7 @@ public class LauncherAppTransitionManagerImpl extends LauncherAppTransitionManag
    private static final String CONTROL_REMOTE_APP_TRANSITION_PERMISSION =
            "android.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS";

    private static final int RECENTS_LAUNCH_DURATION = 336;
    private static final int LAUNCHER_RESUME_START_DELAY = 150;
    private static final int CLOSING_TRANSITION_DURATION_MS = 350;

@@ -139,8 +144,18 @@ public class LauncherAppTransitionManagerImpl extends LauncherAppTransitionManag
                        // Post at front of queue ignoring sync barriers to make sure it gets
                        // processed before the next frame.
                        postAtFrontOfQueueAsynchronously(v.getHandler(), () -> {
                            LauncherTransitionAnimator animator = new LauncherTransitionAnimator(
                                    getLauncherAnimators(v), getWindowAnimators(v, targets));
                            final boolean removeTrackingView;
                            LauncherTransitionAnimator animator =
                                    composeRecentsLaunchAnimator(v, 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() {
@@ -148,7 +163,10 @@ public class LauncherAppTransitionManagerImpl extends LauncherAppTransitionManag
                                public void onAnimationEnd(Animator animation) {
                                    // Reset launcher to normal state
                                    v.setVisibility(View.VISIBLE);
                                    ((ViewGroup) mDragLayer.getParent()).removeView(mFloatingView);
                                    if (removeTrackingView) {
                                        ((ViewGroup) mDragLayer.getParent()).removeView(
                                                mFloatingView);
                                    }

                                    mDragLayer.setAlpha(1f);
                                    mDragLayer.setTranslationY(0f);
@@ -178,6 +196,131 @@ public class LauncherAppTransitionManagerImpl extends LauncherAppTransitionManag
        return getDefaultActivityLaunchOptions(launcher, v);
    }

    /**
     * Composes the animations for a launch from the recents list if possible.
     */
    private LauncherTransitionAnimator composeRecentsLaunchAnimator(View v,
            RemoteAnimationTargetCompat[] targets) {
        // Ensure recents is actually visible
        if (!mLauncher.isInState(LauncherState.OVERVIEW)) {
            return null;
        }

        // Resolve the opening task id
        int openingTaskId = -1;
        for (RemoteAnimationTargetCompat target : targets) {
            if (target.mode == RemoteAnimationTargetCompat.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
        RecentsView recentsView = mLauncher.getOverviewPanel();
        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
        return new LauncherTransitionAnimator(null, getRecentsWindowAnimator(taskView, targets));
    }

    /**
     * @return Animator that controls the window of the opening targets for the recents launch
     * animation.
     */
    private ValueAnimator getRecentsWindowAnimator(TaskView v,
            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));

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

                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);
                        t.deferTransactionUntil(target.leash, surface, getNextFrameNumber(surface));
                    }
                    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),
                getWindowAnimators(v, targets));
    }

    /**
     * @return Animators that control the movements of the Launcher and icon of the opening target.
     */
+9 −3
Original line number Diff line number Diff line
@@ -32,11 +32,15 @@ public class LauncherTransitionAnimator {
    private Animator mWindowAnimator;

    LauncherTransitionAnimator(Animator launcherAnimator, Animator windowAnimator) {
        if (launcherAnimator != null) {
            mLauncherAnimator = launcherAnimator;
        }
        mWindowAnimator = windowAnimator;

        mAnimatorSet = new AnimatorSet();
        if (launcherAnimator != null) {
            mAnimatorSet.play(launcherAnimator);
        }
        mAnimatorSet.play(windowAnimator);
    }

@@ -53,6 +57,8 @@ public class LauncherTransitionAnimator {
    }

    public void finishLauncherAnimation() {
        if (mLauncherAnimator != null) {
            mLauncherAnimator.end();
        }
    }
}
+111 −0
Original line number Diff line number Diff line
/*
 * 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.quickstep;

import android.graphics.Rect;

import com.android.launcher3.Utilities;

/**
 * Helper class to interpolate the animation between a task view representation and an actual
 * window.
 */
public class RecentsAnimationInterpolator {

    public static class TaskWindowBounds {
        public float taskScale = 1f;
        public float taskX = 0f;
        public float taskY = 0f;

        public float winScale = 1f;
        public float winX = 0f;
        public float winY = 0f;
        public Rect winCrop = new Rect();

        @Override
        public String toString() {
            return "taskScale=" + taskScale + " taskX=" + taskX + " taskY=" + taskY
                    + " winScale=" + winScale + " winX=" + winX + " winY=" + winY
                    + " winCrop=" + winCrop;
        }
    }

    private TaskWindowBounds mTmpTaskWindowBounds = new TaskWindowBounds();
    private Rect mTmpInsets = new Rect();

    private Rect mWindow;
    private Rect mInsetWindow;
    private Rect mInsets;
    private Rect mTask;
    private Rect mTaskInsets;
    private Rect mThumbnail;

    private float mTaskScale;
    private Rect mScaledTask;
    private Rect mTargetTask;
    private Rect mSrcWindow;

    public RecentsAnimationInterpolator(Rect window, Rect insets, Rect task, Rect taskInsets) {
        mWindow = window;
        mInsets = insets;
        mTask = task;
        mTaskInsets = taskInsets;
        mInsetWindow = new Rect(window);
        Utilities.insetRect(mInsetWindow, insets);

        mThumbnail = new Rect(task);
        Utilities.insetRect(mThumbnail, taskInsets);
        mTaskScale = (float) mInsetWindow.width() / mThumbnail.width();
        mScaledTask = new Rect(task);
        Utilities.scaleRectAboutCenter(mScaledTask, mTaskScale);
        Rect finalScaledTaskInsets = new Rect(taskInsets);
        Utilities.scaleRect(finalScaledTaskInsets, mTaskScale);
        mTargetTask = new Rect(mInsetWindow);
        mTargetTask.offsetTo(window.top + insets.top - finalScaledTaskInsets.top,
                window.left + insets.left - finalScaledTaskInsets.left);

        float initialWinScale = 1f / mTaskScale;
        Rect scaledWindow = new Rect(mInsetWindow);
        Utilities.scaleRectAboutCenter(scaledWindow, initialWinScale);
        Rect scaledInsets = new Rect(insets);
        Utilities.scaleRect(scaledInsets, initialWinScale);
        mSrcWindow = new Rect(scaledWindow);
        mSrcWindow.offsetTo(mThumbnail.left - scaledInsets.left,
                mThumbnail.top - scaledInsets.top);
    }

    public TaskWindowBounds interpolate(float t) {
        mTmpTaskWindowBounds.taskScale = Utilities.mapRange(t,
                1, (float) mInsetWindow.width() / mThumbnail.width());
        mTmpTaskWindowBounds.taskX = Utilities.mapRange(t,
                0, mTargetTask.left - mScaledTask.left);
        mTmpTaskWindowBounds.taskY = Utilities.mapRange(t,
                0, mTargetTask.top - mScaledTask.top);

        mTmpTaskWindowBounds.winScale = mTmpTaskWindowBounds.taskScale / mTaskScale;
        mTmpTaskWindowBounds.winX = Utilities.mapRange(t,
                mSrcWindow.left, 0);
        mTmpTaskWindowBounds.winY = Utilities.mapRange(t,
                mSrcWindow.top, 0);

        mTmpInsets.set(mInsets);
        Utilities.scaleRect(mTmpInsets, (1f - t));
        mTmpTaskWindowBounds.winCrop.set(mWindow);
        Utilities.insetRect(mTmpTaskWindowBounds.winCrop, mTmpInsets);

        return mTmpTaskWindowBounds;
    }
}
+21 −1
Original line number Diff line number Diff line
@@ -215,6 +215,21 @@ public class RecentsView extends PagedView implements Insettable {
        return mFirstTaskIndex;
    }

    public boolean isTaskViewVisible(TaskView tv) {
        // For now, just check if it's the active task
        return indexOfChild(tv) == getNextPage();
    }

    public TaskView getTaskView(int taskId) {
        for (int i = getFirstTaskIndex(); i < getChildCount(); i++) {
            TaskView tv = (TaskView) getChildAt(i);
            if (tv.getTask().key.id == taskId) {
                return tv;
            }
        }
        return null;
    }

    public void setStateController(RecentsViewStateController stateController) {
        mStateController = stateController;
    }
@@ -254,11 +269,16 @@ public class RecentsView extends PagedView implements Insettable {
        }
        setLayoutTransition(mLayoutTransition);

        // Rebind all task views
        // Rebind and reset all task views
        for (int i = tasks.size() - 1; i >= 0; i--) {
            final Task task = tasks.get(i);
            final TaskView taskView = (TaskView) getChildAt(tasks.size() - i - 1 + mFirstTaskIndex);
            taskView.bind(task);
            taskView.setScaleX(1f);
            taskView.setScaleY(1f);
            taskView.setTranslationX(0f);
            taskView.setTranslationY(0f);
            taskView.setAlpha(1f);
            loader.loadTaskData(task);
        }
    }
+8 −0
Original line number Diff line number Diff line
@@ -28,6 +28,7 @@ import android.graphics.LinearGradient;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PorterDuff.Mode;
import android.graphics.Rect;
import android.graphics.Shader;
import android.util.AttributeSet;
import android.view.View;
@@ -108,6 +109,13 @@ public class TaskThumbnailView extends View {
        updateThumbnailPaintFilter();
    }

    public Rect getInsets() {
        if (mThumbnailData != null) {
            return mThumbnailData.insets;
        }
        return new Rect();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawRoundRect(0, 0, getMeasuredWidth(), getMeasuredHeight(),
Loading