diff options
author | Winson Chung <winsonc@google.com> | 2018-01-29 11:16:32 -0800 |
---|---|---|
committer | Winson Chung <winsonc@google.com> | 2018-02-09 12:40:10 -0800 |
commit | 3e803c7a4bec4d14006057d918fe8663c7d36769 (patch) | |
tree | 722cfb15c4bf00dfd45f492d662f8b848ff3df5d | |
parent | 7780fa7bc711afbf89fbdc6fe94b473f2e3db3f2 (diff) | |
download | android_packages_apps_Trebuchet-3e803c7a4bec4d14006057d918fe8663c7d36769.tar.gz android_packages_apps_Trebuchet-3e803c7a4bec4d14006057d918fe8663c7d36769.tar.bz2 android_packages_apps_Trebuchet-3e803c7a4bec4d14006057d918fe8663c7d36769.zip |
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
7 files changed, 315 insertions, 9 deletions
diff --git a/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java b/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java index 11bc88348..2f0cd7833 100644 --- a/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java +++ b/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java @@ -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); @@ -179,6 +197,131 @@ public class LauncherAppTransitionManagerImpl extends LauncherAppTransitionManag } /** + * 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. */ private AnimatorSet getLauncherAnimators(View v) { diff --git a/quickstep/src/com/android/launcher3/LauncherTransitionAnimator.java b/quickstep/src/com/android/launcher3/LauncherTransitionAnimator.java index 80eaef7e3..aec28697e 100644 --- a/quickstep/src/com/android/launcher3/LauncherTransitionAnimator.java +++ b/quickstep/src/com/android/launcher3/LauncherTransitionAnimator.java @@ -32,11 +32,15 @@ public class LauncherTransitionAnimator { private Animator mWindowAnimator; LauncherTransitionAnimator(Animator launcherAnimator, Animator windowAnimator) { - mLauncherAnimator = launcherAnimator; + if (launcherAnimator != null) { + mLauncherAnimator = launcherAnimator; + } mWindowAnimator = windowAnimator; mAnimatorSet = new AnimatorSet(); - mAnimatorSet.play(launcherAnimator); + if (launcherAnimator != null) { + mAnimatorSet.play(launcherAnimator); + } mAnimatorSet.play(windowAnimator); } @@ -53,6 +57,8 @@ public class LauncherTransitionAnimator { } public void finishLauncherAnimation() { - mLauncherAnimator.end(); + if (mLauncherAnimator != null) { + mLauncherAnimator.end(); + } } } diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationInterpolator.java b/quickstep/src/com/android/quickstep/RecentsAnimationInterpolator.java new file mode 100644 index 000000000..9cc038f0e --- /dev/null +++ b/quickstep/src/com/android/quickstep/RecentsAnimationInterpolator.java @@ -0,0 +1,111 @@ +/* + * 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; + } +} diff --git a/quickstep/src/com/android/quickstep/RecentsView.java b/quickstep/src/com/android/quickstep/RecentsView.java index 8e03f37bf..26fe54eaa 100644 --- a/quickstep/src/com/android/quickstep/RecentsView.java +++ b/quickstep/src/com/android/quickstep/RecentsView.java @@ -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); } } diff --git a/quickstep/src/com/android/quickstep/TaskThumbnailView.java b/quickstep/src/com/android/quickstep/TaskThumbnailView.java index 36a0601c8..4f93b1c83 100644 --- a/quickstep/src/com/android/quickstep/TaskThumbnailView.java +++ b/quickstep/src/com/android/quickstep/TaskThumbnailView.java @@ -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(), diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java index 158c540ac..d559b44be 100644 --- a/src/com/android/launcher3/Utilities.java +++ b/src/com/android/launcher3/Utilities.java @@ -237,16 +237,27 @@ public final class Utilities { int cx = r.centerX(); int cy = r.centerY(); r.offset(-cx, -cy); + scaleRect(r, scale); + r.offset(cx, cy); + } + } + public static void scaleRect(Rect r, float scale) { + if (scale != 1.0f) { r.left = (int) (r.left * scale + 0.5f); r.top = (int) (r.top * scale + 0.5f); r.right = (int) (r.right * scale + 0.5f); r.bottom = (int) (r.bottom * scale + 0.5f); - - r.offset(cx, cy); } } + public static void insetRect(Rect r, Rect insets) { + r.left = Math.min(r.right, r.left + insets.left); + r.top = Math.min(r.bottom, r.top + insets.top); + r.right = Math.max(r.left, r.right - insets.right); + r.bottom = Math.max(r.top, r.bottom - insets.bottom); + } + public static float shrinkRect(Rect r, float scaleX, float scaleY) { float scale = Math.min(Math.min(scaleX, scaleY), 1.0f); if (scale < 1.0f) { @@ -261,6 +272,10 @@ public final class Utilities { return scale; } + public static float mapRange(float value, float min, float max) { + return min + (value * (max - min)); + } + public static boolean isSystemApp(Context context, Intent intent) { PackageManager pm = context.getPackageManager(); ComponentName cn = intent.getComponent(); diff --git a/src/com/android/launcher3/anim/Interpolators.java b/src/com/android/launcher3/anim/Interpolators.java index 234365424..0dcebe38e 100644 --- a/src/com/android/launcher3/anim/Interpolators.java +++ b/src/com/android/launcher3/anim/Interpolators.java @@ -50,6 +50,9 @@ public class Interpolators { public static final Interpolator OVERSHOOT_0 = new OvershootInterpolator(0); + public static final Interpolator TOUCH_RESPONSE_INTERPOLATOR = + new PathInterpolator(0.3f, 0f, 0.1f, 1f); + /** * Inversion of zInterpolate, compounded with an ease-out. */ |