diff options
Diffstat (limited to 'quickstep/src/com/android')
53 files changed, 4512 insertions, 1688 deletions
diff --git a/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java b/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java index f91933979..e34631042 100644 --- a/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java +++ b/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java @@ -15,8 +15,9 @@ */ package com.android.launcher3; -import static com.android.systemui.shared.recents.utilities.Utilities - .postAtFrontOfQueueAsynchronously; +import static com.android.launcher3.Utilities.SINGLE_FRAME_MS; +import static com.android.launcher3.Utilities.postAsyncCallback; +import static com.android.systemui.shared.recents.utilities.Utilities.postAtFrontOfQueueAsynchronously; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -31,53 +32,49 @@ import com.android.systemui.shared.system.RemoteAnimationRunnerCompat; import com.android.systemui.shared.system.RemoteAnimationTargetCompat; @TargetApi(Build.VERSION_CODES.P) -public abstract class LauncherAnimationRunner extends AnimatorListenerAdapter - implements RemoteAnimationRunnerCompat { - - private static final int REFRESH_RATE_MS = 16; +public abstract class LauncherAnimationRunner implements RemoteAnimationRunnerCompat { private final Handler mHandler; + private final boolean mStartAtFrontOfQueue; + private AnimationResult mAnimationResult; - private Runnable mSysFinishRunnable; - - private AnimatorSet mAnimator; - - public LauncherAnimationRunner(Handler handler) { + /** + * @param startAtFrontOfQueue If true, the animation start will be posted at the front of the + * queue to minimize latency. + */ + public LauncherAnimationRunner(Handler handler, boolean startAtFrontOfQueue) { mHandler = handler; + mStartAtFrontOfQueue = startAtFrontOfQueue; } @BinderThread @Override public void onAnimationStart(RemoteAnimationTargetCompat[] targetCompats, Runnable runnable) { - postAtFrontOfQueueAsynchronously(mHandler, () -> { - // Finish any previous animation - finishSystemAnimation(); - - mSysFinishRunnable = runnable; - mAnimator = getAnimator(targetCompats); - if (mAnimator == null) { - finishSystemAnimation(); - return; - } - mAnimator.addListener(this); - 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); - - }); + Runnable r = () -> { + finishExistingAnimation(); + mAnimationResult = new AnimationResult(runnable); + onCreateAnimation(targetCompats, mAnimationResult); + }; + if (mStartAtFrontOfQueue) { + postAtFrontOfQueueAsynchronously(mHandler, r); + } else { + postAsyncCallback(mHandler, r); + } } - + /** + * Called on the UI thread when the animation targets are received. The implementation must + * call {@link AnimationResult#setAnimation(AnimatorSet)} with the target animation to be run. + */ @UiThread - public abstract AnimatorSet getAnimator(RemoteAnimationTargetCompat[] targetCompats); + public abstract void onCreateAnimation( + RemoteAnimationTargetCompat[] targetCompats, AnimationResult result); @UiThread - @Override - public void onAnimationEnd(Animator animation) { - if (animation == mAnimator) { - mAnimator = null; - finishSystemAnimation(); + private void finishExistingAnimation() { + if (mAnimationResult != null) { + mAnimationResult.finish(); + mAnimationResult = null; } } @@ -87,20 +84,56 @@ public abstract class LauncherAnimationRunner extends AnimatorListenerAdapter @BinderThread @Override public void onAnimationCancelled() { - postAtFrontOfQueueAsynchronously(mHandler, () -> { - if (mAnimator != null) { - mAnimator.removeListener(this); - mAnimator.end(); - mAnimator = null; - } - }); + postAsyncCallback(mHandler, this::finishExistingAnimation); } - @UiThread - private void finishSystemAnimation() { - if (mSysFinishRunnable != null) { - mSysFinishRunnable.run(); - mSysFinishRunnable = null; + public static final class AnimationResult { + + private final Runnable mFinishRunnable; + + private AnimatorSet mAnimator; + private boolean mFinished = false; + private boolean mInitialized = false; + + private AnimationResult(Runnable finishRunnable) { + mFinishRunnable = finishRunnable; + } + + @UiThread + private void finish() { + if (!mFinished) { + mFinishRunnable.run(); + mFinished = true; + } + } + + @UiThread + public void setAnimation(AnimatorSet animation) { + if (mInitialized) { + throw new IllegalStateException("Animation already initialized"); + } + mInitialized = true; + mAnimator = animation; + if (mAnimator == null) { + finish(); + } else if (mFinished) { + // Animation callback was already finished, skip the animation. + mAnimator.start(); + mAnimator.end(); + } else { + // Start the animation + mAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + finish(); + } + }); + 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(SINGLE_FRAME_MS); + } } } }
\ No newline at end of file diff --git a/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java b/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java index ad0b7344b..2e31ef239 100644 --- a/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java +++ b/quickstep/src/com/android/launcher3/LauncherAppTransitionManagerImpl.java @@ -16,9 +16,21 @@ 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; @@ -31,7 +43,6 @@ 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; @@ -42,10 +53,10 @@ import android.os.Build; 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 android.view.animation.Interpolator; import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener; import com.android.launcher3.InsettableFrameLayout.LayoutParams; @@ -55,12 +66,13 @@ 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.quickstep.RecentsAnimationInterpolator; -import com.android.quickstep.RecentsAnimationInterpolator.TaskWindowBounds; +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.recents.model.Task; import com.android.systemui.shared.system.ActivityCompat; import com.android.systemui.shared.system.ActivityOptionsCompat; import com.android.systemui.shared.system.RemoteAnimationAdapterCompat; @@ -79,54 +91,66 @@ public class LauncherAppTransitionManagerImpl extends LauncherAppTransitionManag implements OnDeviceProfileChangeListener { private static final String TAG = "LauncherTransition"; - private static final int STATUS_BAR_TRANSITION_DURATION = 120; + 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 = 233; - private static final int RECENTS_LAUNCH_DURATION = 336; + 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; private static final int LAUNCHER_RESUME_START_DELAY = 100; - private static final int CLOSING_TRANSITION_DURATION_MS = 350; + 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; - public static final float ALL_APPS_PROGRESS_OVERSHOOT = 0.99581414f; - private final DragLayer mDragLayer; 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 mReapplyStateListener = new AnimatorListenerAdapter() { + 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.getStateManager().reapplyState(); + 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(); @@ -144,102 +168,56 @@ public class LauncherAppTransitionManagerImpl extends LauncherAppTransitionManag @Override public ActivityOptions getActivityLaunchOptions(Launcher launcher, View v) { if (hasControlRemoteAppTransitionPermission()) { - try { - RemoteAnimationRunnerCompat runner = new LauncherAnimationRunner(mHandler) { + RemoteAnimationRunnerCompat runner = new LauncherAnimationRunner(mHandler, + true /* startAtFrontOfQueue */) { - @Override - public AnimatorSet getAnimator(RemoteAnimationTargetCompat[] targetCompats) { - AnimatorSet anim = new AnimatorSet(); + @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); + 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); - anim.play(getIconAnimator(v)); - if (launcherIsATargetWithMode(targetCompats, MODE_CLOSING)) { - anim.play(getLauncherContentAnimator(false /* show */)); - } - anim.play(getWindowAnimators(v, targetCompats)); + anim.play(getIconAnimator(v)); + if (launcherClosing) { + Pair<AnimatorSet, Runnable> launcherContentAnimator = + getLauncherContentAnimator(true /* isAppOpening */); + anim.play(launcherContentAnimator.first); + anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + launcherContentAnimator.second.run(); + } + }); } - return anim; + anim.play(getOpeningWindowAnimators(v, targetCompats)); } - }; - - 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)); - } 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); - } - public void setRemoteAnimationProvider(RemoteAnimationProvider animationProvider) { - mRemoteAnimationProvider = animationProvider; - } - - /** - * 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( - BaseDraggingActivity activity, View v, RemoteAnimationTargetCompat[] targets) { - if (v instanceof TaskView) { - return (TaskView) v; - } - RecentsView recentsView = activity.getOverviewPanel(); - - // 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. - if (v.getTag() instanceof ItemInfo) { - ItemInfo itemInfo = (ItemInfo) v.getTag(); - ComponentName componentName = itemInfo.getTargetComponent(); - if (componentName != null) { - 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; - } + if (launcherClosing) { + anim.addListener(mForceInvisibleListener); } - } - } - } - if (targets == null) { - return null; - } - // Resolve the opening task id - int openingTaskId = -1; - for (RemoteAnimationTargetCompat target : targets) { - if (target.mode == MODE_OPENING) { - openingTaskId = target.taskId; - break; - } - } + result.setAnimation(anim); + } + }; - // If there is no opening task id, fall back to the normal app icon launch animation - if (openingTaskId == -1) { - return null; + 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 getDefaultActivityLaunchOptions(launcher, v); + } - // 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; - } - return taskView; + public void setRemoteAnimationProvider(RemoteAnimationProvider animationProvider) { + mRemoteAnimationProvider = animationProvider; } /** @@ -261,21 +239,33 @@ public class LauncherAppTransitionManagerImpl extends LauncherAppTransitionManag return false; } + ClipAnimationHelper helper = new ClipAnimationHelper(); + target.play(getRecentsWindowAnimator(taskView, skipLauncherChanges, targets, helper) + .setDuration(RECENTS_LAUNCH_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); + launcherAnim = recentsView.createAdjacentPageAnimForTaskLaunch(taskView, helper); launcherAnim.setInterpolator(Interpolators.TOUCH_RESPONSE_INTERPOLATOR); launcherAnim.setDuration(RECENTS_LAUNCH_DURATION); // Make sure recents gets fixed up by resetting task alphas and scales, etc. - windowAnimEndListener = mReapplyStateListener; + windowAnimEndListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mLauncher.getStateManager().moveToRestState(); + mLauncher.getStateManager().reapplyState(); + } + }; } else { AnimatorPlaybackController controller = mLauncher.getStateManager() .createAnimationToNewWorkspace(NORMAL, RECENTS_LAUNCH_DURATION); controller.dispatchOnStart(); + childStateAnimation = controller.getTarget(); launcherAnim = controller.getAnimationPlayer().setDuration(RECENTS_LAUNCH_DURATION); windowAnimEndListener = new AnimatorListenerAdapter() { @Override @@ -284,112 +274,34 @@ public class LauncherAppTransitionManagerImpl extends LauncherAppTransitionManag } }; } - - target.play(getRecentsWindowAnimator(taskView, skipLauncherChanges, targets)); 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); + mLauncher.getStateManager().setCurrentAnimation(target, childStateAnimation); target.addListener(windowAnimEndListener); return true; } /** - * @return Animator that controls the window of the opening targets for the recents launch - * animation. - */ - private ValueAnimator getRecentsWindowAnimator(TaskView v, boolean skipLauncherChanges, - RemoteAnimationTargetCompat[] targets) { - final RecentsAnimationInterpolator recentsInterpolator = v.getRecentsInterpolator(); - - 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); - - float alphaDuration = 75; - if (!skipLauncherChanges) { - 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, alphaDuration, alphaDuration, - 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 alpha = getValue(0f, 1f, 0, 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) { - t.deferTransactionUntil(target.leash, surface, frameNumber); - } - } - if (isFirstFrame) { - t.show(target.leash); - } - } - t.setEarlyWakeup(); - t.apply(); - - matrix.reset(); - isFirstFrame = false; - } - }); - return appAnimator; - } - - /** * 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. + * @param isAppOpening True when this is called when an app is opening. + * False when this is called when an app is closing. */ - private AnimatorSet getLauncherContentAnimator(boolean show) { + private Pair<AnimatorSet, Runnable> getLauncherContentAnimator(boolean isAppOpening) { AnimatorSet launcherAnimator = new AnimatorSet(); + Runnable endListener; - float[] alphas = show - ? new float[] {0, 1} - : new float[] {1, 0}; - float[] trans = show - ? new float[] {mContentTransY, 0,} - : new float[] {0, mContentTransY}; + 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(LauncherState.ALL_APPS) && !mDeviceProfile.isVerticalBarLayout()) { + 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(); @@ -399,44 +311,72 @@ public class LauncherAppTransitionManagerImpl extends LauncherAppTransitionManag ObjectAnimator alpha = ObjectAnimator.ofFloat(appsView, View.ALPHA, alphas); alpha.setDuration(217); - alpha.setInterpolator(Interpolators.LINEAR); + 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(Interpolators.AGGRESSIVE_EASE); + transY.setInterpolator(AGGRESSIVE_EASE); transY.setDuration(350); launcherAnimator.play(alpha); launcherAnimator.play(transY); - launcherAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - appsView.setAlpha(startAlpha); - appsView.setTranslationY(startY); - } - }); + 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 { - mDragLayer.setAlpha(alphas[0]); + 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); - 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); - launcherAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - mDragLayer.setAlpha(1); - mDragLayer.setTranslationY(0); - } - }); + 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 launcherAnimator; + return new Pair<>(launcherAnimator, endListener); } /** @@ -511,12 +451,19 @@ public class LauncherAppTransitionManagerImpl extends LauncherAppTransitionManag 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); + // 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); @@ -527,14 +474,21 @@ public class LauncherAppTransitionManagerImpl extends LauncherAppTransitionManag float scale = Math.max(maxScaleX, maxScaleY); ObjectAnimator scaleAnim = ObjectAnimator .ofFloat(mFloatingView, SCALE_PROPERTY, startScale, scale); - scaleAnim.setDuration(APP_LAUNCH_DURATION).setInterpolator(Interpolators.EXAGGERATED_EASE); + 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); + 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() { @@ -551,7 +505,7 @@ public class LauncherAppTransitionManagerImpl extends LauncherAppTransitionManag /** * @return Animator that controls the window of the opening targets. */ - private ValueAnimator getWindowAnimators(View v, RemoteAnimationTargetCompat[] targets) { + private ValueAnimator getOpeningWindowAnimators(View v, RemoteAnimationTargetCompat[] targets) { Rect bounds = new Rect(); if (v.getParent() instanceof DeepShortcutView) { // Deep shortcut views have their icon drawn in a separate view. @@ -569,11 +523,13 @@ public class LauncherAppTransitionManagerImpl extends LauncherAppTransitionManag ValueAnimator appAnimator = ValueAnimator.ofFloat(0, 1); appAnimator.setDuration(APP_LAUNCH_DURATION); - appAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + 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 onAnimationUpdate(ValueAnimator animation) { + public void onUpdate(float percent) { final Surface surface = getSurface(mFloatingView); final long frameNumber = surface != null ? getNextFrameNumber(surface) : -1; if (frameNumber == -1) { @@ -581,8 +537,7 @@ public class LauncherAppTransitionManagerImpl extends LauncherAppTransitionManag Log.w(TAG, "Failed to animate, surface got destroyed."); return; } - final float percent = animation.getAnimatedFraction(); - final float easePercent = Interpolators.AGGRESSIVE_EASE.getInterpolation(percent); + final float easePercent = AGGRESSIVE_EASE.getInterpolation(percent); // Calculate app icon size. float iconWidth = bounds.width() * mFloatingView.getScaleX(); @@ -607,11 +562,6 @@ public class LauncherAppTransitionManagerImpl extends LauncherAppTransitionManag float transY0 = floatingViewBounds[1] - offsetY; matrix.postTranslate(transX0, transY0); - // Fade in the app window. - float alphaDuration = 60; - float alpha = getValue(0f, 1f, 0, 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); @@ -622,9 +572,13 @@ public class LauncherAppTransitionManagerImpl extends LauncherAppTransitionManag 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, alpha); + t.setAlpha(target.leash, mAlpha.value); // TODO: This isn't correct at the beginning of the animation, but better // than nothing. @@ -633,15 +587,11 @@ public class LauncherAppTransitionManagerImpl extends LauncherAppTransitionManag t.setWindowCrop(target.leash, crop); t.deferTransactionUntil(target.leash, surface, getNextFrameNumber(surface)); } - if (isFirstFrame) { - t.show(target.leash); - } } t.setEarlyWakeup(); t.apply(); matrix.reset(); - isFirstFrame = false; } }); return appAnimator; @@ -653,30 +603,19 @@ public class LauncherAppTransitionManagerImpl extends LauncherAppTransitionManag private void registerRemoteAnimations() { // Unregister this if (hasControlRemoteAppTransitionPermission()) { - try { - RemoteAnimationDefinitionCompat definition = new RemoteAnimationDefinitionCompat(); - definition.addRemoteAnimation(WindowManagerWrapper.TRANSIT_WALLPAPER_OPEN, - WindowManagerWrapper.ACTIVITY_TYPE_STANDARD, - 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 - } + 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) { - int launcherTaskId = mLauncher.getTaskId(); - for (RemoteAnimationTargetCompat target : targets) { - if (target.mode == mode && target.taskId == launcherTaskId) { - return true; - } - } - return false; + return taskIsATargetWithMode(targets, mLauncher.getTaskId(), mode); } /** @@ -684,9 +623,18 @@ public class LauncherAppTransitionManagerImpl extends LauncherAppTransitionManag * ie. pressing home, swiping up from nav bar. */ private RemoteAnimationRunnerCompat getWallpaperOpenRunner() { - return new LauncherAnimationRunner(mHandler) { + return new LauncherAnimationRunner(mHandler, false /* startAtFrontOfQueue */) { @Override - public AnimatorSet getAnimator(RemoteAnimationTargetCompat[] targetCompats) { + 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) { @@ -713,8 +661,8 @@ public class LauncherAppTransitionManagerImpl extends LauncherAppTransitionManag } } - mLauncher.setForceInvisible(false); - return anim; + mLauncher.clearForceInvisibleFlag(INVISIBLE_ALL); + result.setAnimation(anim); } }; } @@ -724,54 +672,41 @@ public class LauncherAppTransitionManagerImpl extends LauncherAppTransitionManag */ 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); + 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); - 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); - + 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, getValue(1f, 0f, 0, 350, currentPlayTime, - Interpolators.APP_CLOSE_ALPHA)); - matrix.setScale(scale, scale, + t.setAlpha(app.leash, mAlpha.value); + matrix.setScale(mScale.value, mScale.value, app.sourceContainerBounds.centerX(), app.sourceContainerBounds.centerY()); - matrix.postTranslate(dX, 0); + matrix.postTranslate(0, mDy.value); 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.setEarlyWakeup(); t.apply(); matrix.reset(); - isFirstFrame = false; } }); + return closingAnimator; } @@ -779,69 +714,58 @@ public class LauncherAppTransitionManagerImpl extends LauncherAppTransitionManag * Creates an animator that modifies Launcher as a result from {@link #getWallpaperOpenRunner}. */ private void createLauncherResumeAnimation(AnimatorSet anim) { - if (mLauncher.isInState(LauncherState.ALL_APPS) - || mLauncher.getDeviceProfile().isVerticalBarLayout()) { - AnimatorSet contentAnimator = getLauncherContentAnimator(true /* show */); - contentAnimator.setStartDelay(LAUNCHER_RESUME_START_DELAY); - anim.play(contentAnimator); + if (mLauncher.isInState(LauncherState.ALL_APPS)) { + Pair<AnimatorSet, Runnable> 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(); - mLauncher.getWorkspace().setTranslationY(mWorkspaceTransY); - workspaceAnimator.play(ObjectAnimator.ofFloat(mLauncher.getWorkspace(), - View.TRANSLATION_Y, mWorkspaceTransY, 0)); + mDragLayer.setTranslationY(-mWorkspaceTransY);; + workspaceAnimator.play(ObjectAnimator.ofFloat(mDragLayer, View.TRANSLATION_Y, + -mWorkspaceTransY, 0)); - View currentPage = ((CellLayout) mLauncher.getWorkspace() - .getChildAt(mLauncher.getWorkspace().getCurrentPage())) - .getShortcutsAndWidgets(); - currentPage.setAlpha(0f); - workspaceAnimator.play(ObjectAnimator.ofFloat(currentPage, View.ALPHA, 0, 1f)); + mDragLayerAlpha.setValue(0); + workspaceAnimator.play(ObjectAnimator.ofFloat( + mDragLayerAlpha, MultiValueAlpha.VALUE, 0, 1f)); workspaceAnimator.setStartDelay(LAUNCHER_RESUME_START_DELAY); workspaceAnimator.setDuration(333); - workspaceAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); + workspaceAnimator.setInterpolator(Interpolators.DEACCEL_1_7); - // 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); + 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); - anim.playSequentially(allAppsSlideIn, allAppsOvershoot); - anim.addListener(mReapplyStateListener); } } + 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; } - - /** - * 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); - } } diff --git a/quickstep/src/com/android/launcher3/LauncherInitListener.java b/quickstep/src/com/android/launcher3/LauncherInitListener.java index 27f169834..e5e377f83 100644 --- a/quickstep/src/com/android/launcher3/LauncherInitListener.java +++ b/quickstep/src/com/android/launcher3/LauncherInitListener.java @@ -24,6 +24,7 @@ import android.os.Handler; import com.android.launcher3.states.InternalStateHandler; import com.android.quickstep.ActivityControlHelper.ActivityInitListener; +import com.android.quickstep.OverviewCallbacks; import com.android.quickstep.util.RemoteAnimationProvider; import java.util.function.BiPredicate; @@ -60,6 +61,7 @@ public class LauncherInitListener extends InternalStateHandler implements Activi return null; }); } + OverviewCallbacks.get(launcher).onInitOverviewTransition(); return mOnInitListener.test(launcher, alreadyOnHome); } diff --git a/quickstep/src/com/android/launcher3/uioverrides/AllAppsState.java b/quickstep/src/com/android/launcher3/uioverrides/AllAppsState.java index d2f54874f..d86ba6aa6 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/AllAppsState.java +++ b/quickstep/src/com/android/launcher3/uioverrides/AllAppsState.java @@ -16,7 +16,6 @@ package com.android.launcher3.uioverrides; import static com.android.launcher3.LauncherAnimUtils.ALL_APPS_TRANSITION_MS; -import static com.android.launcher3.allapps.DiscoveryBounce.APPS_VIEW_SHOWN; import static com.android.launcher3.anim.Interpolators.DEACCEL_2; import android.view.View; @@ -32,7 +31,7 @@ import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; */ public class AllAppsState extends LauncherState { - private static final int STATE_FLAGS = FLAG_DISABLE_ACCESSIBILITY | FLAG_ALL_APPS_SCRIM; + private static final int STATE_FLAGS = FLAG_DISABLE_ACCESSIBILITY; private static final PageAlphaProvider PAGE_ALPHA_PROVIDER = new PageAlphaProvider(DEACCEL_2) { @Override @@ -47,10 +46,6 @@ public class AllAppsState extends LauncherState { @Override public void onStateEnabled(Launcher launcher) { - if (!launcher.getSharedPrefs().getBoolean(APPS_VIEW_SHOWN, false)) { - launcher.getSharedPrefs().edit().putBoolean(APPS_VIEW_SHOWN, true).apply(); - } - AbstractFloatingView.closeAllOpenViews(launcher); dispatchWindowStateChanged(launcher); } @@ -73,8 +68,10 @@ public class AllAppsState extends LauncherState { @Override public float[] getWorkspaceScaleAndTranslation(Launcher launcher) { - // TODO: interpolate - return LauncherState.OVERVIEW.getWorkspaceScaleAndTranslation(launcher); + float[] scaleAndTranslation = LauncherState.OVERVIEW.getWorkspaceScaleAndTranslation( + launcher); + scaleAndTranslation[0] = 1; + return scaleAndTranslation; } @Override @@ -89,7 +86,7 @@ public class AllAppsState extends LauncherState { @Override public float[] getOverviewScaleAndTranslationYFactor(Launcher launcher) { - return new float[] {1f, -0.2f}; + return new float[] {0.9f, -0.2f}; } @Override diff --git a/quickstep/src/com/android/launcher3/uioverrides/DisplayRotationListener.java b/quickstep/src/com/android/launcher3/uioverrides/DisplayRotationListener.java new file mode 100644 index 000000000..2d9a16147 --- /dev/null +++ b/quickstep/src/com/android/launcher3/uioverrides/DisplayRotationListener.java @@ -0,0 +1,48 @@ +/* + * 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.uioverrides; + +import android.content.Context; +import android.os.Handler; + +import com.android.systemui.shared.system.RotationWatcher; + +/** + * Utility class for listening for rotation changes + */ +public class DisplayRotationListener extends RotationWatcher { + + private final Runnable mCallback; + private Handler mHandler; + + public DisplayRotationListener(Context context, Runnable callback) { + super(context); + mCallback = callback; + } + + @Override + public void enable() { + if (mHandler == null) { + mHandler = new Handler(); + } + super.enable(); + } + + @Override + protected void onRotationChanged(int i) { + mHandler.post(mCallback); + } +} diff --git a/quickstep/src/com/android/launcher3/uioverrides/FastOverviewState.java b/quickstep/src/com/android/launcher3/uioverrides/FastOverviewState.java index f98f7a5ff..a11625a34 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/FastOverviewState.java +++ b/quickstep/src/com/android/launcher3/uioverrides/FastOverviewState.java @@ -15,7 +15,6 @@ */ package com.android.launcher3.uioverrides; -import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.Launcher; import com.android.quickstep.QuickScrubController; import com.android.quickstep.views.RecentsView; @@ -25,11 +24,11 @@ import com.android.quickstep.views.RecentsView; */ public class FastOverviewState extends OverviewState { - private static final int STATE_FLAGS = FLAG_SHOW_SCRIM | FLAG_DISABLE_RESTORE - | FLAG_DISABLE_INTERACTION | FLAG_OVERVIEW_UI | FLAG_HIDE_BACK_BUTTON; + private static final int STATE_FLAGS = FLAG_DISABLE_RESTORE | FLAG_DISABLE_INTERACTION + | FLAG_OVERVIEW_UI | FLAG_HIDE_BACK_BUTTON | FLAG_DISABLE_ACCESSIBILITY; public FastOverviewState(int id) { - super(id, QuickScrubController.QUICK_SCRUB_START_DURATION, STATE_FLAGS); + super(id, QuickScrubController.QUICK_SCRUB_FROM_HOME_START_DURATION, STATE_FLAGS); } @Override @@ -39,11 +38,6 @@ public class FastOverviewState extends OverviewState { recentsView.getQuickScrubController().onFinishedTransitionToQuickScrub(); } - public void onStateEnabled(Launcher launcher) { - super.onStateEnabled(launcher); - AbstractFloatingView.closeAllOpenViews(launcher); - } - @Override public int getVisibleElements(Launcher launcher) { return NONE; diff --git a/quickstep/src/com/android/launcher3/uioverrides/LandscapeEdgeSwipeController.java b/quickstep/src/com/android/launcher3/uioverrides/LandscapeEdgeSwipeController.java index 3622fc425..68773b418 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/LandscapeEdgeSwipeController.java +++ b/quickstep/src/com/android/launcher3/uioverrides/LandscapeEdgeSwipeController.java @@ -9,16 +9,20 @@ import android.view.MotionEvent; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherState; +import com.android.launcher3.LauncherStateManager.AnimationComponents; import com.android.launcher3.touch.AbstractStateChangeTouchController; import com.android.launcher3.touch.SwipeDetector; +import com.android.launcher3.userevent.nano.LauncherLogProto; import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction; -import com.android.quickstep.util.SysuiEventLogger; +import com.android.quickstep.RecentsModel; /** * Touch controller for handling edge swipes in landscape/seascape UI */ public class LandscapeEdgeSwipeController extends AbstractStateChangeTouchController { + private static final String TAG = "LandscapeEdgeSwipeCtrl"; + public LandscapeEdgeSwipeController(Launcher l) { super(l, SwipeDetector.HORIZONTAL); } @@ -36,27 +40,27 @@ public class LandscapeEdgeSwipeController extends AbstractStateChangeTouchContro } @Override - protected int getSwipeDirection(MotionEvent ev) { - return SwipeDetector.DIRECTION_BOTH; - } - - @Override protected LauncherState getTargetState(LauncherState fromState, boolean isDragTowardPositive) { boolean draggingFromNav = mLauncher.getDeviceProfile().isSeascape() != isDragTowardPositive; return draggingFromNav ? OVERVIEW : NORMAL; } @Override + protected int getLogContainerTypeForNormalState() { + return LauncherLogProto.ContainerType.NAVBAR; + } + + @Override protected float getShiftRange() { return mLauncher.getDragLayer().getWidth(); } @Override - protected float initCurrentAnimation() { + protected float initCurrentAnimation(@AnimationComponents int animComponent) { float range = getShiftRange(); long maxAccuracy = (long) (2 * range); - mCurrentAnimation = mLauncher.getStateManager() - .createAnimationToNewWorkspace(mToState, maxAccuracy); + mCurrentAnimation = mLauncher.getStateManager().createAnimationToNewWorkspace(mToState, + maxAccuracy, animComponent); return (mLauncher.getDeviceProfile().isSeascape() ? 2 : -2) / range; } @@ -69,7 +73,7 @@ public class LandscapeEdgeSwipeController extends AbstractStateChangeTouchContro protected void onSwipeInteractionCompleted(LauncherState targetState, int logAction) { super.onSwipeInteractionCompleted(targetState, logAction); if (mFromState == NORMAL && targetState == OVERVIEW) { - SysuiEventLogger.writeDummyRecentsTransition(0); + RecentsModel.getInstance(mLauncher).onOverviewShown(true, TAG); } } } diff --git a/quickstep/src/com/android/launcher3/uioverrides/OverviewState.java b/quickstep/src/com/android/launcher3/uioverrides/OverviewState.java index 9c7db3093..3a49294c5 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/OverviewState.java +++ b/quickstep/src/com/android/launcher3/uioverrides/OverviewState.java @@ -21,9 +21,12 @@ import static com.android.launcher3.states.RotationHelper.REQUEST_ROTATE; import android.view.View; +import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.DeviceProfile; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherState; +import com.android.launcher3.Workspace; +import com.android.launcher3.allapps.DiscoveryBounce; import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; import com.android.quickstep.views.RecentsView; @@ -32,8 +35,8 @@ import com.android.quickstep.views.RecentsView; */ public class OverviewState extends LauncherState { - private static final int STATE_FLAGS = FLAG_SHOW_SCRIM | FLAG_WORKSPACE_ICONS_CAN_BE_DRAGGED - | FLAG_DISABLE_RESTORE | FLAG_OVERVIEW_UI; + private static final int STATE_FLAGS = FLAG_WORKSPACE_ICONS_CAN_BE_DRAGGED + | FLAG_DISABLE_RESTORE | FLAG_OVERVIEW_UI | FLAG_DISABLE_ACCESSIBILITY; public OverviewState(int id) { this(id, OVERVIEW_TRANSITION_MS, STATE_FLAGS); @@ -45,8 +48,15 @@ public class OverviewState extends LauncherState { @Override public float[] getWorkspaceScaleAndTranslation(Launcher launcher) { - // TODO: provide a valid value - return new float[]{1, 0, -launcher.getDeviceProfile().hotseatBarSizePx / 2}; + RecentsView recentsView = launcher.getOverviewPanel(); + Workspace workspace = launcher.getWorkspace(); + View workspacePage = workspace.getPageAt(workspace.getCurrentPage()); + float workspacePageWidth = workspacePage != null && workspacePage.getWidth() != 0 + ? workspacePage.getWidth() : launcher.getDeviceProfile().availableWidthPx; + recentsView.getTaskSize(sTempRect); + float scale = (float) sTempRect.width() / workspacePageWidth; + float parallaxFactor = 0.5f; + return new float[]{scale, 0, -getDefaultSwipeHeight(launcher) * parallaxFactor}; } @Override @@ -58,6 +68,7 @@ public class OverviewState extends LauncherState { public void onStateEnabled(Launcher launcher) { RecentsView rv = launcher.getOverviewPanel(); rv.setOverviewStateEnabled(true); + AbstractFloatingView.closeAllOpenViews(launcher); } @Override @@ -69,6 +80,7 @@ public class OverviewState extends LauncherState { @Override public void onStateTransitionEnd(Launcher launcher) { launcher.getRotationHelper().setCurrentStateRequest(REQUEST_ROTATE); + DiscoveryBounce.showForOverviewIfNeeded(launcher); } @Override @@ -88,15 +100,20 @@ public class OverviewState extends LauncherState { @Override public int getVisibleElements(Launcher launcher) { if (launcher.getDeviceProfile().isVerticalBarLayout()) { - return DRAG_HANDLE_INDICATOR; + return 0; } else { - return HOTSEAT_SEARCH_BOX | DRAG_HANDLE_INDICATOR | + return HOTSEAT_SEARCH_BOX | (launcher.getAppsView().getFloatingHeaderView().hasVisibleContent() ? ALL_APPS_HEADER_EXTRA : HOTSEAT_ICONS); } } @Override + public float getWorkspaceScrimAlpha(Launcher launcher) { + return 0.5f; + } + + @Override public float getVerticalProgress(Launcher launcher) { if ((getVisibleElements(launcher) & ALL_APPS_HEADER_EXTRA) == 0) { // We have no all apps content, so we're still at the fully down progress. diff --git a/quickstep/src/com/android/launcher3/uioverrides/LandscapeStatesTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/OverviewToAllAppsTouchController.java index 30ceb43f9..0f9b57f03 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/LandscapeStatesTouchController.java +++ b/quickstep/src/com/android/launcher3/uioverrides/OverviewToAllAppsTouchController.java @@ -24,15 +24,18 @@ import android.view.MotionEvent; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherState; +import com.android.launcher3.userevent.nano.LauncherLogProto; import com.android.quickstep.TouchInteractionService; import com.android.quickstep.views.RecentsView; /** - * Touch controller from going from OVERVIEW to ALL_APPS + * Touch controller from going from OVERVIEW to ALL_APPS. + * + * This is used in landscape mode. It is also used in portrait mode for the fallback recents. */ -public class LandscapeStatesTouchController extends PortraitStatesTouchController { +public class OverviewToAllAppsTouchController extends PortraitStatesTouchController { - public LandscapeStatesTouchController(Launcher l) { + public OverviewToAllAppsTouchController(Launcher l) { super(l); } @@ -69,4 +72,9 @@ public class LandscapeStatesTouchController extends PortraitStatesTouchControlle } return fromState; } + + @Override + protected int getLogContainerTypeForNormalState() { + return LauncherLogProto.ContainerType.WORKSPACE; + } } diff --git a/quickstep/src/com/android/launcher3/uioverrides/PortraitStatesTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/PortraitStatesTouchController.java index 012b54521..987f952ba 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/PortraitStatesTouchController.java +++ b/quickstep/src/com/android/launcher3/uioverrides/PortraitStatesTouchController.java @@ -18,20 +18,20 @@ package com.android.launcher3.uioverrides; 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.anim.AnimatorSetBuilder.ANIM_OVERVIEW_TRANSLATION; import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_VERTICAL_PROGRESS; -import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN; import static com.android.launcher3.anim.Interpolators.LINEAR; import android.animation.TimeInterpolator; import android.animation.ValueAnimator; import android.view.MotionEvent; import android.view.animation.Interpolator; +import android.view.animation.OvershootInterpolator; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.DeviceProfile; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherState; +import com.android.launcher3.LauncherStateManager.AnimationComponents; import com.android.launcher3.anim.AnimatorPlaybackController; import com.android.launcher3.anim.AnimatorSetBuilder; import com.android.launcher3.anim.Interpolators; @@ -39,8 +39,8 @@ import com.android.launcher3.touch.AbstractStateChangeTouchController; import com.android.launcher3.touch.SwipeDetector; import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch; import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; +import com.android.quickstep.RecentsModel; import com.android.quickstep.TouchInteractionService; -import com.android.quickstep.util.SysuiEventLogger; import com.android.quickstep.views.RecentsView; import com.android.quickstep.views.TaskView; @@ -49,43 +49,13 @@ import com.android.quickstep.views.TaskView; */ public class PortraitStatesTouchController extends AbstractStateChangeTouchController { - private static final float TOTAL_DISTANCE_MULTIPLIER = 3f; - private static final float LINEAR_SCALE_LIMIT = 1 / TOTAL_DISTANCE_MULTIPLIER; - - // Must be greater than LINEAR_SCALE_LIMIT; - private static final float MAXIMUM_DISTANCE_FACTOR = 0.9f; - - // Maximum amount to overshoot. - private static final float MAX_OVERSHOOT = 0.3f; - - private static final double PI_BY_2 = Math.PI / 2; + private static final String TAG = "PortraitStatesTouchCtrl"; private InterpolatorWrapper mAllAppsInterpolatorWrapper = new InterpolatorWrapper(); // If true, we will finish the current animation instantly on second touch. private boolean mFinishFastOnSecondTouch; - private final Interpolator mAllAppsDampedInterpolator = new Interpolator() { - - private final double mAngleMultiplier = Math.PI / - (2 * (MAXIMUM_DISTANCE_FACTOR - LINEAR_SCALE_LIMIT)); - - @Override - public float getInterpolation(float v) { - if (v <= LINEAR_SCALE_LIMIT) { - return v * TOTAL_DISTANCE_MULTIPLIER; - } - float overshoot = (v - LINEAR_SCALE_LIMIT); - return (float) (1 + MAX_OVERSHOOT * Math.sin(overshoot * mAngleMultiplier)); - } - }; - - private final Interpolator mOverviewBoundInterpolator = (v) -> { - if (v >= MAXIMUM_DISTANCE_FACTOR) { - return 1; - } - return FAST_OUT_SLOW_IN.getInterpolation(v / MAXIMUM_DISTANCE_FACTOR); - }; public PortraitStatesTouchController(Launcher l) { super(l, SwipeDetector.VERTICAL); @@ -122,24 +92,6 @@ public class PortraitStatesTouchController extends AbstractStateChangeTouchContr } @Override - protected int getSwipeDirection(MotionEvent ev) { - final int directionsToDetectScroll; - if (mLauncher.isInState(ALL_APPS)) { - directionsToDetectScroll = SwipeDetector.DIRECTION_NEGATIVE; - mStartContainerType = ContainerType.ALLAPPS; - } else if (mLauncher.isInState(NORMAL)) { - directionsToDetectScroll = SwipeDetector.DIRECTION_POSITIVE; - mStartContainerType = ContainerType.HOTSEAT; - } else if (mLauncher.isInState(OVERVIEW)) { - directionsToDetectScroll = SwipeDetector.DIRECTION_BOTH; - mStartContainerType = ContainerType.TASKSWITCHER; - } else { - return 0; - } - return directionsToDetectScroll; - } - - @Override protected LauncherState getTargetState(LauncherState fromState, boolean isDragTowardPositive) { if (fromState == ALL_APPS && !isDragTowardPositive) { // Should swipe down go to OVERVIEW instead? @@ -147,24 +99,28 @@ public class PortraitStatesTouchController extends AbstractStateChangeTouchContr mLauncher.getStateManager().getLastState() : NORMAL; } else if (fromState == OVERVIEW) { return isDragTowardPositive ? ALL_APPS : NORMAL; - } else if (isDragTowardPositive) { + } else if (fromState == NORMAL && isDragTowardPositive) { return TouchInteractionService.isConnected() ? OVERVIEW : ALL_APPS; } return fromState; } + @Override + protected int getLogContainerTypeForNormalState() { + return ContainerType.HOTSEAT; + } + private AnimatorSetBuilder getNormalToOverviewAnimation() { - mAllAppsInterpolatorWrapper.baseInterpolator = mAllAppsDampedInterpolator; + mAllAppsInterpolatorWrapper.baseInterpolator = LINEAR; AnimatorSetBuilder builder = new AnimatorSetBuilder(); builder.setInterpolator(ANIM_VERTICAL_PROGRESS, mAllAppsInterpolatorWrapper); - builder.setInterpolator(ANIM_OVERVIEW_TRANSLATION, mOverviewBoundInterpolator); return builder; } @Override - protected float initCurrentAnimation() { + protected float initCurrentAnimation(@AnimationComponents int animComponents) { float range = getShiftRange(); long maxAccuracy = (long) (2 * range); @@ -177,15 +133,11 @@ public class PortraitStatesTouchController extends AbstractStateChangeTouchContr if (mFromState == NORMAL && mToState == OVERVIEW && totalShift != 0) { builder = getNormalToOverviewAnimation(); - totalShift = totalShift * TOTAL_DISTANCE_MULTIPLIER; } else { builder = new AnimatorSetBuilder(); } - if (mPendingAnimation != null) { - mPendingAnimation.finish(false, Touch.SWIPE); - mPendingAnimation = null; - } + cancelPendingAnim(); RecentsView recentsView = mLauncher.getOverviewPanel(); TaskView taskView = (TaskView) recentsView.getChildAt(recentsView.getNextPage()); @@ -194,10 +146,17 @@ public class PortraitStatesTouchController extends AbstractStateChangeTouchContr mPendingAnimation = recentsView.createTaskLauncherAnimation(taskView, maxAccuracy); mPendingAnimation.anim.setInterpolator(Interpolators.ZOOM_IN); - mCurrentAnimation = AnimatorPlaybackController.wrap(mPendingAnimation.anim, maxAccuracy); + Runnable onCancelRunnable = () -> { + cancelPendingAnim(); + clearState(); + }; + mCurrentAnimation = AnimatorPlaybackController.wrap(mPendingAnimation.anim, maxAccuracy, + onCancelRunnable); + mLauncher.getStateManager().setCurrentUserControlledAnimation(mCurrentAnimation); } else { mCurrentAnimation = mLauncher.getStateManager() - .createAnimationToNewWorkspace(mToState, builder, maxAccuracy); + .createAnimationToNewWorkspace(mToState, builder, maxAccuracy, this::clearState, + animComponents); } if (totalShift == 0) { @@ -207,12 +166,19 @@ public class PortraitStatesTouchController extends AbstractStateChangeTouchContr return 1 / totalShift; } + private void cancelPendingAnim() { + if (mPendingAnimation != null) { + mPendingAnimation.finish(false, Touch.SWIPE); + mPendingAnimation = null; + } + } + @Override protected void updateSwipeCompleteAnimation(ValueAnimator animator, long expectedDuration, LauncherState targetState, float velocity, boolean isFling) { - handleFirstSwipeToOverview(animator, expectedDuration, targetState, velocity, isFling); super.updateSwipeCompleteAnimation(animator, expectedDuration, targetState, velocity, isFling); + handleFirstSwipeToOverview(animator, expectedDuration, targetState, velocity, isFling); } private void handleFirstSwipeToOverview(final ValueAnimator animator, @@ -220,69 +186,24 @@ public class PortraitStatesTouchController extends AbstractStateChangeTouchContr final boolean isFling) { if (mFromState == NORMAL && mToState == OVERVIEW && targetState == OVERVIEW) { mFinishFastOnSecondTouch = true; - - // Update all apps interpolator - float currentFraction = mCurrentAnimation.getProgressFraction(); - float absVelocity = Math.abs(velocity); - float currentValue = mAllAppsDampedInterpolator.getInterpolation(currentFraction); - - if (isFling && absVelocity > 1 && currentFraction < LINEAR_SCALE_LIMIT) { - - // TODO: Clean up these magic calculations - // Linearly interpolate the max value based on the velocity. - float maxValue = Math.max(absVelocity > 4 ? 1 + MAX_OVERSHOOT : - 1 + (absVelocity - 1) * MAX_OVERSHOOT / 3, - currentValue); - double angleToPeak = PI_BY_2 - Math.asin(currentValue / maxValue); - - if (expectedDuration != 0 && angleToPeak != 0) { - - float distanceLeft = 1 - currentFraction; - mAllAppsInterpolatorWrapper.baseInterpolator = (f) -> { - float scaledF = (f - currentFraction) / distanceLeft; - - if (scaledF < 0.5f) { - double angle = PI_BY_2 - angleToPeak + scaledF * angleToPeak / 0.5f; - return (float) (maxValue * Math.sin(angle)); - } - - scaledF = ((scaledF - .5f) / .5f); - double angle = PI_BY_2 + 3 * scaledF * PI_BY_2; - float amplitude = (1 - scaledF) * (1 - scaledF) * (maxValue - 1); - return 1 + (float) (amplitude * Math.sin(angle)); - }; - - animator.setDuration(expectedDuration).setInterpolator(LINEAR); - return; - } + if (isFling && expectedDuration != 0) { + // Update all apps interpolator to add a bit of overshoot starting from currFraction + final float currFraction = mCurrentAnimation.getProgressFraction(); + mAllAppsInterpolatorWrapper.baseInterpolator = Interpolators.clampToProgress( + new OvershootInterpolator(Math.min(Math.abs(velocity), 3f)), currFraction, 1); + animator.setDuration(Math.min(expectedDuration, ATOMIC_DURATION)) + .setInterpolator(LINEAR); } - - if (currentFraction < LINEAR_SCALE_LIMIT) { - mAllAppsInterpolatorWrapper.baseInterpolator = LINEAR; - return; - } - float extraValue = mAllAppsDampedInterpolator.getInterpolation(currentFraction) - 1; - float distanceLeft = 1 - currentFraction; - - animator.setFloatValues(currentFraction, 1); - mAllAppsInterpolatorWrapper.baseInterpolator = (f) -> { - float scaledF = (f - currentFraction) / distanceLeft; - - double angle = scaledF * 1.5 * Math.PI; - float amplitude = (1 - scaledF) * (1 - scaledF) * extraValue; - return 1 + (float) (amplitude * Math.sin(angle)); - }; - animator.setDuration(200).setInterpolator(LINEAR); - return; + } else { + mFinishFastOnSecondTouch = false; } - mFinishFastOnSecondTouch = false; } @Override protected void onSwipeInteractionCompleted(LauncherState targetState, int logAction) { super.onSwipeInteractionCompleted(targetState, logAction); if (mFromState == NORMAL && targetState == OVERVIEW) { - SysuiEventLogger.writeDummyRecentsTransition(0); + RecentsModel.getInstance(mLauncher).onOverviewShown(true, TAG); } } diff --git a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java index 124ec202d..e43b24a6a 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java +++ b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java @@ -15,39 +15,49 @@ */ package com.android.launcher3.uioverrides; -import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_TRANSLATION; +import static com.android.launcher3.LauncherState.FAST_OVERVIEW; +import static com.android.launcher3.LauncherState.OVERVIEW; +import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_FADE; +import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_SCALE; import static com.android.launcher3.anim.Interpolators.AGGRESSIVE_EASE_IN_OUT; import static com.android.launcher3.anim.Interpolators.LINEAR; +import static com.android.quickstep.QuickScrubController.QUICK_SCRUB_START_INTERPOLATOR; +import static com.android.quickstep.QuickScrubController.QUICK_SCRUB_TRANSLATION_Y_FACTOR; import static com.android.quickstep.views.LauncherRecentsView.TRANSLATION_Y_FACTOR; import static com.android.quickstep.views.RecentsView.ADJACENT_SCALE; -import static com.android.quickstep.views.RecentsView.CONTENT_ALPHA; +import static com.android.quickstep.views.RecentsViewContainer.CONTENT_ALPHA; import android.animation.ValueAnimator; import android.annotation.TargetApi; import android.os.Build; +import android.view.animation.Interpolator; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherState; import com.android.launcher3.LauncherStateManager.AnimationConfig; import com.android.launcher3.LauncherStateManager.StateHandler; import com.android.launcher3.anim.AnimatorSetBuilder; +import com.android.launcher3.anim.Interpolators; import com.android.launcher3.anim.PropertySetter; import com.android.quickstep.views.LauncherRecentsView; +import com.android.quickstep.views.RecentsViewContainer; @TargetApi(Build.VERSION_CODES.O) public class RecentsViewStateController implements StateHandler { private final Launcher mLauncher; private final LauncherRecentsView mRecentsView; + private final RecentsViewContainer mRecentsViewContainer; public RecentsViewStateController(Launcher launcher) { mLauncher = launcher; mRecentsView = launcher.getOverviewPanel(); + mRecentsViewContainer = launcher.getOverviewPanelContainer(); } @Override public void setState(LauncherState state) { - mRecentsView.setContentAlpha(state.overviewUi ? 1 : 0); + mRecentsViewContainer.setContentAlpha(state.overviewUi ? 1 : 0); float[] scaleTranslationYFactor = state.getOverviewScaleAndTranslationYFactor(mLauncher); mRecentsView.setAdjacentScale(scaleTranslationYFactor[0]); mRecentsView.setTranslationYFactor(scaleTranslationYFactor[1]); @@ -60,14 +70,23 @@ public class RecentsViewStateController implements StateHandler { @Override public void setStateWithAnimation(final LauncherState toState, AnimatorSetBuilder builder, AnimationConfig config) { - PropertySetter setter = config.getProperSetter(builder); + if (!config.playAtomicComponent()) { + // The entire recents animation is played atomically. + return; + } + PropertySetter setter = config.getPropertySetter(builder); float[] scaleTranslationYFactor = toState.getOverviewScaleAndTranslationYFactor(mLauncher); - setter.setFloat(mRecentsView, ADJACENT_SCALE, scaleTranslationYFactor[0], - builder.getInterpolator(ANIM_OVERVIEW_TRANSLATION, LINEAR)); + Interpolator scaleInterpolator = builder.getInterpolator(ANIM_OVERVIEW_SCALE, LINEAR); + setter.setFloat(mRecentsView, ADJACENT_SCALE, scaleTranslationYFactor[0], scaleInterpolator); + Interpolator transYInterpolator = scaleInterpolator; + if (mLauncher.getStateManager().getState() == OVERVIEW && toState == FAST_OVERVIEW) { + transYInterpolator = Interpolators.clampToProgress(QUICK_SCRUB_START_INTERPOLATOR, 0, + QUICK_SCRUB_TRANSLATION_Y_FACTOR); + } setter.setFloat(mRecentsView, TRANSLATION_Y_FACTOR, scaleTranslationYFactor[1], - builder.getInterpolator(ANIM_OVERVIEW_TRANSLATION, LINEAR)); - setter.setFloat(mRecentsView, CONTENT_ALPHA, toState.overviewUi ? 1 : 0, - AGGRESSIVE_EASE_IN_OUT); + transYInterpolator); + setter.setFloat(mRecentsViewContainer, CONTENT_ALPHA, toState.overviewUi ? 1 : 0, + builder.getInterpolator(ANIM_OVERVIEW_FADE, AGGRESSIVE_EASE_IN_OUT)); if (!toState.overviewUi) { builder.addOnFinishRunnable(mRecentsView::resetTaskVisuals); diff --git a/quickstep/src/com/android/launcher3/uioverrides/TaskViewTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/TaskViewTouchController.java index 84a60bd9a..a40573500 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/TaskViewTouchController.java +++ b/quickstep/src/com/android/launcher3/uioverrides/TaskViewTouchController.java @@ -15,25 +15,27 @@ */ package com.android.launcher3.uioverrides; +import static com.android.launcher3.Utilities.SINGLE_FRAME_MS; import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; -import android.util.Log; import android.view.MotionEvent; -import android.view.View; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.BaseDraggingActivity; +import com.android.launcher3.LauncherAnimUtils; import com.android.launcher3.Utilities; import com.android.launcher3.anim.AnimatorPlaybackController; import com.android.launcher3.anim.Interpolators; import com.android.launcher3.touch.SwipeDetector; import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch; +import com.android.launcher3.util.FlingBlockCheck; import com.android.launcher3.util.PendingAnimation; import com.android.launcher3.util.TouchController; import com.android.launcher3.views.BaseDragLayer; +import com.android.quickstep.OverviewInteractionState; import com.android.quickstep.views.RecentsView; import com.android.quickstep.views.TaskView; @@ -45,9 +47,6 @@ public abstract class TaskViewTouchController<T extends BaseDraggingActivity> private static final String TAG = "OverviewSwipeController"; - private static final float ALLOWED_FLING_DIRECTION_CHANGE_PROGRESS = 0.1f; - private static final int SINGLE_FRAME_MS = 16; - // Progress after which the transition is assumed to be a success in case user does not fling private static final float SUCCESS_TRANSITION_PROGRESS = 0.5f; @@ -65,6 +64,7 @@ public abstract class TaskViewTouchController<T extends BaseDraggingActivity> private float mDisplacementShift; private float mProgressMultiplier; private float mEndDisplacement; + private FlingBlockCheck mFlingBlockCheck = new FlingBlockCheck(); private TaskView mTaskBeingDragged; @@ -87,12 +87,13 @@ public abstract class TaskViewTouchController<T extends BaseDraggingActivity> protected abstract boolean isRecentsInteractive(); + protected void onUserControlledAnimationCreated(AnimatorPlaybackController animController) { + } + @Override public void onAnimationCancel(Animator animation) { if (mCurrentAnimation != null && animation == mCurrentAnimation.getTarget()) { - Log.e(TAG, "Who dare cancel the animation when I am in control", new Exception()); - mDetector.finishedScrolling(); - mCurrentAnimation = null; + clearState(); } } @@ -106,7 +107,7 @@ public abstract class TaskViewTouchController<T extends BaseDraggingActivity> // Now figure out which direction scroll events the controller will start // calling the callbacks. - final int directionsToDetectScroll; + int directionsToDetectScroll = 0; boolean ignoreSlopWhenSettling = false; if (mCurrentAnimation != null) { directionsToDetectScroll = SwipeDetector.DIRECTION_BOTH; @@ -114,12 +115,26 @@ public abstract class TaskViewTouchController<T extends BaseDraggingActivity> } else { mTaskBeingDragged = null; - View view = mRecentsView.getChildAt(mRecentsView.getCurrentPage()); - if (view instanceof TaskView && mActivity.getDragLayer().isEventOverView(view, ev)) { - // The tile can be dragged down to open the task. - mTaskBeingDragged = (TaskView) view; - directionsToDetectScroll = SwipeDetector.DIRECTION_BOTH; - } else { + for (int i = 0; i < mRecentsView.getChildCount(); i++) { + TaskView view = mRecentsView.getPageAt(i); + if (mRecentsView.isTaskViewVisible(view) && mActivity.getDragLayer() + .isEventOverView(view, ev)) { + mTaskBeingDragged = view; + if (!OverviewInteractionState.getInstance(mActivity) + .isSwipeUpGestureEnabled()) { + // Don't allow swipe down to open if we don't support swipe up + // to enter overview. + directionsToDetectScroll = SwipeDetector.DIRECTION_POSITIVE; + } else { + // The task can be dragged up to dismiss it, + // and down to open if it's the current page. + directionsToDetectScroll = i == mRecentsView.getCurrentPage() + ? SwipeDetector.DIRECTION_BOTH : SwipeDetector.DIRECTION_POSITIVE; + } + break; + } + } + if (mTaskBeingDragged == null) { mNoIntercept = true; return false; } @@ -147,6 +162,12 @@ public abstract class TaskViewTouchController<T extends BaseDraggingActivity> // No need to init return; } + int scrollDirections = mDetector.getScrollDirections(); + if (goingUp && ((scrollDirections & SwipeDetector.DIRECTION_POSITIVE) == 0) + || !goingUp && ((scrollDirections & SwipeDetector.DIRECTION_NEGATIVE) == 0)) { + // Trying to re-init in an unsupported direction. + return; + } if (mCurrentAnimation != null) { mCurrentAnimation.setPlayFraction(0); } @@ -174,8 +195,12 @@ public abstract class TaskViewTouchController<T extends BaseDraggingActivity> mEndDisplacement = dl.getHeight() - mTempCords[1]; } + if (mCurrentAnimation != null) { + mCurrentAnimation.setOnCancelRunnable(null); + } mCurrentAnimation = AnimatorPlaybackController - .wrap(mPendingAnimation.anim, maxDuration); + .wrap(mPendingAnimation.anim, maxDuration, this::clearState); + onUserControlledAnimationCreated(mCurrentAnimation); mCurrentAnimation.getTarget().addListener(this); mCurrentAnimation.dispatchOnStart(); mProgressMultiplier = 1 / mEndDisplacement; @@ -190,6 +215,7 @@ public abstract class TaskViewTouchController<T extends BaseDraggingActivity> mDisplacementShift = mCurrentAnimation.getProgressFraction() / mProgressMultiplier; mCurrentAnimation.pause(); } + mFlingBlockCheck.unblockFling(); } @Override @@ -199,6 +225,9 @@ public abstract class TaskViewTouchController<T extends BaseDraggingActivity> totalDisplacement == 0 ? mCurrentAnimationIsGoingUp : totalDisplacement < 0; if (isGoingUp != mCurrentAnimationIsGoingUp) { reInitAnimationController(isGoingUp); + mFlingBlockCheck.blockFling(); + } else { + mFlingBlockCheck.onEvent(); } mCurrentAnimation.setPlayFraction(totalDisplacement * mProgressMultiplier); return true; @@ -208,23 +237,14 @@ public abstract class TaskViewTouchController<T extends BaseDraggingActivity> public void onDragEnd(float velocity, boolean fling) { final boolean goingToEnd; final int logAction; + boolean blockedFling = fling && mFlingBlockCheck.isBlocked(); + if (blockedFling) { + fling = false; + } if (fling) { logAction = Touch.FLING; boolean goingUp = velocity < 0; - if (goingUp != mCurrentAnimationIsGoingUp) { - // In case the fling is in opposite direction, make sure if is close enough - // from the start position - if (mCurrentAnimation.getProgressFraction() - >= ALLOWED_FLING_DIRECTION_CHANGE_PROGRESS) { - // Not allowed - goingToEnd = false; - } else { - reInitAnimationController(goingUp); - goingToEnd = true; - } - } else { - goingToEnd = true; - } + goingToEnd = goingUp == mCurrentAnimationIsGoingUp; } else { logAction = Touch.SWIPE; goingToEnd = mCurrentAnimation.getProgressFraction() > SUCCESS_TRANSITION_PROGRESS; @@ -233,6 +253,9 @@ public abstract class TaskViewTouchController<T extends BaseDraggingActivity> float progress = mCurrentAnimation.getProgressFraction(); long animationDuration = SwipeDetector.calculateDuration( velocity, goingToEnd ? (1 - progress) : progress); + if (blockedFling && !goingToEnd) { + animationDuration *= LauncherAnimUtils.blockedFlingDurationFactor(velocity); + } float nextFrameProgress = Utilities.boundToRange( progress + velocity * SINGLE_FRAME_MS / Math.abs(mEndDisplacement), 0f, 1f); @@ -251,8 +274,17 @@ public abstract class TaskViewTouchController<T extends BaseDraggingActivity> mPendingAnimation.finish(wasSuccess, logAction); mPendingAnimation = null; } + clearState(); + } + + private void clearState() { mDetector.finishedScrolling(); + mDetector.setDetectableScrollConditions(0, false); mTaskBeingDragged = null; mCurrentAnimation = null; + if (mPendingAnimation != null) { + mPendingAnimation.finish(false, Touch.SWIPE); + mPendingAnimation = null; + } } } diff --git a/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java b/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java index c1590f63d..b371677f3 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java +++ b/quickstep/src/com/android/launcher3/uioverrides/UiFactory.java @@ -16,51 +16,67 @@ package com.android.launcher3.uioverrides; +import static android.view.View.VISIBLE; +import static com.android.launcher3.AbstractFloatingView.TYPE_ALL; +import static com.android.launcher3.AbstractFloatingView.TYPE_HIDE_BACK_BUTTON; +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.getPrefs; -import static com.android.quickstep.OverviewInteractionState.KEY_SWIPE_UP_ENABLED; -import static com.android.launcher3.LauncherState.ALL_APPS; +import static com.android.launcher3.allapps.DiscoveryBounce.HOME_BOUNCE_SEEN; +import static com.android.launcher3.allapps.DiscoveryBounce.SHELF_BOUNCE_SEEN; +import android.app.Activity; import android.content.Context; -import android.content.SharedPreferences; +import android.util.Base64; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.DeviceProfile; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherState; +import com.android.launcher3.LauncherStateManager; import com.android.launcher3.LauncherStateManager.StateHandler; +import com.android.launcher3.Utilities; +import com.android.launcher3.anim.AnimatorPlaybackController; import com.android.launcher3.util.TouchController; import com.android.quickstep.OverviewInteractionState; import com.android.quickstep.RecentsModel; import com.android.quickstep.views.RecentsView; +import com.android.systemui.shared.system.ActivityCompat; import com.android.systemui.shared.system.WindowManagerWrapper; +import java.io.ByteArrayOutputStream; +import java.io.PrintWriter; +import java.util.zip.Deflater; + public class UiFactory { public static TouchController[] createTouchControllers(Launcher launcher) { - SharedPreferences prefs = getPrefs(launcher); - boolean swipeUpEnabled = prefs.getBoolean(KEY_SWIPE_UP_ENABLED, true); + boolean swipeUpEnabled = OverviewInteractionState.getInstance(launcher) + .isSwipeUpGestureEnabled(); if (!swipeUpEnabled) { return new TouchController[] { launcher.getDragController(), - new LandscapeStatesTouchController(launcher), - new LauncherTaskViewcontroller(launcher)}; + new OverviewToAllAppsTouchController(launcher), + new LauncherTaskViewController(launcher)}; } if (launcher.getDeviceProfile().isVerticalBarLayout()) { return new TouchController[] { launcher.getDragController(), - new LandscapeStatesTouchController(launcher), + new OverviewToAllAppsTouchController(launcher), new LandscapeEdgeSwipeController(launcher), - new LauncherTaskViewcontroller(launcher)}; + new LauncherTaskViewController(launcher)}; } else { return new TouchController[] { launcher.getDragController(), new PortraitStatesTouchController(launcher), - new LauncherTaskViewcontroller(launcher)}; + new LauncherTaskViewController(launcher)}; } } + public static void setOnTouchControllersChangedListener(Context context, Runnable listener) { + OverviewInteractionState.getInstance(context).setOnSwipeUpSettingChangedListener(listener); + } + public static StateHandler[] getStateHandler(Launcher launcher) { return new StateHandler[] { launcher.getAllAppsController(), launcher.getWorkspace(), @@ -73,10 +89,15 @@ public class UiFactory { && launcher.hasWindowFocus(); if (shouldBackButtonBeHidden) { // Show the back button if there is a floating view visible. - shouldBackButtonBeHidden = AbstractFloatingView.getTopOpenView(launcher) == null; + shouldBackButtonBeHidden = AbstractFloatingView.getTopOpenViewWithType(launcher, + TYPE_ALL & ~TYPE_HIDE_BACK_BUTTON) == null; } OverviewInteractionState.getInstance(launcher) - .setBackButtonVisible(!shouldBackButtonBeHidden); + .setBackButtonAlpha(shouldBackButtonBeHidden ? 0 : 1, true /* animate */); + } + + public static void setBackButtonAlpha(Launcher launcher, float alpha, boolean animate) { + OverviewInteractionState.getInstance(launcher).setBackButtonAlpha(alpha,animate); } public static void resetOverview(Launcher launcher) { @@ -84,6 +105,55 @@ public class UiFactory { recents.reset(); } + public static void onCreate(Launcher launcher) { + if (!launcher.getSharedPrefs().getBoolean(HOME_BOUNCE_SEEN, false)) { + launcher.getStateManager().addStateListener(new LauncherStateManager.StateListener() { + @Override + public void onStateSetImmediately(LauncherState state) { + } + + @Override + public void onStateTransitionStart(LauncherState toState) { + } + + @Override + public void onStateTransitionComplete(LauncherState finalState) { + boolean swipeUpEnabled = OverviewInteractionState.getInstance(launcher) + .isSwipeUpGestureEnabled(); + LauncherState prevState = launcher.getStateManager().getLastState(); + + if (((swipeUpEnabled && finalState == OVERVIEW) || (!swipeUpEnabled + && finalState == ALL_APPS && prevState == NORMAL))) { + launcher.getSharedPrefs().edit().putBoolean(HOME_BOUNCE_SEEN, true).apply(); + launcher.getStateManager().removeStateListener(this); + } + } + }); + } + + if (!launcher.getSharedPrefs().getBoolean(SHELF_BOUNCE_SEEN, false)) { + launcher.getStateManager().addStateListener(new LauncherStateManager.StateListener() { + @Override + public void onStateSetImmediately(LauncherState state) { + } + + @Override + public void onStateTransitionStart(LauncherState toState) { + } + + @Override + public void onStateTransitionComplete(LauncherState finalState) { + LauncherState prevState = launcher.getStateManager().getLastState(); + + if (finalState == ALL_APPS && prevState == OVERVIEW) { + launcher.getSharedPrefs().edit().putBoolean(SHELF_BOUNCE_SEEN, true).apply(); + launcher.getStateManager().removeStateListener(this); + } + } + }); + } + } + public static void onStart(Context context) { RecentsModel model = RecentsModel.getInstance(context); if (model != null) { @@ -110,9 +180,42 @@ public class UiFactory { } } - private static class LauncherTaskViewcontroller extends TaskViewTouchController<Launcher> { + public static boolean dumpActivity(Activity activity, PrintWriter writer) { + if (!Utilities.IS_DEBUG_DEVICE) { + return false; + } + ByteArrayOutputStream out = new ByteArrayOutputStream(); + if (!(new ActivityCompat(activity).encodeViewHierarchy(out))) { + return false; + } + + Deflater deflater = new Deflater(); + deflater.setInput(out.toByteArray()); + deflater.finish(); + + out.reset(); + byte[] buffer = new byte[1024]; + while (!deflater.finished()) { + int count = deflater.deflate(buffer); // returns the generated code... index + out.write(buffer, 0, count); + } + + writer.println("--encoded-view-dump-v0--"); + writer.println(Base64.encodeToString( + out.toByteArray(), Base64.NO_WRAP | Base64.NO_PADDING)); + return true; + } + + public static void prepareToShowOverview(Launcher launcher) { + RecentsView overview = launcher.getOverviewPanel(); + if (overview.getVisibility() != VISIBLE || overview.getContentAlpha() == 0) { + overview.setAdjacentScale(1.33f); + } + } + + private static class LauncherTaskViewController extends TaskViewTouchController<Launcher> { - public LauncherTaskViewcontroller(Launcher activity) { + public LauncherTaskViewController(Launcher activity) { super(activity); } @@ -120,5 +223,10 @@ public class UiFactory { protected boolean isRecentsInteractive() { return mActivity.isInState(OVERVIEW); } + + @Override + protected void onUserControlledAnimationCreated(AnimatorPlaybackController animController) { + mActivity.getStateManager().setCurrentUserControlledAnimation(animController); + } } } diff --git a/quickstep/src/com/android/launcher3/uioverrides/WallpaperColorInfo.java b/quickstep/src/com/android/launcher3/uioverrides/WallpaperColorInfo.java new file mode 100644 index 000000000..8218517dc --- /dev/null +++ b/quickstep/src/com/android/launcher3/uioverrides/WallpaperColorInfo.java @@ -0,0 +1,115 @@ +/* + * 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.uioverrides; + +import static android.app.WallpaperManager.FLAG_SYSTEM; + +import android.annotation.TargetApi; +import android.app.WallpaperColors; +import android.app.WallpaperManager; +import android.app.WallpaperManager.OnColorsChangedListener; +import android.content.Context; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; + +import com.android.systemui.shared.system.TonalCompat; +import com.android.systemui.shared.system.TonalCompat.ExtractionInfo; + +import java.util.ArrayList; + +@TargetApi(Build.VERSION_CODES.P) +public class WallpaperColorInfo implements OnColorsChangedListener { + + private static final Object sInstanceLock = new Object(); + private static WallpaperColorInfo sInstance; + + public static WallpaperColorInfo getInstance(Context context) { + synchronized (sInstanceLock) { + if (sInstance == null) { + sInstance = new WallpaperColorInfo(context.getApplicationContext()); + } + return sInstance; + } + } + + private final ArrayList<OnChangeListener> mListeners = new ArrayList<>(); + private final WallpaperManager mWallpaperManager; + private final TonalCompat mTonalCompat; + + private ExtractionInfo mExtractionInfo; + + private OnChangeListener[] mTempListeners = new OnChangeListener[0]; + + private WallpaperColorInfo(Context context) { + mWallpaperManager = context.getSystemService(WallpaperManager.class); + mTonalCompat = new TonalCompat(context); + + mWallpaperManager.addOnColorsChangedListener(this, new Handler(Looper.getMainLooper())); + update(mWallpaperManager.getWallpaperColors(FLAG_SYSTEM)); + } + + public int getMainColor() { + return mExtractionInfo.mainColor; + } + + public int getSecondaryColor() { + return mExtractionInfo.secondaryColor; + } + + public boolean isDark() { + return mExtractionInfo.supportsDarkTheme; + } + + public boolean supportsDarkText() { + return mExtractionInfo.supportsDarkText; + } + + @Override + public void onColorsChanged(WallpaperColors colors, int which) { + if ((which & FLAG_SYSTEM) != 0) { + update(colors); + notifyChange(); + } + } + + private void update(WallpaperColors wallpaperColors) { + mExtractionInfo = mTonalCompat.extractDarkColors(wallpaperColors); + } + + public void addOnChangeListener(OnChangeListener listener) { + mListeners.add(listener); + } + + public void removeOnChangeListener(OnChangeListener listener) { + mListeners.remove(listener); + } + + private void notifyChange() { + // Create a new array to avoid concurrent modification when the activity destroys itself. + mTempListeners = mListeners.toArray(mTempListeners); + for (OnChangeListener listener : mTempListeners) { + if (listener != null) { + listener.onExtractedColorsChanged(this); + } + } + } + + public interface OnChangeListener { + void onExtractedColorsChanged(WallpaperColorInfo wallpaperColorInfo); + } +} diff --git a/quickstep/src/com/android/quickstep/ActivityControlHelper.java b/quickstep/src/com/android/quickstep/ActivityControlHelper.java index 584128585..ae0affee0 100644 --- a/quickstep/src/com/android/quickstep/ActivityControlHelper.java +++ b/quickstep/src/com/android/quickstep/ActivityControlHelper.java @@ -15,16 +15,23 @@ */ package com.android.quickstep; +import static com.android.launcher3.LauncherAnimUtils.OVERVIEW_TRANSITION_MS; import static com.android.launcher3.LauncherState.FAST_OVERVIEW; import static com.android.launcher3.LauncherState.OVERVIEW; import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS; import static com.android.launcher3.anim.Interpolators.LINEAR; +import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_BACK; +import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_ROTATION; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; +import android.annotation.TargetApi; +import android.app.ActivityManager.RunningTaskInfo; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.graphics.Rect; +import android.os.Build; import android.os.Handler; import android.os.Looper; import android.support.annotation.Nullable; @@ -38,38 +45,41 @@ import com.android.launcher3.LauncherAppState; import com.android.launcher3.LauncherInitListener; import com.android.launcher3.LauncherState; import com.android.launcher3.allapps.AllAppsTransitionController; +import com.android.launcher3.allapps.DiscoveryBounce; import com.android.launcher3.anim.AnimatorPlaybackController; -import com.android.launcher3.util.ViewOnDrawExecutor; -import com.android.quickstep.fallback.FallbackRecentsView; +import com.android.launcher3.dragndrop.DragLayer; +import com.android.launcher3.userevent.nano.LauncherLogProto; +import com.android.launcher3.util.MultiValueAlpha.AlphaProperty; +import com.android.quickstep.util.LayoutUtils; import com.android.quickstep.util.RemoteAnimationProvider; +import com.android.quickstep.util.RemoteAnimationTargetSet; import com.android.quickstep.views.LauncherLayoutListener; import com.android.quickstep.views.LauncherRecentsView; import com.android.quickstep.views.RecentsView; -import com.android.quickstep.views.TaskView; -import com.android.systemui.shared.system.ActivityManagerWrapper; -import com.android.systemui.shared.system.AssistDataReceiver; -import com.android.systemui.shared.system.RecentsAnimationListener; +import com.android.quickstep.views.RecentsViewContainer; +import com.android.systemui.shared.system.RemoteAnimationTargetCompat; +import java.util.Objects; import java.util.function.BiPredicate; +import java.util.function.Consumer; /** * Utility class which abstracts out the logical differences between Launcher and RecentsActivity. */ +@TargetApi(Build.VERSION_CODES.P) public interface ActivityControlHelper<T extends BaseDraggingActivity> { LayoutListener createLayoutListener(T activity); - void onQuickstepGestureStarted(T activity, boolean activityVisible); - /** * Updates the UI to indicate quick interaction. - * @return true if there any any UI change as a result of this */ - boolean onQuickInteractionStart(T activity, boolean activityVisible); + void onQuickInteractionStart(T activity, @Nullable RunningTaskInfo taskInfo, + boolean activityVisible); - void executeOnWindowAvailable(T activity, Runnable action); + float getTranslationYForQuickScrub(T activity); - void executeOnNextDraw(T activity, TaskView targetView, Runnable action); + void executeOnWindowAvailable(T activity, Runnable action); void onTransitionCancelled(T activity, boolean activityVisible); @@ -77,17 +87,11 @@ public interface ActivityControlHelper<T extends BaseDraggingActivity> { void onSwipeUpComplete(T activity); - void prepareRecentsUI(T activity, boolean activityVisible); - - AnimatorPlaybackController createControllerForVisibleActivity(T activity); - - AnimatorPlaybackController createControllerForHiddenActivity(T activity, int transitionLength); + AnimationFactory prepareRecentsUI(T activity, boolean activityVisible, + Consumer<AnimatorPlaybackController> callback); ActivityInitListener createActivityInitListener(BiPredicate<T, Boolean> onInitListener); - void startRecentsFromSwipe(Intent intent, AssistDataReceiver assistDataReceiver, - final RecentsAnimationListener remoteAnimationListener); - @Nullable T getCreatedActivity(); @@ -96,7 +100,31 @@ public interface ActivityControlHelper<T extends BaseDraggingActivity> { RecentsView getVisibleRecentsView(); @UiThread - boolean switchToRecentsIfVisible(); + boolean switchToRecentsIfVisible(boolean fromRecentsButton); + + Rect getOverviewWindowBounds(Rect homeBounds, RemoteAnimationTargetCompat target); + + boolean shouldMinimizeSplitScreen(); + + /** + * @return {@code true} if recents activity should be started immediately on touchDown, + * {@code false} if it should deferred until some threshold is crossed. + */ + boolean deferStartingActivity(int downHitTarget); + + boolean supportsLongSwipe(T activity); + + AlphaProperty getAlphaProperty(T activity); + + /** + * Must return a non-null controller is supportsLongSwipe was true. + */ + LongSwipeHelper getLongSwipeController(T activity, RemoteAnimationTargetSet targetSet); + + /** + * Used for containerType in {@link com.android.launcher3.logging.UserEventDispatcher} + */ + int getContainerType(); class LauncherActivityControllerHelper implements ActivityControlHelper<Launcher> { @@ -106,42 +134,31 @@ public interface ActivityControlHelper<T extends BaseDraggingActivity> { } @Override - public void onQuickstepGestureStarted(Launcher activity, boolean activityVisible) { - activity.onQuickstepGestureStarted(activityVisible); - } - - @Override - public boolean onQuickInteractionStart(Launcher activity, boolean activityVisible) { + public void onQuickInteractionStart(Launcher activity, RunningTaskInfo taskInfo, + boolean activityVisible) { LauncherState fromState = activity.getStateManager().getState(); activity.getStateManager().goToState(FAST_OVERVIEW, activityVisible); - return !fromState.overviewUi; + + QuickScrubController controller = activity.<RecentsView>getOverviewPanel() + .getQuickScrubController(); + controller.onQuickScrubStart(activityVisible && !fromState.overviewUi, this); } @Override - public void executeOnWindowAvailable(Launcher activity, Runnable action) { - if (activity.getWorkspace().runOnOverlayHidden(action)) { - // Notify the activity that qiuckscrub has started - onQuickstepGestureStarted(activity, true); - } + public float getTranslationYForQuickScrub(Launcher activity) { + LauncherRecentsView recentsView = activity.getOverviewPanel(); + float transYFactor = FAST_OVERVIEW.getOverviewScaleAndTranslationYFactor(activity)[1]; + return recentsView.computeTranslationYForFactor(transYFactor); } @Override - public void executeOnNextDraw(Launcher activity, TaskView targetView, Runnable action) { - ViewOnDrawExecutor executor = new ViewOnDrawExecutor() { - @Override - public void onViewDetachedFromWindow(View v) { - if (!isCompleted()) { - runAllTasks(); - } - } - }; - executor.attachTo(activity, targetView, false /* waitForLoadAnimation */); - executor.execute(action); + public void executeOnWindowAvailable(Launcher activity, Runnable action) { + activity.getWorkspace().runOnOverlayHidden(action); } @Override public int getSwipeUpDestinationAndLength(DeviceProfile dp, Context context, Rect outRect) { - LauncherRecentsView.getPageRect(dp, context, outRect); + LayoutUtils.calculateLauncherTaskSize(context, dp, outRect); if (dp.isVerticalBarLayout()) { Rect targetInsets = dp.getInsets(); int hotseatInset = dp.isSeascape() ? targetInsets.left : targetInsets.right; @@ -161,15 +178,19 @@ public interface ActivityControlHelper<T extends BaseDraggingActivity> { public void onSwipeUpComplete(Launcher activity) { // Re apply state in case we did something funky during the transition. activity.getStateManager().reapplyState(); + DiscoveryBounce.showForOverviewIfNeeded(activity); } @Override - public void prepareRecentsUI(Launcher activity, boolean activityVisible) { - LauncherState startState = activity.getStateManager().getState(); + public AnimationFactory prepareRecentsUI(Launcher activity, boolean activityVisible, + Consumer<AnimatorPlaybackController> callback) { + final LauncherState startState = activity.getStateManager().getState(); + + LauncherState resetState = startState; if (startState.disableRestore) { - startState = activity.getStateManager().getRestState(); + resetState = activity.getStateManager().getRestState(); } - activity.getStateManager().setRestState(startState); + activity.getStateManager().setRestState(resetState); if (!activityVisible) { // Since the launcher is not visible, we can safely reset the scroll position. @@ -180,38 +201,53 @@ public interface ActivityControlHelper<T extends BaseDraggingActivity> { // Optimization, hide the all apps view to prevent layout while initializing activity.getAppsView().getContentView().setVisibility(View.GONE); } - } - @Override - public AnimatorPlaybackController createControllerForVisibleActivity(Launcher activity) { - DeviceProfile dp = activity.getDeviceProfile(); - long accuracy = 2 * Math.max(dp.widthPx, dp.heightPx); - return activity.getStateManager().createAnimationToNewWorkspace(OVERVIEW, accuracy); + return new AnimationFactory() { + @Override + public void createActivityController(long transitionLength) { + createActivityControllerInternal(activity, activityVisible, startState, + transitionLength, callback); + } + + @Override + public void onTransitionCancelled() { + activity.getStateManager().goToState(startState, false /* animate */); + } + }; } - @Override - public AnimatorPlaybackController createControllerForHiddenActivity( - Launcher activity, int transitionLength) { - AllAppsTransitionController controller = activity.getAllAppsController(); - AnimatorSet anim = new AnimatorSet(); + private void createActivityControllerInternal(Launcher activity, boolean wasVisible, + LauncherState startState, long transitionLength, + Consumer<AnimatorPlaybackController> callback) { + if (wasVisible) { + DeviceProfile dp = activity.getDeviceProfile(); + long accuracy = 2 * Math.max(dp.widthPx, dp.heightPx); + activity.getStateManager().goToState(startState, false); + callback.accept(activity.getStateManager() + .createAnimationToNewWorkspace(OVERVIEW, accuracy)); + return; + } + if (activity.getDeviceProfile().isVerticalBarLayout()) { - // TODO: - } else { - float scrollRange = Math.max(controller.getShiftRange(), 1); - float progressDelta = (transitionLength / scrollRange); - - float endProgress = OVERVIEW.getVerticalProgress(activity); - float startProgress = endProgress + progressDelta; - ObjectAnimator shiftAnim = ObjectAnimator.ofFloat( - controller, ALL_APPS_PROGRESS, startProgress, endProgress); - shiftAnim.setInterpolator(LINEAR); - anim.play(shiftAnim); + return; } - // TODO: Link this animation to state animation, so that it is cancelled - // automatically on state change + AllAppsTransitionController controller = activity.getAllAppsController(); + AnimatorSet anim = new AnimatorSet(); + + float scrollRange = Math.max(controller.getShiftRange(), 1); + float progressDelta = (transitionLength / scrollRange); + + float endProgress = OVERVIEW.getVerticalProgress(activity); + float startProgress = endProgress + progressDelta; + ObjectAnimator shiftAnim = ObjectAnimator.ofFloat( + controller, ALL_APPS_PROGRESS, startProgress, endProgress); + shiftAnim.setInterpolator(LINEAR); + anim.play(shiftAnim); + anim.setDuration(transitionLength * 2); - return AnimatorPlaybackController.wrap(anim, transitionLength * 2); + activity.getStateManager().setCurrentAnimation(anim); + callback.accept(AnimatorPlaybackController.wrap(anim, transitionLength * 2)); } @Override @@ -220,13 +256,6 @@ public interface ActivityControlHelper<T extends BaseDraggingActivity> { return new LauncherInitListener(onInitListener); } - @Override - public void startRecentsFromSwipe(Intent intent, AssistDataReceiver assistDataReceiver, - final RecentsAnimationListener remoteAnimationListener) { - ActivityManagerWrapper.getInstance().startRecentsActivity( - intent, assistDataReceiver, remoteAnimationListener, null, null); - } - @Nullable @Override public Launcher getCreatedActivity() { @@ -249,44 +278,101 @@ public interface ActivityControlHelper<T extends BaseDraggingActivity> { @Override public RecentsView getVisibleRecentsView() { Launcher launcher = getVisibleLaucher(); - return launcher != null && launcher.isInState(OVERVIEW) + return launcher != null && launcher.getStateManager().getState().overviewUi ? launcher.getOverviewPanel() : null; } @Override - public boolean switchToRecentsIfVisible() { + public boolean switchToRecentsIfVisible(boolean fromRecentsButton) { Launcher launcher = getVisibleLaucher(); if (launcher != null) { + if (fromRecentsButton) { + launcher.getUserEventDispatcher().logActionCommand( + LauncherLogProto.Action.Command.RECENTS_BUTTON, + getContainerType(), + LauncherLogProto.ContainerType.TASKSWITCHER); + } launcher.getStateManager().goToState(OVERVIEW); return true; } return false; } + + @Override + public boolean deferStartingActivity(int downHitTarget) { + return downHitTarget == HIT_TARGET_BACK || downHitTarget == HIT_TARGET_ROTATION; + } + + @Override + public Rect getOverviewWindowBounds(Rect homeBounds, RemoteAnimationTargetCompat target) { + return homeBounds; + } + + @Override + public boolean shouldMinimizeSplitScreen() { + return true; + } + + @Override + public boolean supportsLongSwipe(Launcher activity) { + return !activity.getDeviceProfile().isVerticalBarLayout(); + } + + @Override + public LongSwipeHelper getLongSwipeController(Launcher activity, + RemoteAnimationTargetSet targetSet) { + if (activity.getDeviceProfile().isVerticalBarLayout()) { + return null; + } + return new LongSwipeHelper(activity, targetSet); + } + + @Override + public AlphaProperty getAlphaProperty(Launcher activity) { + return activity.getDragLayer().getAlphaProperty(DragLayer.ALPHA_INDEX_SWIPE_UP); + } + + @Override + public int getContainerType() { + final Launcher launcher = getVisibleLaucher(); + return launcher != null ? launcher.getStateManager().getState().containerType + : LauncherLogProto.ContainerType.APP; + } } class FallbackActivityControllerHelper implements ActivityControlHelper<RecentsActivity> { - @Override - public void onQuickstepGestureStarted(RecentsActivity activity, boolean activityVisible) { - // TODO: + private final ComponentName mHomeComponent; + private final Handler mUiHandler = new Handler(Looper.getMainLooper()); + + public FallbackActivityControllerHelper(ComponentName homeComponent) { + mHomeComponent = homeComponent; } @Override - public boolean onQuickInteractionStart(RecentsActivity activity, boolean activityVisible) { - // Activity does not need any UI change for quickscrub. - return false; + public void onQuickInteractionStart(RecentsActivity activity, RunningTaskInfo taskInfo, + boolean activityVisible) { + QuickScrubController controller = activity.<RecentsView>getOverviewPanel() + .getQuickScrubController(); + + // TODO: match user is as well + boolean startingFromHome = !activityVisible && + (taskInfo == null || Objects.equals(taskInfo.topActivity, mHomeComponent)); + controller.onQuickScrubStart(startingFromHome, this); + if (activityVisible) { + mUiHandler.postDelayed(controller::onFinishedTransitionToQuickScrub, + OVERVIEW_TRANSITION_MS); + } } @Override - public void executeOnWindowAvailable(RecentsActivity activity, Runnable action) { - action.run(); + public float getTranslationYForQuickScrub(RecentsActivity activity) { + return 0; } @Override - public void executeOnNextDraw(RecentsActivity activity, TaskView targetView, - Runnable action) { - // TODO: - new Handler(Looper.getMainLooper()).post(action); + public void executeOnWindowAvailable(RecentsActivity activity, Runnable action) { + action.run(); } @Override @@ -296,7 +382,7 @@ public interface ActivityControlHelper<T extends BaseDraggingActivity> { @Override public int getSwipeUpDestinationAndLength(DeviceProfile dp, Context context, Rect outRect) { - FallbackRecentsView.getPageRect(dp, context, outRect); + LayoutUtils.calculateFallbackTaskSize(context, dp, outRect); if (dp.isVerticalBarLayout()) { Rect targetInsets = dp.getInsets(); int hotseatInset = dp.isSeascape() ? targetInsets.left : targetInsets.right; @@ -312,23 +398,43 @@ public interface ActivityControlHelper<T extends BaseDraggingActivity> { } @Override - public void prepareRecentsUI(RecentsActivity activity, boolean activityVisible) { - // TODO: - } + public AnimationFactory prepareRecentsUI(RecentsActivity activity, boolean activityVisible, + Consumer<AnimatorPlaybackController> callback) { + if (activityVisible) { + return (transitionLength) -> { }; + } - @Override - public AnimatorPlaybackController createControllerForVisibleActivity( - RecentsActivity activity) { - DeviceProfile dp = activity.getDeviceProfile(); - return createControllerForHiddenActivity(activity, Math.max(dp.widthPx, dp.heightPx)); - } + RecentsViewContainer rv = activity.getOverviewPanelContainer(); + rv.setContentAlpha(0); - @Override - public AnimatorPlaybackController createControllerForHiddenActivity( - RecentsActivity activity, int transitionLength) { - // We do not animate anything. Create a empty controller - AnimatorSet anim = new AnimatorSet(); - return AnimatorPlaybackController.wrap(anim, transitionLength * 2); + return new AnimationFactory() { + + boolean isAnimatingHome = false; + + @Override + public void onRemoteAnimationReceived(RemoteAnimationTargetSet targets) { + isAnimatingHome = targets != null && targets.isAnimatingHome(); + if (!isAnimatingHome) { + rv.setContentAlpha(1); + } + createActivityController(getSwipeUpDestinationAndLength( + activity.getDeviceProfile(), activity, new Rect())); + } + + @Override + public void createActivityController(long transitionLength) { + if (!isAnimatingHome) { + return; + } + + ObjectAnimator anim = ObjectAnimator + .ofFloat(rv, RecentsViewContainer.CONTENT_ALPHA, 0, 1); + anim.setDuration(transitionLength).setInterpolator(LINEAR); + AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.play(anim); + callback.accept(AnimatorPlaybackController.wrap(animatorSet, transitionLength)); + } + }; } @Override @@ -353,14 +459,6 @@ public interface ActivityControlHelper<T extends BaseDraggingActivity> { return new RecentsActivityTracker(onInitListener); } - @Override - public void startRecentsFromSwipe(Intent intent, AssistDataReceiver assistDataReceiver, - final RecentsAnimationListener remoteAnimationListener) { - // We can use the normal recents animation for swipe up - ActivityManagerWrapper.getInstance().startRecentsActivity( - intent, assistDataReceiver, remoteAnimationListener, null, null); - } - @Nullable @Override public RecentsActivity getCreatedActivity() { @@ -378,9 +476,48 @@ public interface ActivityControlHelper<T extends BaseDraggingActivity> { } @Override - public boolean switchToRecentsIfVisible() { + public boolean switchToRecentsIfVisible(boolean fromRecentsButton) { + return false; + } + + @Override + public boolean deferStartingActivity(int downHitTarget) { + // Always defer starting the activity when using fallback + return true; + } + + @Override + public Rect getOverviewWindowBounds(Rect homeBounds, RemoteAnimationTargetCompat target) { + // TODO: Remove this once b/77875376 is fixed + return target.sourceContainerBounds; + } + + @Override + public boolean shouldMinimizeSplitScreen() { + // TODO: Remove this once b/77875376 is fixed + return false; + } + + @Override + public boolean supportsLongSwipe(RecentsActivity activity) { return false; } + + @Override + public LongSwipeHelper getLongSwipeController(RecentsActivity activity, + RemoteAnimationTargetSet targetSet) { + return null; + } + + @Override + public AlphaProperty getAlphaProperty(RecentsActivity activity) { + return activity.getDragLayer().getAlphaProperty(0); + } + + @Override + public int getContainerType() { + return LauncherLogProto.ContainerType.SIDELOADED_LAUNCHER; + } } interface LayoutListener { @@ -401,4 +538,13 @@ public interface ActivityControlHelper<T extends BaseDraggingActivity> { void registerAndStartActivity(Intent intent, RemoteAnimationProvider animProvider, Context context, Handler handler, long duration); } + + interface AnimationFactory { + + default void onRemoteAnimationReceived(RemoteAnimationTargetSet targets) { } + + void createActivityController(long transitionLength); + + default void onTransitionCancelled() { } + } } diff --git a/quickstep/src/com/android/quickstep/LongSwipeHelper.java b/quickstep/src/com/android/quickstep/LongSwipeHelper.java new file mode 100644 index 000000000..fbcde8bba --- /dev/null +++ b/quickstep/src/com/android/quickstep/LongSwipeHelper.java @@ -0,0 +1,179 @@ +/* + * 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 static com.android.launcher3.LauncherAnimUtils.MIN_PROGRESS_TO_ALL_APPS; +import static com.android.launcher3.LauncherState.ALL_APPS; +import static com.android.launcher3.LauncherState.OVERVIEW; +import static com.android.launcher3.anim.Interpolators.DEACCEL; +import static com.android.quickstep.WindowTransformSwipeHandler.MAX_SWIPE_DURATION; +import static com.android.systemui.shared.recents.utilities.Utilities.getNextFrameNumber; +import static com.android.systemui.shared.recents.utilities.Utilities.getSurface; + +import android.animation.ValueAnimator; +import android.view.Surface; + +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherAnimUtils; +import com.android.launcher3.R; +import com.android.launcher3.allapps.AllAppsTransitionController; +import com.android.launcher3.allapps.DiscoveryBounce; +import com.android.launcher3.anim.AnimatorPlaybackController; +import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction; +import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch; +import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; +import com.android.launcher3.util.FlingBlockCheck; +import com.android.quickstep.util.RemoteAnimationTargetSet; +import com.android.quickstep.views.RecentsView; +import com.android.systemui.shared.system.RemoteAnimationTargetCompat; +import com.android.systemui.shared.system.TransactionCompat; + +/** + * Utility class to handle long swipe from an app. + * This assumes the presence of Launcher activity as long swipe is not supported on the + * fallback activity. + */ +public class LongSwipeHelper { + + private static final float SWIPE_DURATION_MULTIPLIER = + Math.min(1 / MIN_PROGRESS_TO_ALL_APPS, 1 / (1 - MIN_PROGRESS_TO_ALL_APPS)); + + private final Launcher mLauncher; + private final RemoteAnimationTargetSet mTargetSet; + + private float mMaxSwipeDistance = 1; + private AnimatorPlaybackController mAnimator; + private FlingBlockCheck mFlingBlockCheck = new FlingBlockCheck(); + + LongSwipeHelper(Launcher launcher, RemoteAnimationTargetSet targetSet) { + mLauncher = launcher; + mTargetSet = targetSet; + init(); + } + + private void init() { + setTargetAlpha(0, true); + mFlingBlockCheck.blockFling(); + + // Init animations + AllAppsTransitionController controller = mLauncher.getAllAppsController(); + // TODO: Scale it down so that we can reach all-apps in screen space + mMaxSwipeDistance = Math.max(1, controller.getProgress() * controller.getShiftRange()); + mAnimator = mLauncher.getStateManager() + .createAnimationToNewWorkspace(ALL_APPS, Math.round(2 * mMaxSwipeDistance)); + mAnimator.dispatchOnStart(); + } + + public void onMove(float displacement) { + mAnimator.setPlayFraction(displacement / mMaxSwipeDistance); + mFlingBlockCheck.onEvent(); + } + + public void destroy() { + // TODO: We can probably also hide the task view + setTargetAlpha(1, false); + + mLauncher.getStateManager().goToState(OVERVIEW, false); + } + + public void end(float velocity, boolean isFling, Runnable callback) { + long duration = MAX_SWIPE_DURATION; + + final float currentFraction = mAnimator.getProgressFraction(); + final boolean toAllApps; + float endProgress; + + boolean blockedFling = isFling && mFlingBlockCheck.isBlocked(); + if (blockedFling) { + isFling = false; + } + + if (!isFling) { + toAllApps = currentFraction > MIN_PROGRESS_TO_ALL_APPS; + endProgress = toAllApps ? 1 : 0; + + long expectedDuration = Math.abs(Math.round((endProgress - currentFraction) + * MAX_SWIPE_DURATION * SWIPE_DURATION_MULTIPLIER)); + duration = Math.min(MAX_SWIPE_DURATION, expectedDuration); + } else { + toAllApps = velocity < 0; + endProgress = toAllApps ? 1 : 0; + + float minFlingVelocity = mLauncher.getResources() + .getDimension(R.dimen.quickstep_fling_min_velocity); + if (Math.abs(velocity) > minFlingVelocity && mMaxSwipeDistance > 0) { + float distanceToTravel = (endProgress - currentFraction) * mMaxSwipeDistance; + + // we want the page's snap velocity to approximately match the velocity at + // which the user flings, so we scale the duration by a value near to the + // derivative of the scroll interpolator at zero, ie. 2. + long baseDuration = Math.round(1000 * Math.abs(distanceToTravel / velocity)); + duration = Math.min(MAX_SWIPE_DURATION, 2 * baseDuration); + } + } + + if (blockedFling && !toAllApps) { + duration *= LauncherAnimUtils.blockedFlingDurationFactor(0); + } + final boolean finalIsFling = isFling; + mAnimator.setEndAction(() -> onSwipeAnimationComplete(toAllApps, finalIsFling, callback)); + ValueAnimator animator = mAnimator.getAnimationPlayer(); + animator.setDuration(duration).setInterpolator(DEACCEL); + animator.setFloatValues(currentFraction, endProgress); + animator.start(); + } + + private void setTargetAlpha(float alpha, boolean defer) { + final Surface surface = getSurface(mLauncher.getDragLayer()); + final long frameNumber = defer && surface != null ? getNextFrameNumber(surface) : -1; + if (defer) { + if (frameNumber == -1) { + defer = false; + } else { + mLauncher.getDragLayer().invalidate(); + } + } + + TransactionCompat transaction = new TransactionCompat(); + for (RemoteAnimationTargetCompat app : mTargetSet.apps) { + if (!(app.isNotInRecents + || app.activityType == RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME)) { + transaction.setAlpha(app.leash, alpha); + if (defer) { + transaction.deferTransactionUntil(app.leash, surface, frameNumber); + } + } + } + transaction.setEarlyWakeup(); + transaction.apply(); + } + + private void onSwipeAnimationComplete(boolean toAllApps, boolean isFling, Runnable callback) { + mLauncher.getStateManager().goToState(toAllApps ? ALL_APPS : OVERVIEW, false); + if (!toAllApps) { + DiscoveryBounce.showForOverviewIfNeeded(mLauncher); + mLauncher.<RecentsView>getOverviewPanel().setSwipeDownShouldLaunchApp(true); + } + + mLauncher.getUserEventDispatcher().logStateChangeAction( + isFling ? Touch.FLING : Touch.SWIPE, Direction.UP, + ContainerType.NAVBAR, ContainerType.APP, + toAllApps ? ContainerType.ALLAPPS : ContainerType.TASKSWITCHER, + 0); + + callback.run(); + } +} diff --git a/quickstep/src/com/android/quickstep/MotionEventQueue.java b/quickstep/src/com/android/quickstep/MotionEventQueue.java index 538e23c3a..15f5aa524 100644 --- a/quickstep/src/com/android/quickstep/MotionEventQueue.java +++ b/quickstep/src/com/android/quickstep/MotionEventQueue.java @@ -163,7 +163,7 @@ public class MotionEventQueue { mConsumer.updateTouchTracking(INTERACTION_QUICK_SCRUB); break; case ACTION_QUICK_STEP: - mConsumer.onQuickStep(event.getX(), event.getY(), event.getEventTime()); + mConsumer.onQuickStep(event); break; default: Log.e(TAG, "Invalid virtual event: " + event.getAction()); diff --git a/quickstep/src/com/android/quickstep/MultiStateCallback.java b/quickstep/src/com/android/quickstep/MultiStateCallback.java index 7a741764f..bda3d06aa 100644 --- a/quickstep/src/com/android/quickstep/MultiStateCallback.java +++ b/quickstep/src/com/android/quickstep/MultiStateCallback.java @@ -59,4 +59,8 @@ public class MultiStateCallback { public int getState() { return mState; } + + public boolean hasStates(int stateMask) { + return (mState & stateMask) == stateMask; + } } diff --git a/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java b/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java index 4d695de90..23738fb25 100644 --- a/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java +++ b/quickstep/src/com/android/quickstep/OtherActivityTouchConsumer.java @@ -21,9 +21,10 @@ import static android.view.MotionEvent.ACTION_MOVE; import static android.view.MotionEvent.ACTION_POINTER_UP; import static android.view.MotionEvent.ACTION_UP; import static android.view.MotionEvent.INVALID_POINTER_ID; -import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_BACK; -import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_OVERVIEW; -import static com.android.systemui.shared.system.NavigationBarCompat.QUICK_STEP_DRAG_SLOP_PX; + +import static com.android.systemui.shared.system.ActivityManagerWrapper + .CLOSE_SYSTEM_WINDOWS_REASON_RECENTS; +import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING; import android.annotation.TargetApi; import android.app.ActivityManager.RunningTaskInfo; @@ -46,16 +47,17 @@ import android.view.WindowManager; import com.android.launcher3.MainThreadExecutor; import com.android.launcher3.util.TraceHelper; +import com.android.quickstep.util.RemoteAnimationTargetSet; import com.android.systemui.shared.system.ActivityManagerWrapper; import com.android.systemui.shared.system.AssistDataReceiver; import com.android.systemui.shared.system.BackgroundExecutor; +import com.android.systemui.shared.system.NavigationBarCompat; import com.android.systemui.shared.system.NavigationBarCompat.HitTarget; import com.android.systemui.shared.system.RecentsAnimationControllerCompat; import com.android.systemui.shared.system.RecentsAnimationListener; import com.android.systemui.shared.system.RemoteAnimationTargetCompat; import com.android.systemui.shared.system.WindowManagerWrapper; -import java.util.Arrays; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -66,8 +68,6 @@ import java.util.concurrent.TimeUnit; public class OtherActivityTouchConsumer extends ContextWrapper implements TouchConsumer { private static final long LAUNCHER_DRAW_TIMEOUT_MS = 150; - private static final int[] DEFERRED_HIT_TARGETS = false - ? new int[] {HIT_TARGET_BACK, HIT_TARGET_OVERVIEW} : new int[] {HIT_TARGET_BACK}; private final RunningTaskInfo mRunningTask; private final RecentsModel mRecentsModel; @@ -75,12 +75,15 @@ public class OtherActivityTouchConsumer extends ContextWrapper implements TouchC private final ActivityControlHelper mActivityControlHelper; private final MainThreadExecutor mMainThreadExecutor; private final Choreographer mBackgroundThreadChoreographer; + private final OverviewCallbacks mOverviewCallbacks; private final boolean mIsDeferredDownTarget; private final PointF mDownPos = new PointF(); private final PointF mLastPos = new PointF(); private int mActivePointerId = INVALID_POINTER_ID; private boolean mPassedInitialSlop; + // Used for non-deferred gestures to determine when to start dragging + private int mQuickStepDragSlop; private float mStartDisplacement; private WindowTransformSwipeHandler mInteractionHandler; private int mDisplayRotation; @@ -93,8 +96,10 @@ public class OtherActivityTouchConsumer extends ContextWrapper implements TouchC public OtherActivityTouchConsumer(Context base, RunningTaskInfo runningTaskInfo, RecentsModel recentsModel, Intent homeIntent, ActivityControlHelper activityControl, MainThreadExecutor mainThreadExecutor, Choreographer backgroundThreadChoreographer, - @HitTarget int downHitTarget, VelocityTracker velocityTracker) { + @HitTarget int downHitTarget, OverviewCallbacks overviewCallbacks, + VelocityTracker velocityTracker) { super(base); + mRunningTask = runningTaskInfo; mRecentsModel = recentsModel; mHomeIntent = homeIntent; @@ -102,7 +107,8 @@ public class OtherActivityTouchConsumer extends ContextWrapper implements TouchC mActivityControlHelper = activityControl; mMainThreadExecutor = mainThreadExecutor; mBackgroundThreadChoreographer = backgroundThreadChoreographer; - mIsDeferredDownTarget = Arrays.binarySearch(DEFERRED_HIT_TARGETS, downHitTarget) >= 0; + mIsDeferredDownTarget = activityControl.deferStartingActivity(downHitTarget); + mOverviewCallbacks = overviewCallbacks; } @Override @@ -122,6 +128,7 @@ public class OtherActivityTouchConsumer extends ContextWrapper implements TouchC mDownPos.set(ev.getX(), ev.getY()); mLastPos.set(mDownPos); mPassedInitialSlop = false; + mQuickStepDragSlop = NavigationBarCompat.getQuickStepDragSlopPx(); // Start the window animation on down to give more time for launcher to draw if the // user didn't start the gesture over the back button @@ -154,14 +161,14 @@ public class OtherActivityTouchConsumer extends ContextWrapper implements TouchC } mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex)); float displacement = getDisplacement(ev); - if (!mPassedInitialSlop && Math.abs(displacement) > QUICK_STEP_DRAG_SLOP_PX) { - mPassedInitialSlop = true; - mStartDisplacement = displacement; - - // If we deferred starting the window animation on touch down, then - // start tracking now - if (mIsDeferredDownTarget) { - startTouchTrackingForWindowAnimation(ev.getEventTime()); + if (!mPassedInitialSlop) { + if (!mIsDeferredDownTarget) { + // Normal gesture, ensure we pass the drag slop before we start tracking + // the gesture + if (Math.abs(displacement) > mQuickStepDragSlop) { + mPassedInitialSlop = true; + mStartDisplacement = displacement; + } } } @@ -186,6 +193,11 @@ public class OtherActivityTouchConsumer extends ContextWrapper implements TouchC if (mInteractionHandler == null) { return; } + + mOverviewCallbacks.closeAllWindows(); + ActivityManagerWrapper.getInstance().closeSystemWindows( + CLOSE_SYSTEM_WINDOWS_REASON_RECENTS); + // Notify the handler that the gesture has actually started mInteractionHandler.onGestureStarted(); } @@ -218,7 +230,8 @@ public class OtherActivityTouchConsumer extends ContextWrapper implements TouchC handler.initWhenReady(); TraceHelper.beginSection("RecentsController"); - Runnable startActivity = () -> mActivityControlHelper.startRecentsFromSwipe(mHomeIntent, + Runnable startActivity = () -> ActivityManagerWrapper.getInstance().startRecentsActivity( + mHomeIntent, new AssistDataReceiver() { @Override public void onHandleAssistData(Bundle bundle) { @@ -232,8 +245,9 @@ public class OtherActivityTouchConsumer extends ContextWrapper implements TouchC Rect minimizedHomeBounds) { if (mInteractionHandler == handler) { TraceHelper.partitionSection("RecentsController", "Received"); - handler.onRecentsAnimationStart(controller, apps, homeContentInsets, - minimizedHomeBounds); + handler.onRecentsAnimationStart(controller, + new RemoteAnimationTargetSet(apps, MODE_CLOSING), + homeContentInsets, minimizedHomeBounds); } else { TraceHelper.endSection("RecentsController", "Finishing no handler"); controller.finish(false /* toHome */); @@ -247,7 +261,7 @@ public class OtherActivityTouchConsumer extends ContextWrapper implements TouchC handler.onRecentsAnimationCanceled(); } } - }); + }, null, null); if (Looper.myLooper() != Looper.getMainLooper()) { startActivity.run(); @@ -305,6 +319,13 @@ public class OtherActivityTouchConsumer extends ContextWrapper implements TouchC @Override public void updateTouchTracking(int interactionType) { + if (!mPassedInitialSlop && mIsDeferredDownTarget && mInteractionHandler == null) { + // If we deferred starting the window animation on touch down, then + // start tracking now + startTouchTrackingForWindowAnimation(SystemClock.uptimeMillis()); + mPassedInitialSlop = true; + } + notifyGestureStarted(); if (mInteractionHandler != null) { mInteractionHandler.updateInteractionType(interactionType); @@ -332,7 +353,14 @@ public class OtherActivityTouchConsumer extends ContextWrapper implements TouchC } @Override - public void onQuickStep(float eventX, float eventY, long eventTime) { + public void onQuickStep(MotionEvent ev) { + if (mIsDeferredDownTarget) { + // Deferred gesture, start the animation and gesture tracking once we pass the actual + // touch slop + startTouchTrackingForWindowAnimation(ev.getEventTime()); + mPassedInitialSlop = true; + mStartDisplacement = getDisplacement(ev); + } notifyGestureStarted(); } diff --git a/quickstep/src/com/android/quickstep/OverviewCallbacks.java b/quickstep/src/com/android/quickstep/OverviewCallbacks.java new file mode 100644 index 000000000..ac4a40b98 --- /dev/null +++ b/quickstep/src/com/android/quickstep/OverviewCallbacks.java @@ -0,0 +1,45 @@ +/* + * 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.content.Context; + +import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.util.Preconditions; + +/** + * Callbacks related to overview/quicksteps. + */ +public class OverviewCallbacks { + + private static OverviewCallbacks sInstance; + + public static OverviewCallbacks get(Context context) { + Preconditions.assertUIThread(); + if (sInstance == null) { + sInstance = Utilities.getOverrideObject(OverviewCallbacks.class, + context.getApplicationContext(), R.string.overview_callbacks_class); + } + return sInstance; + } + + public void onInitOverviewTransition() { } + + public void onResetOverview() { } + + public void closeAllWindows() { } +} diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java index d76c49a09..7b2932383 100644 --- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java +++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java @@ -15,18 +15,32 @@ */ package com.android.quickstep; +import static android.content.Intent.ACTION_PACKAGE_ADDED; +import static android.content.Intent.ACTION_PACKAGE_CHANGED; +import static android.content.Intent.ACTION_PACKAGE_REMOVED; + import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN; +import static com.android.launcher3.anim.Interpolators.TOUCH_RESPONSE_INTERPOLATOR; +import static com.android.systemui.shared.system.ActivityManagerWrapper + .CLOSE_SYSTEM_WINDOWS_REASON_RECENTS; +import static com.android.systemui.shared.system.PackageManagerWrapper + .ACTION_PREFERRED_ACTIVITY_CHANGED; 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.AnimatorSet; import android.animation.ValueAnimator; import android.annotation.TargetApi; +import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.content.pm.ResolveInfo; import android.graphics.Rect; import android.os.Build; +import android.os.PatternMatcher; import android.os.SystemClock; import android.util.Log; import android.view.View; @@ -34,17 +48,26 @@ import android.view.ViewConfiguration; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.BaseDraggingActivity; +import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.MainThreadExecutor; -import com.android.launcher3.anim.AnimatorPlaybackController; +import com.android.launcher3.anim.AnimationSuccessListener; +import com.android.launcher3.logging.UserEventDispatcher; +import com.android.launcher3.userevent.nano.LauncherLogProto.Action; +import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; import com.android.quickstep.ActivityControlHelper.ActivityInitListener; +import com.android.quickstep.ActivityControlHelper.AnimationFactory; import com.android.quickstep.ActivityControlHelper.FallbackActivityControllerHelper; import com.android.quickstep.ActivityControlHelper.LauncherActivityControllerHelper; import com.android.quickstep.util.ClipAnimationHelper; -import com.android.quickstep.util.RemoteAnimationProvider; -import com.android.quickstep.util.SysuiEventLogger; +import com.android.quickstep.util.RemoteAnimationTargetSet; import com.android.quickstep.views.RecentsView; import com.android.systemui.shared.system.ActivityManagerWrapper; +import com.android.systemui.shared.system.LatencyTrackerCompat; +import com.android.systemui.shared.system.PackageManagerWrapper; import com.android.systemui.shared.system.RemoteAnimationTargetCompat; +import com.android.systemui.shared.system.TransactionCompat; + +import java.util.ArrayList; /** * Helper class to handle various atomic commands for switching between Overview. @@ -52,18 +75,33 @@ import com.android.systemui.shared.system.RemoteAnimationTargetCompat; @TargetApi(Build.VERSION_CODES.P) public class OverviewCommandHelper { - private static final long RECENTS_LAUNCH_DURATION = 200; + private static final long RECENTS_LAUNCH_DURATION = 250; private static final String TAG = "OverviewCommandHelper"; - private static final boolean DEBUG_START_FALLBACK_ACTIVITY = false; private final Context mContext; private final ActivityManagerWrapper mAM; private final RecentsModel mRecentsModel; private final MainThreadExecutor mMainThreadExecutor; + private final ComponentName mMyHomeComponent; + + private final BroadcastReceiver mUserPreferenceChangeReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + initOverviewTargets(); + } + }; + private final BroadcastReceiver mOtherHomeAppUpdateReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + initOverviewTargets(); + } + }; + private String mUpdateRegisteredPackage; - public final Intent homeIntent; - public final ComponentName launcher; + public Intent overviewIntent; + public ComponentName overviewComponent; + private ActivityControlHelper mActivityControlHelper; private long mLastToggleTime; @@ -73,22 +111,72 @@ public class OverviewCommandHelper { mMainThreadExecutor = new MainThreadExecutor(); mRecentsModel = RecentsModel.getInstance(mContext); - homeIntent = new Intent(Intent.ACTION_MAIN) + Intent myHomeIntent = new Intent(Intent.ACTION_MAIN) .addCategory(Intent.CATEGORY_HOME) - .setPackage(context.getPackageName()) - .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - ResolveInfo info = context.getPackageManager().resolveActivity(homeIntent, 0); + .setPackage(mContext.getPackageName()); + ResolveInfo info = context.getPackageManager().resolveActivity(myHomeIntent, 0); + mMyHomeComponent = new ComponentName(context.getPackageName(), info.activityInfo.name); + + mContext.registerReceiver(mUserPreferenceChangeReceiver, + new IntentFilter(ACTION_PREFERRED_ACTIVITY_CHANGED)); + initOverviewTargets(); + } - if (DEBUG_START_FALLBACK_ACTIVITY) { - launcher = new ComponentName(context, RecentsActivity.class); - homeIntent.addCategory(Intent.CATEGORY_DEFAULT) - .removeCategory(Intent.CATEGORY_HOME); + private void initOverviewTargets() { + ComponentName defaultHome = PackageManagerWrapper.getInstance() + .getHomeActivities(new ArrayList<>()); + + final String overviewIntentCategory; + if (defaultHome == null || mMyHomeComponent.equals(defaultHome)) { + // User default home is same as out home app. Use Overview integrated in Launcher. + overviewComponent = mMyHomeComponent; + mActivityControlHelper = new LauncherActivityControllerHelper(); + overviewIntentCategory = Intent.CATEGORY_HOME; + + if (mUpdateRegisteredPackage != null) { + // Remove any update listener as we don't care about other packages. + mContext.unregisterReceiver(mOtherHomeAppUpdateReceiver); + mUpdateRegisteredPackage = null; + } } else { - launcher = new ComponentName(context.getPackageName(), info.activityInfo.name); + // The default home app is a different launcher. Use the fallback Overview instead. + overviewComponent = new ComponentName(mContext, RecentsActivity.class); + mActivityControlHelper = new FallbackActivityControllerHelper(defaultHome); + overviewIntentCategory = Intent.CATEGORY_DEFAULT; + + // User's default home app can change as a result of package updates of this app (such + // as uninstalling the app or removing the "Launcher" feature in an update). + // Listen for package updates of this app (and remove any previously attached + // package listener). + if (!defaultHome.getPackageName().equals(mUpdateRegisteredPackage)) { + if (mUpdateRegisteredPackage != null) { + mContext.unregisterReceiver(mOtherHomeAppUpdateReceiver); + } + + mUpdateRegisteredPackage = defaultHome.getPackageName(); + IntentFilter updateReceiver = new IntentFilter(ACTION_PACKAGE_ADDED); + updateReceiver.addAction(ACTION_PACKAGE_CHANGED); + updateReceiver.addAction(ACTION_PACKAGE_REMOVED); + updateReceiver.addDataScheme("package"); + updateReceiver.addDataSchemeSpecificPart(mUpdateRegisteredPackage, + PatternMatcher.PATTERN_LITERAL); + mContext.registerReceiver(mOtherHomeAppUpdateReceiver, updateReceiver); + } } - // Clear the packageName as system can fail to dedupe it b/64108432 - homeIntent.setComponent(launcher).setPackage(null); + overviewIntent = new Intent(Intent.ACTION_MAIN) + .addCategory(overviewIntentCategory) + .setComponent(overviewComponent) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + } + + public void onDestroy() { + mContext.unregisterReceiver(mUserPreferenceChangeReceiver); + + if (mUpdateRegisteredPackage != null) { + mContext.unregisterReceiver(mOtherHomeAppUpdateReceiver); + mUpdateRegisteredPackage = null; + } } public void onOverviewToggle() { @@ -97,7 +185,7 @@ public class OverviewCommandHelper { return; } - mAM.closeSystemWindows("recentapps"); + mAM.closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS); mMainThreadExecutor.execute(new RecentsActivityCommand<>()); } @@ -105,25 +193,26 @@ public class OverviewCommandHelper { mMainThreadExecutor.execute(new ShowRecentsCommand()); } + public void onTip(int actionType, int viewType) { + mMainThreadExecutor.execute(new Runnable() { + @Override + public void run() { + UserEventDispatcher.newInstance(mContext, + new InvariantDeviceProfile(mContext).getDeviceProfile(mContext)) + .logActionTip(actionType, viewType); + } + }); + } + public ActivityControlHelper getActivityControlHelper() { - if (DEBUG_START_FALLBACK_ACTIVITY) { - return new FallbackActivityControllerHelper(); - } else { - return new LauncherActivityControllerHelper(); - } + return mActivityControlHelper; } private class ShowRecentsCommand extends RecentsActivityCommand { @Override protected boolean handleCommand(long elapsedTime) { - RecentsView recents = mHelper.getVisibleRecentsView(); - if (recents != null) { - recents.snapToTaskAfterNext(); - return true; - } else { - return false; - } + return mHelper.getVisibleRecentsView() != null; } } @@ -135,6 +224,9 @@ public class OverviewCommandHelper { private ActivityInitListener mListener; private T mActivity; + private RecentsView mRecentsView; + private final long mToggleClickedTime = SystemClock.uptimeMillis(); + private boolean mUserEventLogged; public RecentsActivityCommand() { mHelper = getActivityControlHelper(); @@ -152,12 +244,9 @@ public class OverviewCommandHelper { if (!handleCommand(elapsedTime)) { // Start overview - if (mHelper.switchToRecentsIfVisible()) { - SysuiEventLogger.writeDummyRecentsTransition(0); - // Do nothing - } else { + if (!mHelper.switchToRecentsIfVisible(true)) { mListener = mHelper.createActivityInitListener(this::onActivityReady); - mListener.registerAndStartActivity(homeIntent, this::createWindowAnimation, + mListener.registerAndStartActivity(overviewIntent, this::createWindowAnimation, mContext, mMainThreadExecutor.getHandler(), RECENTS_LAUNCH_DURATION); } } @@ -183,41 +272,60 @@ public class OverviewCommandHelper { private boolean onActivityReady(T activity, Boolean wasVisible) { activity.<RecentsView>getOverviewPanel().setCurrentTask(mRunningTaskId); AbstractFloatingView.closeAllOpenViews(activity, wasVisible); - mHelper.prepareRecentsUI(activity, wasVisible); + AnimationFactory factory = mHelper.prepareRecentsUI(activity, wasVisible, + (controller) -> { + controller.dispatchOnStart(); + ValueAnimator anim = controller.getAnimationPlayer() + .setDuration(RECENTS_LAUNCH_DURATION); + anim.setInterpolator(FAST_OUT_SLOW_IN); + anim.start(); + }); + factory.onRemoteAnimationReceived(null); if (wasVisible) { - AnimatorPlaybackController controller = - mHelper.createControllerForVisibleActivity(activity); - controller.dispatchOnStart(); - ValueAnimator anim = - controller.getAnimationPlayer().setDuration(RECENTS_LAUNCH_DURATION); - anim.setInterpolator(FAST_OUT_SLOW_IN); - anim.start(); + factory.createActivityController(RECENTS_LAUNCH_DURATION); } mActivity = activity; + mRecentsView = mActivity.getOverviewPanel(); + mRecentsView.setRunningTaskIconScaledDown(true /* isScaledDown */, false /* animate */); + if (!mUserEventLogged) { + activity.getUserEventDispatcher().logActionCommand(Action.Command.RECENTS_BUTTON, + mHelper.getContainerType(), ContainerType.TASKSWITCHER); + mUserEventLogged = true; + } return false; } private AnimatorSet createWindowAnimation(RemoteAnimationTargetCompat[] targetCompats) { + if (LatencyTrackerCompat.isEnabled(mContext)) { + LatencyTrackerCompat.logToggleRecents( + (int) (SystemClock.uptimeMillis() - mToggleClickedTime)); + } + if (mListener != null) { mListener.unregister(); } - RemoteAnimationProvider.showOpeningTarget(targetCompats); AnimatorSet anim = new AnimatorSet(); + anim.addListener(new AnimationSuccessListener() { + @Override + public void onAnimationSuccess(Animator animator) { + if (mRecentsView != null) { + mRecentsView.setRunningTaskIconScaledDown(false /* isScaledDown */, + true /* animate */); + } + } + }); if (mActivity == null) { Log.e(TAG, "Animation created, before activity"); anim.play(ValueAnimator.ofInt(0, 1).setDuration(100)); return anim; } - RemoteAnimationTargetCompat closingTarget = null; + RemoteAnimationTargetSet targetSet = + new RemoteAnimationTargetSet(targetCompats, MODE_CLOSING); + // Use the top closing app to determine the insets for the animation - for (RemoteAnimationTargetCompat target : targetCompats) { - if (target.mode == MODE_CLOSING) { - closingTarget = target; - break; - } - } - if (closingTarget == null) { + RemoteAnimationTargetCompat runningTaskTarget = targetSet.findTask(mRunningTaskId); + if (runningTaskTarget == null) { Log.e(TAG, "No closing app"); anim.play(ValueAnimator.ofInt(0, 1).setDuration(100)); return anim; @@ -232,19 +340,33 @@ public class OverviewCommandHelper { rootView.getLocationOnScreen(loc); Rect homeBounds = new Rect(loc[0], loc[1], loc[0] + rootView.getWidth(), loc[1] + rootView.getHeight()); - clipHelper.updateSource(homeBounds, closingTarget); + clipHelper.updateSource(homeBounds, runningTaskTarget); Rect targetRect = new Rect(); mHelper.getSwipeUpDestinationAndLength( mActivity.getDeviceProfile(), mActivity, targetRect); clipHelper.updateTargetRect(targetRect); - + clipHelper.prepareAnimation(false /* isOpening */); ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1); - valueAnimator.setDuration(RECENTS_LAUNCH_DURATION).setInterpolator(FAST_OUT_SLOW_IN); - valueAnimator.addUpdateListener((v) -> { - clipHelper.applyTransform(targetCompats, (float) v.getAnimatedValue()); - }); + valueAnimator.setDuration(RECENTS_LAUNCH_DURATION); + valueAnimator.setInterpolator(TOUCH_RESPONSE_INTERPOLATOR); + valueAnimator.addUpdateListener((v) -> + clipHelper.applyTransform(targetSet, (float) v.getAnimatedValue())); + + if (targetSet.isAnimatingHome()) { + // If we are animating home, fade in the opening targets + RemoteAnimationTargetSet openingSet = + new RemoteAnimationTargetSet(targetCompats, MODE_OPENING); + + TransactionCompat transaction = new TransactionCompat(); + valueAnimator.addUpdateListener((v) -> { + for (RemoteAnimationTargetCompat app : openingSet.apps) { + transaction.setAlpha(app.leash, (float) v.getAnimatedValue()); + } + transaction.apply(); + }); + } anim.play(valueAnimator); return anim; } diff --git a/quickstep/src/com/android/quickstep/OverviewInteractionState.java b/quickstep/src/com/android/quickstep/OverviewInteractionState.java index 22b175779..d60574676 100644 --- a/quickstep/src/com/android/quickstep/OverviewInteractionState.java +++ b/quickstep/src/com/android/quickstep/OverviewInteractionState.java @@ -15,23 +15,26 @@ */ package com.android.quickstep; -import static com.android.launcher3.Utilities.getPrefs; import static com.android.systemui.shared.system.NavigationBarCompat.FLAG_DISABLE_QUICK_SCRUB; import static com.android.systemui.shared.system.NavigationBarCompat.FLAG_DISABLE_SWIPE_UP; -import static com.android.systemui.shared.system.NavigationBarCompat.FLAG_HIDE_BACK_BUTTON; import static com.android.systemui.shared.system.NavigationBarCompat.FLAG_SHOW_OVERVIEW_BUTTON; +import static com.android.systemui.shared.system.SettingsCompat.SWIPE_UP_SETTING_NAME; +import android.content.ContentResolver; import android.content.Context; -import android.content.SharedPreferences; -import android.content.SharedPreferences.OnSharedPreferenceChangeListener; +import android.content.res.Resources; +import android.database.ContentObserver; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.RemoteException; +import android.provider.Settings; import android.support.annotation.WorkerThread; import android.util.Log; import com.android.launcher3.MainThreadExecutor; +import com.android.launcher3.Utilities; +import com.android.launcher3.allapps.DiscoveryBounce; import com.android.launcher3.util.UiThreadHelper; import com.android.systemui.shared.recents.ISystemUiProxy; @@ -42,15 +45,20 @@ import java.util.concurrent.ExecutionException; * * - FLAG_DISABLE_QUICK_SCRUB * - FLAG_DISABLE_SWIPE_UP - * - FLAG_HIDE_BACK_BUTTON * - FLAG_SHOW_OVERVIEW_BUTTON * * @see com.android.systemui.shared.system.NavigationBarCompat.InteractionType and associated flags. */ -public class OverviewInteractionState implements OnSharedPreferenceChangeListener { +public class OverviewInteractionState { private static final String TAG = "OverviewFlags"; + private static final String HAS_ENABLED_QUICKSTEP_ONCE = "launcher.has_enabled_quickstep_once"; + private static final String SWIPE_UP_SETTING_AVAILABLE_RES_NAME = + "config_swipe_up_gesture_setting_available"; + private static final String SWIPE_UP_ENABLED_DEFAULT_RES_NAME = + "config_swipe_up_gesture_default"; + // We do not need any synchronization for this variable as its only written on UI thread. private static OverviewInteractionState INSTANCE; @@ -70,42 +78,48 @@ public class OverviewInteractionState implements OnSharedPreferenceChangeListene return INSTANCE; } - public static final String KEY_SWIPE_UP_ENABLED = "pref_enable_quickstep"; - private static final int MSG_SET_PROXY = 200; - private static final int MSG_SET_BACK_BUTTON_VISIBLE = 201; + private static final int MSG_SET_BACK_BUTTON_ALPHA = 201; private static final int MSG_SET_SWIPE_UP_ENABLED = 202; + private final SwipeUpGestureEnabledSettingObserver mSwipeUpSettingObserver; + + private final Context mContext; private final Handler mUiHandler; private final Handler mBgHandler; // These are updated on the background thread private ISystemUiProxy mISystemUiProxy; - private boolean mBackButtonVisible = true; private boolean mSwipeUpEnabled = true; + private Runnable mOnSwipeUpSettingChangedListener; + private OverviewInteractionState(Context context) { + mContext = context; + + // Data posted to the uihandler will be sent to the bghandler. Data is sent to uihandler + // because of its high send frequency and data may be very different than the previous value + // For example, send back alpha on uihandler to avoid flickering when setting its visibility mUiHandler = new Handler(this::handleUiMessage); mBgHandler = new Handler(UiThreadHelper.getBackgroundLooper(), this::handleBgMessage); - SharedPreferences prefs = getPrefs(context); - prefs.registerOnSharedPreferenceChangeListener(this); - onSharedPreferenceChanged(prefs, KEY_SWIPE_UP_ENABLED); + if (getSystemBooleanRes(SWIPE_UP_SETTING_AVAILABLE_RES_NAME)) { + mSwipeUpSettingObserver = new SwipeUpGestureEnabledSettingObserver(mUiHandler, + context.getContentResolver()); + mSwipeUpSettingObserver.register(); + } else { + mSwipeUpSettingObserver = null; + mSwipeUpEnabled = getSystemBooleanRes(SWIPE_UP_ENABLED_DEFAULT_RES_NAME); + } } - @Override - public void onSharedPreferenceChanged(SharedPreferences prefs, String s) { - if (KEY_SWIPE_UP_ENABLED.equals(s)) { - mUiHandler.removeMessages(MSG_SET_SWIPE_UP_ENABLED); - boolean swipeUpEnabled = prefs.getBoolean(s, true); - mUiHandler.obtainMessage(MSG_SET_SWIPE_UP_ENABLED, - swipeUpEnabled ? 1 : 0, 0).sendToTarget(); - } + public boolean isSwipeUpGestureEnabled() { + return mSwipeUpEnabled; } - public void setBackButtonVisible(boolean visible) { - mUiHandler.removeMessages(MSG_SET_BACK_BUTTON_VISIBLE); - mUiHandler.obtainMessage(MSG_SET_BACK_BUTTON_VISIBLE, visible ? 1 : 0, 0) + public void setBackButtonAlpha(float alpha, boolean animate) { + mUiHandler.removeMessages(MSG_SET_BACK_BUTTON_ALPHA); + mUiHandler.obtainMessage(MSG_SET_BACK_BUTTON_ALPHA, animate ? 1 : 0, 0, alpha) .sendToTarget(); } @@ -114,7 +128,7 @@ public class OverviewInteractionState implements OnSharedPreferenceChangeListene } private boolean handleUiMessage(Message msg) { - mBgHandler.obtainMessage(msg.what, msg.arg1, msg.arg2).sendToTarget(); + mBgHandler.obtainMessage(msg.what, msg.arg1, msg.arg2, msg.obj).sendToTarget(); return true; } @@ -123,27 +137,34 @@ public class OverviewInteractionState implements OnSharedPreferenceChangeListene case MSG_SET_PROXY: mISystemUiProxy = (ISystemUiProxy) msg.obj; break; - case MSG_SET_BACK_BUTTON_VISIBLE: - mBackButtonVisible = msg.arg1 != 0; - break; + case MSG_SET_BACK_BUTTON_ALPHA: + applyBackButtonAlpha((float) msg.obj, msg.arg1 == 1); + return true; case MSG_SET_SWIPE_UP_ENABLED: mSwipeUpEnabled = msg.arg1 != 0; + resetHomeBounceSeenOnQuickstepEnabledFirstTime(); + + if (mOnSwipeUpSettingChangedListener != null) { + mOnSwipeUpSettingChangedListener.run(); + } break; } applyFlags(); return true; } + public void setOnSwipeUpSettingChangedListener(Runnable listener) { + mOnSwipeUpSettingChangedListener = listener; + } + @WorkerThread private void applyFlags() { if (mISystemUiProxy == null) { return; } - int flags; - if (mSwipeUpEnabled) { - flags = mBackButtonVisible ? 0 : FLAG_HIDE_BACK_BUTTON; - } else { + int flags = 0; + if (!mSwipeUpEnabled) { flags = FLAG_DISABLE_SWIPE_UP | FLAG_DISABLE_QUICK_SCRUB | FLAG_SHOW_OVERVIEW_BUTTON; } try { @@ -152,4 +173,69 @@ public class OverviewInteractionState implements OnSharedPreferenceChangeListene Log.w(TAG, "Unable to update overview interaction flags", e); } } + + @WorkerThread + private void applyBackButtonAlpha(float alpha, boolean animate) { + if (mISystemUiProxy == null) { + return; + } + try { + mISystemUiProxy.setBackButtonAlpha(alpha, animate); + } catch (RemoteException e) { + Log.w(TAG, "Unable to update overview back button alpha", e); + } + } + + private class SwipeUpGestureEnabledSettingObserver extends ContentObserver { + private Handler mHandler; + private ContentResolver mResolver; + private final int defaultValue; + + SwipeUpGestureEnabledSettingObserver(Handler handler, ContentResolver resolver) { + super(handler); + mHandler = handler; + mResolver = resolver; + defaultValue = getSystemBooleanRes(SWIPE_UP_ENABLED_DEFAULT_RES_NAME) ? 1 : 0; + } + + public void register() { + mResolver.registerContentObserver(Settings.Secure.getUriFor(SWIPE_UP_SETTING_NAME), + false, this); + mSwipeUpEnabled = getValue(); + resetHomeBounceSeenOnQuickstepEnabledFirstTime(); + } + + @Override + public void onChange(boolean selfChange) { + super.onChange(selfChange); + mHandler.removeMessages(MSG_SET_SWIPE_UP_ENABLED); + mHandler.obtainMessage(MSG_SET_SWIPE_UP_ENABLED, getValue() ? 1 : 0, 0).sendToTarget(); + } + + private boolean getValue() { + return Settings.Secure.getInt(mResolver, SWIPE_UP_SETTING_NAME, defaultValue) == 1; + } + } + + private boolean getSystemBooleanRes(String resName) { + Resources res = Resources.getSystem(); + int resId = res.getIdentifier(resName, "bool", "android"); + + if (resId != 0) { + return res.getBoolean(resId); + } else { + Log.e(TAG, "Failed to get system resource ID. Incompatible framework version?"); + return false; + } + } + + private void resetHomeBounceSeenOnQuickstepEnabledFirstTime() { + if (mSwipeUpEnabled && !Utilities.getPrefs(mContext).getBoolean( + HAS_ENABLED_QUICKSTEP_ONCE, true)) { + Utilities.getPrefs(mContext).edit() + .putBoolean(HAS_ENABLED_QUICKSTEP_ONCE, true) + .putBoolean(DiscoveryBounce.HOME_BOUNCE_SEEN, false) + .apply(); + } + } } diff --git a/quickstep/src/com/android/quickstep/QuickScrubController.java b/quickstep/src/com/android/quickstep/QuickScrubController.java index fd089b592..abb479dea 100644 --- a/quickstep/src/com/android/quickstep/QuickScrubController.java +++ b/quickstep/src/com/android/quickstep/QuickScrubController.java @@ -16,15 +16,18 @@ package com.android.quickstep; +import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN; + +import android.util.Log; import android.view.HapticFeedbackConstants; +import android.view.animation.Interpolator; import com.android.launcher3.Alarm; import com.android.launcher3.BaseActivity; import com.android.launcher3.OnAlarmListener; import com.android.launcher3.Utilities; +import com.android.launcher3.userevent.nano.LauncherLogProto; import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch; -import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; -import com.android.launcher3.userevent.nano.LauncherLogProto.ControlType; import com.android.quickstep.views.RecentsView; import com.android.quickstep.views.TaskView; @@ -36,15 +39,20 @@ import com.android.quickstep.views.TaskView; */ public class QuickScrubController implements OnAlarmListener { - public static final int QUICK_SCRUB_START_DURATION = 210; + public static final int QUICK_SCRUB_FROM_APP_START_DURATION = 240; + public static final int QUICK_SCRUB_FROM_HOME_START_DURATION = 150; + // We want the translation y to finish faster than the rest of the animation. + public static final float QUICK_SCRUB_TRANSLATION_Y_FACTOR = 5f / 6; + public static final Interpolator QUICK_SCRUB_START_INTERPOLATOR = FAST_OUT_SLOW_IN; /** * Snap to a new page when crossing these thresholds. The first and last auto-advance. */ private static final float[] QUICK_SCRUB_THRESHOLDS = new float[] { - 0.05f, 0.35f, 0.65f, 0.95f + 0.04f, 0.27f, 0.50f, 0.73f, 0.96f }; + private static final String TAG = "QuickScrubController"; private static final boolean ENABLE_AUTO_ADVANCE = true; private static final long AUTO_ADVANCE_DELAY = 500; private static final int QUICKSCRUB_SNAP_DURATION_PER_PAGE = 325; @@ -55,9 +63,12 @@ public class QuickScrubController implements OnAlarmListener { private final BaseActivity mActivity; private boolean mInQuickScrub; + private boolean mWaitingForTaskLaunch; private int mQuickScrubSection; private boolean mStartedFromHome; private boolean mFinishedTransitionToQuickScrub; + private Runnable mOnFinishedTransitionToQuickScrubRunnable; + private ActivityControlHelper mActivityControlHelper; public QuickScrubController(BaseActivity activity, RecentsView recentsView) { mActivity = activity; @@ -68,11 +79,13 @@ public class QuickScrubController implements OnAlarmListener { } } - public void onQuickScrubStart(boolean startingFromHome) { + public void onQuickScrubStart(boolean startingFromHome, ActivityControlHelper controlHelper) { + prepareQuickScrub(TAG); mInQuickScrub = true; mStartedFromHome = startingFromHome; mQuickScrubSection = 0; mFinishedTransitionToQuickScrub = false; + mActivityControlHelper = controlHelper; snapToNextTaskIfAvailable(); mActivity.getUserEventDispatcher().resetActionDurationMillis(); @@ -85,26 +98,61 @@ public class QuickScrubController implements OnAlarmListener { } int page = mRecentsView.getNextPage(); Runnable launchTaskRunnable = () -> { - TaskView taskView = ((TaskView) mRecentsView.getPageAt(page)); + TaskView taskView = mRecentsView.getPageAt(page); if (taskView != null) { - taskView.launchTask(true); + mWaitingForTaskLaunch = true; + taskView.launchTask(true, (result) -> { + if (!result) { + taskView.notifyTaskLaunchFailed(TAG); + breakOutOfQuickScrub(); + } else { + mActivity.getUserEventDispatcher().logTaskLaunchOrDismiss(Touch.DRAGDROP, + LauncherLogProto.Action.Direction.NONE, page, + TaskUtils.getComponentKeyForTask(taskView.getTask().key)); + } + mWaitingForTaskLaunch = false; + }, taskView.getHandler()); } else { - // Break out of quick scrub so user can interact with launcher. - mActivity.onBackPressed(); + breakOutOfQuickScrub(); } + mActivityControlHelper = null; }; int snapDuration = Math.abs(page - mRecentsView.getPageNearestToCenterOfScreen()) * QUICKSCRUB_END_SNAP_DURATION_PER_PAGE; - if (mRecentsView.snapToPage(page, snapDuration)) { + if (mRecentsView.getChildCount() > 0 && mRecentsView.snapToPage(page, snapDuration)) { // Settle on the page then launch it mRecentsView.setNextPageSwitchRunnable(launchTaskRunnable); } else { // No page move needed, just launch it - launchTaskRunnable.run(); + if (mFinishedTransitionToQuickScrub) { + launchTaskRunnable.run(); + } else { + mOnFinishedTransitionToQuickScrubRunnable = launchTaskRunnable; + } + } + } + + /** + * Initializes the UI for quick scrub, returns true if success. + */ + public boolean prepareQuickScrub(String tag) { + if (mWaitingForTaskLaunch || mInQuickScrub) { + Log.d(tag, "Waiting for last scrub to finish, will skip this interaction"); + return false; + } + mOnFinishedTransitionToQuickScrubRunnable = null; + mRecentsView.setNextPageSwitchRunnable(null); + return true; + } + + /** + * Attempts to go to normal overview or back to home, so UI doesn't prevent user interaction. + */ + private void breakOutOfQuickScrub() { + if (mRecentsView.getChildCount() == 0 || mActivityControlHelper == null + || !mActivityControlHelper.switchToRecentsIfVisible(false)) { + mActivity.onBackPressed(); } - mActivity.getUserEventDispatcher().logActionOnControl(Touch.DRAGDROP, - ControlType.QUICK_SCRUB_BUTTON, null, mStartedFromHome ? - ContainerType.WORKSPACE : ContainerType.APP); } public void onQuickScrubProgress(float progress) { @@ -135,20 +183,39 @@ public class QuickScrubController implements OnAlarmListener { public void onFinishedTransitionToQuickScrub() { mFinishedTransitionToQuickScrub = true; + Runnable action = mOnFinishedTransitionToQuickScrubRunnable; + // Clear the runnable before executing it, to prevent potential recursion. + mOnFinishedTransitionToQuickScrubRunnable = null; + if (action != null) { + action.run(); + } } public void snapToNextTaskIfAvailable() { - if (!mStartedFromHome && mInQuickScrub && mRecentsView.getChildCount() > 0) { - mRecentsView.snapToPage(mRecentsView.getNextPage() + 1, QUICK_SCRUB_START_DURATION); + if (mInQuickScrub && mRecentsView.getChildCount() > 0) { + int duration = mStartedFromHome ? QUICK_SCRUB_FROM_HOME_START_DURATION + : QUICK_SCRUB_FROM_APP_START_DURATION; + int pageToGoTo = mStartedFromHome ? 0 : mRecentsView.getNextPage() + 1; + goToPageWithHaptic(pageToGoTo, duration, true /* forceHaptic */, + QUICK_SCRUB_START_INTERPOLATOR); } } private void goToPageWithHaptic(int pageToGoTo) { + goToPageWithHaptic(pageToGoTo, -1 /* overrideDuration */, false /* forceHaptic */, null); + } + + private void goToPageWithHaptic(int pageToGoTo, int overrideDuration, boolean forceHaptic, + Interpolator interpolator) { pageToGoTo = Utilities.boundToRange(pageToGoTo, 0, mRecentsView.getPageCount() - 1); - if (pageToGoTo != mRecentsView.getNextPage()) { - int duration = Math.abs(pageToGoTo - mRecentsView.getNextPage()) + boolean snappingToPage = pageToGoTo != mRecentsView.getNextPage(); + if (snappingToPage) { + int duration = overrideDuration > -1 ? overrideDuration + : Math.abs(pageToGoTo - mRecentsView.getNextPage()) * QUICKSCRUB_SNAP_DURATION_PER_PAGE; - mRecentsView.snapToPage(pageToGoTo, duration); + mRecentsView.snapToPage(pageToGoTo, duration, interpolator); + } + if (snappingToPage || forceHaptic) { mRecentsView.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); } @@ -157,6 +224,12 @@ public class QuickScrubController implements OnAlarmListener { @Override public void onAlarm(Alarm alarm) { int currPage = mRecentsView.getNextPage(); + boolean recentsVisible = mActivityControlHelper != null + && mActivityControlHelper.getVisibleRecentsView() != null; + if (!recentsVisible) { + Log.w(TAG, "Failed to auto advance; recents not visible"); + return; + } if (mQuickScrubSection == QUICK_SCRUB_THRESHOLDS.length && currPage < mRecentsView.getPageCount() - 1) { goToPageWithHaptic(currPage + 1); diff --git a/quickstep/src/com/android/quickstep/RecentsActivity.java b/quickstep/src/com/android/quickstep/RecentsActivity.java index cf60fdff6..b472d611a 100644 --- a/quickstep/src/com/android/quickstep/RecentsActivity.java +++ b/quickstep/src/com/android/quickstep/RecentsActivity.java @@ -15,15 +15,35 @@ */ package com.android.quickstep; +import static android.content.pm.ActivityInfo.CONFIG_ORIENTATION; +import static android.content.pm.ActivityInfo.CONFIG_SCREEN_SIZE; + +import static com.android.launcher3.LauncherAppTransitionManagerImpl.RECENTS_LAUNCH_DURATION; +import static com.android.launcher3.LauncherAppTransitionManagerImpl.STATUS_BAR_TRANSITION_DURATION; +import static com.android.quickstep.TaskUtils.getRecentsWindowAnimator; +import static com.android.quickstep.TaskUtils.taskIsATargetWithMode; +import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; import android.app.ActivityOptions; +import android.content.Intent; +import android.content.res.Configuration; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; import android.view.View; +import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.BaseDraggingActivity; +import com.android.launcher3.DeviceProfile; import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.ItemInfo; +import com.android.launcher3.LauncherAnimationRunner; import com.android.launcher3.LauncherAppState; import com.android.launcher3.R; +import com.android.launcher3.anim.Interpolators; import com.android.launcher3.badge.BadgeInfo; import com.android.launcher3.uioverrides.UiFactory; import com.android.launcher3.util.SystemUiController; @@ -31,29 +51,40 @@ import com.android.launcher3.util.Themes; import com.android.launcher3.views.BaseDragLayer; import com.android.quickstep.fallback.FallbackRecentsView; import com.android.quickstep.fallback.RecentsRootView; +import com.android.quickstep.util.ClipAnimationHelper; +import com.android.quickstep.views.RecentsViewContainer; +import com.android.quickstep.views.TaskView; +import com.android.systemui.shared.system.ActivityOptionsCompat; +import com.android.systemui.shared.system.RemoteAnimationAdapterCompat; +import com.android.systemui.shared.system.RemoteAnimationRunnerCompat; +import com.android.systemui.shared.system.RemoteAnimationTargetCompat; + +import java.io.FileDescriptor; +import java.io.PrintWriter; /** * A simple activity to show the recently launched tasks */ public class RecentsActivity extends BaseDraggingActivity { + private Handler mUiHandler = new Handler(Looper.getMainLooper()); private RecentsRootView mRecentsRootView; private FallbackRecentsView mFallbackRecentsView; + private RecentsViewContainer mOverviewPanelContainer; + + private Configuration mOldConfig; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - // In case we are reusing IDP, create a copy so that we dont conflict with Launcher - // activity. - LauncherAppState appState = LauncherAppState.getInstanceNoCreate(); - setDeviceProfile(appState != null - ? appState.getInvariantDeviceProfile().getDeviceProfile(this).copy(this) - : new InvariantDeviceProfile(this).getDeviceProfile(this)); + mOldConfig = new Configuration(getResources().getConfiguration()); + initDeviceProfile(); setContentView(R.layout.fallback_recents_activity); mRecentsRootView = findViewById(R.id.drag_layer); mFallbackRecentsView = findViewById(R.id.overview_panel); + mOverviewPanelContainer = findViewById(R.id.overview_panel_container); mRecentsRootView.setup(); @@ -63,6 +94,64 @@ public class RecentsActivity extends BaseDraggingActivity { } @Override + public void onConfigurationChanged(Configuration newConfig) { + int diff = newConfig.diff(mOldConfig); + if ((diff & (CONFIG_ORIENTATION | CONFIG_SCREEN_SIZE)) != 0) { + onHandleConfigChanged(); + } + mOldConfig.setTo(newConfig); + super.onConfigurationChanged(newConfig); + } + + @Override + public void onMultiWindowModeChanged(boolean isInMultiWindowMode, Configuration newConfig) { + onHandleConfigChanged(); + super.onMultiWindowModeChanged(isInMultiWindowMode, newConfig); + } + + public void onRootViewSizeChanged() { + if (isInMultiWindowModeCompat()) { + onHandleConfigChanged(); + } + } + + private void onHandleConfigChanged() { + mUserEventDispatcher = null; + initDeviceProfile(); + + AbstractFloatingView.closeOpenViews(this, true, + AbstractFloatingView.TYPE_ALL & ~AbstractFloatingView.TYPE_REBIND_SAFE); + dispatchDeviceProfileChanged(); + + mRecentsRootView.setup(); + reapplyUi(); + } + + @Override + protected void reapplyUi() { + mRecentsRootView.dispatchInsets(); + } + + private void initDeviceProfile() { + // In case we are reusing IDP, create a copy so that we dont conflict with Launcher + // activity. + LauncherAppState appState = LauncherAppState.getInstanceNoCreate(); + if (isInMultiWindowModeCompat()) { + InvariantDeviceProfile idp = appState == null + ? new InvariantDeviceProfile(this) : appState.getInvariantDeviceProfile(); + DeviceProfile dp = idp.getDeviceProfile(this); + mDeviceProfile = mRecentsRootView == null ? dp.copy(this) + : dp.getMultiWindowProfile(this, mRecentsRootView.getLastKnownSize()); + } else { + // If we are reusing the Invariant device profile, make a copy. + mDeviceProfile = appState == null + ? new InvariantDeviceProfile(this).getDeviceProfile(this) + : appState.getInvariantDeviceProfile().getDeviceProfile(this).copy(this); + } + onDeviceProfileInitiated(); + } + + @Override public BaseDragLayer getDragLayer() { return mRecentsRootView; } @@ -77,14 +166,62 @@ public class RecentsActivity extends BaseDraggingActivity { return (T) mFallbackRecentsView; } + public RecentsViewContainer getOverviewPanelContainer() { + return mOverviewPanelContainer; + } + @Override public BadgeInfo getBadgeInfoForItem(ItemInfo info) { return null; } @Override - public ActivityOptions getActivityLaunchOptions(View v, boolean useDefaultLaunchOptions) { - return null; + public ActivityOptions getActivityLaunchOptions(final View v, boolean useDefaultLaunchOptions) { + if (useDefaultLaunchOptions || !(v instanceof TaskView)) { + return null; + } + + final TaskView taskView = (TaskView) v; + RemoteAnimationRunnerCompat runner = new LauncherAnimationRunner(mUiHandler, + true /* startAtFrontOfQueue */) { + + @Override + public void onCreateAnimation(RemoteAnimationTargetCompat[] targetCompats, + AnimationResult result) { + result.setAnimation(composeRecentsLaunchAnimator(taskView, targetCompats)); + } + }; + return ActivityOptionsCompat.makeRemoteAnimation(new RemoteAnimationAdapterCompat( + runner, RECENTS_LAUNCH_DURATION, + RECENTS_LAUNCH_DURATION - STATUS_BAR_TRANSITION_DURATION)); + } + + /** + * Composes the animations for a launch from the recents list if possible. + */ + private AnimatorSet composeRecentsLaunchAnimator(TaskView taskView, + RemoteAnimationTargetCompat[] targets) { + AnimatorSet target = new AnimatorSet(); + boolean activityClosing = taskIsATargetWithMode(targets, getTaskId(), MODE_CLOSING); + ClipAnimationHelper helper = new ClipAnimationHelper(); + target.play(getRecentsWindowAnimator(taskView, !activityClosing, targets, helper) + .setDuration(RECENTS_LAUNCH_DURATION)); + + // Found a visible recents task that matches the opening app, lets launch the app from there + if (activityClosing) { + Animator adjacentAnimation = mFallbackRecentsView + .createAdjacentPageAnimForTaskLaunch(taskView, helper); + adjacentAnimation.setInterpolator(Interpolators.TOUCH_RESPONSE_INTERPOLATOR); + adjacentAnimation.setDuration(RECENTS_LAUNCH_DURATION); + adjacentAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mFallbackRecentsView.resetTaskVisuals(); + } + }); + target.play(adjacentAnimation); + } + return target; } @Override @@ -92,8 +229,20 @@ public class RecentsActivity extends BaseDraggingActivity { @Override protected void onStart() { + // Set the alpha to 1 before calling super, as it may get set back to 0 due to + // onActivityStart callback. + mFallbackRecentsView.setContentAlpha(1); super.onStart(); UiFactory.onStart(this); + mFallbackRecentsView.resetTaskVisuals(); + } + + @Override + protected void onStop() { + super.onStop(); + + // Workaround for b/78520668, explicitly trim memory once UI is hidden + onTrimMemory(TRIM_MEMORY_UI_HIDDEN); } @Override @@ -103,8 +252,33 @@ public class RecentsActivity extends BaseDraggingActivity { } @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + RecentsActivityTracker.onRecentsActivityNewIntent(this); + } + + @Override protected void onDestroy() { super.onDestroy(); RecentsActivityTracker.onRecentsActivityDestroy(this); } + + @Override + public void onBackPressed() { + // TODO: Launch the task we came from + startHome(); + } + + public void startHome() { + startActivity(new Intent(Intent.ACTION_MAIN) + .addCategory(Intent.CATEGORY_HOME) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); + } + + @Override + public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { + super.dump(prefix, fd, writer, args); + writer.println(prefix + "Misc:"); + dumpMisc(writer); + } } diff --git a/quickstep/src/com/android/quickstep/RecentsActivityTracker.java b/quickstep/src/com/android/quickstep/RecentsActivityTracker.java index 77f0e7a53..fb6090e2a 100644 --- a/quickstep/src/com/android/quickstep/RecentsActivityTracker.java +++ b/quickstep/src/com/android/quickstep/RecentsActivityTracker.java @@ -22,6 +22,7 @@ import android.os.Build; import android.os.Bundle; import android.os.Handler; +import com.android.launcher3.MainThreadExecutor; import com.android.quickstep.ActivityControlHelper.ActivityInitListener; import com.android.quickstep.util.RemoteAnimationProvider; @@ -34,9 +35,8 @@ import java.util.function.BiPredicate; @TargetApi(Build.VERSION_CODES.P) public class RecentsActivityTracker implements ActivityInitListener { - private static final Object LOCK = new Object(); - private static WeakReference<RecentsActivityTracker> sTracker = new WeakReference<>(null); private static WeakReference<RecentsActivity> sCurrentActivity = new WeakReference<>(null); + private static final Scheduler sScheduler = new Scheduler(); private final BiPredicate<RecentsActivity, Boolean> mOnInitListener; @@ -46,42 +46,20 @@ public class RecentsActivityTracker implements ActivityInitListener { @Override public void register() { - synchronized (LOCK) { - sTracker = new WeakReference<>(this); - } + sScheduler.schedule(this); } @Override public void unregister() { - synchronized (LOCK) { - if (sTracker.get() == this) { - sTracker.clear(); - } - } + sScheduler.clearReference(this); } - public static void onRecentsActivityCreate(RecentsActivity activity) { - synchronized (LOCK) { - RecentsActivityTracker tracker = sTracker.get(); - if (tracker != null && tracker.mOnInitListener.test(activity, false)) { - sTracker.clear(); - } - sCurrentActivity = new WeakReference<>(activity); - } - } - - public static void onRecentsActivityDestroy(RecentsActivity activity) { - synchronized (LOCK) { - if (sCurrentActivity.get() == activity) { - sCurrentActivity.clear(); - } - } + private boolean init(RecentsActivity activity, boolean visible) { + return mOnInitListener.test(activity, visible); } public static RecentsActivity getCurrentActivity() { - synchronized (LOCK) { - return sCurrentActivity.get(); - } + return sCurrentActivity.get(); } @Override @@ -92,4 +70,62 @@ public class RecentsActivityTracker implements ActivityInitListener { Bundle options = animProvider.toActivityOptions(handler, duration).toBundle(); context.startActivity(intent, options); } + + public static void onRecentsActivityCreate(RecentsActivity activity) { + sCurrentActivity = new WeakReference<>(activity); + sScheduler.initIfPending(activity, false); + } + + + public static void onRecentsActivityNewIntent(RecentsActivity activity) { + sScheduler.initIfPending(activity, activity.isStarted()); + } + + public static void onRecentsActivityDestroy(RecentsActivity activity) { + if (sCurrentActivity.get() == activity) { + sCurrentActivity.clear(); + } + } + + + private static class Scheduler implements Runnable { + + private WeakReference<RecentsActivityTracker> mPendingTracker = new WeakReference<>(null); + private MainThreadExecutor mMainThreadExecutor; + + public synchronized void schedule(RecentsActivityTracker tracker) { + mPendingTracker = new WeakReference<>(tracker); + if (mMainThreadExecutor == null) { + mMainThreadExecutor = new MainThreadExecutor(); + } + mMainThreadExecutor.execute(this); + } + + @Override + public void run() { + RecentsActivity activity = sCurrentActivity.get(); + if (activity != null) { + initIfPending(activity, activity.isStarted()); + } + } + + public synchronized boolean initIfPending(RecentsActivity activity, boolean alreadyOnHome) { + RecentsActivityTracker tracker = mPendingTracker.get(); + if (tracker != null) { + if (!tracker.init(activity, alreadyOnHome)) { + mPendingTracker.clear(); + } + return true; + } + return false; + } + + public synchronized boolean clearReference(RecentsActivityTracker tracker) { + if (mPendingTracker.get() == tracker) { + mPendingTracker.clear(); + return true; + } + return false; + } + } } diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationInterpolator.java b/quickstep/src/com/android/quickstep/RecentsAnimationInterpolator.java deleted file mode 100644 index fdeb0c170..000000000 --- a/quickstep/src/com/android/quickstep/RecentsAnimationInterpolator.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * 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 mInitialTaskScale; - private float mInitialTaskTranslationX; - private float mFinalTaskScale; - private Rect mScaledTask; - private Rect mTargetTask; - private Rect mSrcWindow; - - public RecentsAnimationInterpolator(Rect window, Rect insets, Rect task, Rect taskInsets, - float taskScale, float taskTranslationX) { - mWindow = window; - mInsets = insets; - mTask = task; - mTaskInsets = taskInsets; - mInsetWindow = new Rect(window); - Utilities.insetRect(mInsetWindow, insets); - - mThumbnail = new Rect(task); - Utilities.insetRect(mThumbnail, taskInsets); - mInitialTaskScale = taskScale; - mInitialTaskTranslationX = taskTranslationX; - mFinalTaskScale = (float) mInsetWindow.width() / mThumbnail.width(); - mScaledTask = new Rect(task); - Utilities.scaleRectAboutCenter(mScaledTask, mFinalTaskScale); - Rect finalScaledTaskInsets = new Rect(taskInsets); - Utilities.scaleRect(finalScaledTaskInsets, mFinalTaskScale); - mTargetTask = new Rect(mInsetWindow); - mTargetTask.offsetTo(window.left + insets.left - finalScaledTaskInsets.left, - window.top + insets.top - finalScaledTaskInsets.top); - - float initialWinScale = 1f / mFinalTaskScale; - 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, - mInitialTaskScale, mFinalTaskScale); - mTmpTaskWindowBounds.taskX = Utilities.mapRange(t, - mInitialTaskTranslationX, mTargetTask.left - mScaledTask.left); - mTmpTaskWindowBounds.taskY = Utilities.mapRange(t, - 0, mTargetTask.top - mScaledTask.top); - - float taskScale = Utilities.mapRange(t, 1, mFinalTaskScale); - mTmpTaskWindowBounds.winScale = taskScale / mFinalTaskScale; - 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/RecentsAnimationWrapper.java b/quickstep/src/com/android/quickstep/RecentsAnimationWrapper.java index 12f8d52b8..30b10b0ea 100644 --- a/quickstep/src/com/android/quickstep/RecentsAnimationWrapper.java +++ b/quickstep/src/com/android/quickstep/RecentsAnimationWrapper.java @@ -15,28 +15,33 @@ */ package com.android.quickstep; +import com.android.launcher3.util.LooperExecutor; import com.android.launcher3.util.TraceHelper; -import com.android.systemui.shared.system.BackgroundExecutor; +import com.android.launcher3.util.UiThreadHelper; +import com.android.quickstep.util.RemoteAnimationTargetSet; import com.android.systemui.shared.system.RecentsAnimationControllerCompat; -import com.android.systemui.shared.system.RemoteAnimationTargetCompat; +import java.util.concurrent.ExecutorService; /** * Wrapper around RecentsAnimationController to help with some synchronization */ public class RecentsAnimationWrapper { - public RecentsAnimationControllerCompat controller; - public RemoteAnimationTargetCompat[] targets; + public RemoteAnimationTargetSet targetSet; + private RecentsAnimationControllerCompat mController; private boolean mInputConsumerEnabled = false; private boolean mBehindSystemBars = true; private boolean mSplitScreenMinimized = false; + private final ExecutorService mExecutorService = + new LooperExecutor(UiThreadHelper.getBackgroundLooper()); + public synchronized void setController( - RecentsAnimationControllerCompat controller, RemoteAnimationTargetCompat[] targets) { + RecentsAnimationControllerCompat controller, RemoteAnimationTargetSet targetSet) { TraceHelper.partitionSection("RecentsController", "Set controller " + controller); - this.controller = controller; - this.targets = targets; + this.mController = controller; + this.targetSet = targetSet; if (mInputConsumerEnabled) { enableInputConsumer(); @@ -48,16 +53,16 @@ public class RecentsAnimationWrapper { * on the background thread. */ public void finish(boolean toHome, Runnable onFinishComplete) { - BackgroundExecutor.get().submit(() -> { - synchronized (this) { - TraceHelper.endSection("RecentsController", - "Finish " + controller + ", toHome=" + toHome); - if (controller != null) { - controller.setInputConsumerEnabled(false); - controller.finish(toHome); - if (onFinishComplete != null) { - onFinishComplete.run(); - } + mExecutorService.submit(() -> { + RecentsAnimationControllerCompat controller = mController; + mController = null; + TraceHelper.endSection("RecentsController", + "Finish " + controller + ", toHome=" + toHome); + if (controller != null) { + controller.setInputConsumerEnabled(false); + controller.finish(toHome); + if (onFinishComplete != null) { + onFinishComplete.run(); } } }); @@ -66,13 +71,12 @@ public class RecentsAnimationWrapper { public void enableInputConsumer() { mInputConsumerEnabled = true; if (mInputConsumerEnabled) { - BackgroundExecutor.get().submit(() -> { - synchronized (this) { - TraceHelper.partitionSection("RecentsController", - "Enabling consumer on " + controller); - if (controller != null) { - controller.setInputConsumerEnabled(true); - } + mExecutorService.submit(() -> { + RecentsAnimationControllerCompat controller = mController; + TraceHelper.partitionSection("RecentsController", + "Enabling consumer on " + controller); + if (controller != null) { + controller.setInputConsumerEnabled(true); } }); } @@ -83,13 +87,12 @@ public class RecentsAnimationWrapper { return; } mBehindSystemBars = behindSystemBars; - BackgroundExecutor.get().submit(() -> { - synchronized (this) { - TraceHelper.partitionSection("RecentsController", - "Setting behind system bars on " + controller); - if (controller != null) { - controller.setAnimationTargetsBehindSystemBars(behindSystemBars); - } + mExecutorService.submit(() -> { + RecentsAnimationControllerCompat controller = mController; + TraceHelper.partitionSection("RecentsController", + "Setting behind system bars on " + controller); + if (controller != null) { + controller.setAnimationTargetsBehindSystemBars(behindSystemBars); } }); } @@ -105,14 +108,28 @@ public class RecentsAnimationWrapper { return; } mSplitScreenMinimized = minimized; - BackgroundExecutor.get().submit(() -> { - synchronized (this) { - TraceHelper.partitionSection("RecentsController", - "Setting minimize dock on " + controller); - if (controller != null) { - controller.setSplitScreenMinimized(minimized); - } + mExecutorService.submit(() -> { + RecentsAnimationControllerCompat controller = mController; + TraceHelper.partitionSection("RecentsController", + "Setting minimize dock on " + controller); + if (controller != null) { + controller.setSplitScreenMinimized(minimized); + } + }); + } + + public void hideCurrentInputMethod() { + mExecutorService.submit(() -> { + RecentsAnimationControllerCompat controller = mController; + TraceHelper.partitionSection("RecentsController", + "Hiding currentinput method on " + controller); + if (controller != null) { + controller.hideCurrentInputMethod(); } }); } + + public RecentsAnimationControllerCompat getController() { + return mController; + } } diff --git a/quickstep/src/com/android/quickstep/RecentsModel.java b/quickstep/src/com/android/quickstep/RecentsModel.java index 4652f2d65..9c2c8b313 100644 --- a/quickstep/src/com/android/quickstep/RecentsModel.java +++ b/quickstep/src/com/android/quickstep/RecentsModel.java @@ -15,6 +15,8 @@ */ package com.android.quickstep; +import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId; + import android.annotation.TargetApi; import android.app.ActivityManager; import android.content.ComponentCallbacks2; @@ -26,8 +28,10 @@ import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Bundle; import android.os.Looper; +import android.os.RemoteException; import android.os.UserHandle; import android.support.annotation.WorkerThread; +import android.util.Log; import android.util.LruCache; import android.util.SparseArray; import android.view.accessibility.AccessibilityManager; @@ -85,7 +89,8 @@ public class RecentsModel extends TaskStackChangeListener { private int mTaskChangeId; private ISystemUiProxy mSystemUiProxy; private boolean mClearAssistCacheOnStackChange = true; - private final boolean mPreloadTasksInBackground; + private final boolean mIsLowRamDevice; + private boolean mPreloadTasksInBackground; private final AccessibilityManager mAccessibilityManager; private RecentsModel(Context context) { @@ -93,7 +98,7 @@ public class RecentsModel extends TaskStackChangeListener { ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); - mPreloadTasksInBackground = !activityManager.isLowRamDevice(); + mIsLowRamDevice = activityManager.isLowRamDevice(); mMainThreadExecutor = new MainThreadExecutor(); Resources res = context.getResources(); @@ -158,6 +163,10 @@ public class RecentsModel extends TaskStackChangeListener { return requestId; } + public void setPreloadTasksInBackground(boolean preloadTasksInBackground) { + mPreloadTasksInBackground = preloadTasksInBackground && !mIsLowRamDevice; + } + @Override public void onActivityPinned(String packageName, int userId, int taskId, int stackId) { mTaskChangeId++; @@ -183,7 +192,7 @@ public class RecentsModel extends TaskStackChangeListener { @Override public void onTaskStackChangedBackground() { int userId = UserHandle.myUserId(); - if (!mPreloadTasksInBackground || !checkCurrentUserId(userId, false /* debug */)) { + if (!mPreloadTasksInBackground || !checkCurrentOrManagedUserId(userId, mContext)) { // TODO: Only register this for the current user return; } @@ -234,6 +243,19 @@ public class RecentsModel extends TaskStackChangeListener { mRecentsTaskLoader.onTrimMemory(level); } + public void onOverviewShown(boolean fromHome, String tag) { + if (mSystemUiProxy == null) { + return; + } + try { + mSystemUiProxy.onOverviewShown(fromHome); + } catch (RemoteException e) { + Log.w(tag, + "Failed to notify SysUI of overview shown from " + (fromHome ? "home" : "app") + + ": ", e); + } + } + @WorkerThread public void preloadAssistData(int taskId, Bundle data) { mMainThreadExecutor.execute(() -> { diff --git a/quickstep/src/com/android/quickstep/TaskSystemShortcut.java b/quickstep/src/com/android/quickstep/TaskSystemShortcut.java index 2ebf2525f..228af8e90 100644 --- a/quickstep/src/com/android/quickstep/TaskSystemShortcut.java +++ b/quickstep/src/com/android/quickstep/TaskSystemShortcut.java @@ -27,7 +27,6 @@ import android.os.RemoteException; import android.os.UserHandle; import android.util.Log; import android.view.View; -import android.view.ViewTreeObserver.OnPreDrawListener; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.BaseDraggingActivity; @@ -101,13 +100,9 @@ public class TaskSystemShortcut<T extends SystemShortcut> extends SystemShortcut } } - public static class SplitScreen extends TaskSystemShortcut implements OnPreDrawListener, - DeviceProfile.OnDeviceProfileChangeListener, View.OnLayoutChangeListener { + public static class SplitScreen extends TaskSystemShortcut { private Handler mHandler; - private RecentsView mRecentsView; - private TaskView mTaskView; - private BaseDraggingActivity mActivity; public SplitScreen() { super(R.drawable.ic_split_screen, R.string.recent_task_option_split_screen); @@ -125,16 +120,45 @@ public class TaskSystemShortcut<T extends SystemShortcut> extends SystemShortcut if (!task.isDockable) { return null; } - mActivity = activity; - mRecentsView = activity.getOverviewPanel(); - mTaskView = taskView; + final RecentsView recentsView = activity.getOverviewPanel(); + final TaskThumbnailView thumbnailView = taskView.getThumbnail(); return (v -> { + final View.OnLayoutChangeListener onLayoutChangeListener = + new View.OnLayoutChangeListener() { + @Override + public void onLayoutChange(View v, int l, int t, int r, int b, + int oldL, int oldT, int oldR, int oldB) { + taskView.getRootView().removeOnLayoutChangeListener(this); + recentsView.removeIgnoreResetTask(taskView); + + // Start animating in the side pages once launcher has been resized + recentsView.dismissTask(taskView, false, false); + } + }; + + final DeviceProfile.OnDeviceProfileChangeListener onDeviceProfileChangeListener = + new DeviceProfile.OnDeviceProfileChangeListener() { + @Override + public void onDeviceProfileChanged(DeviceProfile dp) { + activity.removeOnDeviceProfileChangeListener(this); + if (dp.isMultiWindowMode) { + taskView.getRootView().addOnLayoutChangeListener( + onLayoutChangeListener); + } + } + }; + AbstractFloatingView.closeOpenViews(activity, true, AbstractFloatingView.TYPE_ALL & ~AbstractFloatingView.TYPE_REBIND_SAFE); + final int navBarPosition = WindowManagerWrapper.getInstance().getNavBarPosition(); + if (navBarPosition == WindowManagerWrapper.NAV_BAR_POS_INVALID) { + return; + } + boolean dockTopOrLeft = navBarPosition != WindowManagerWrapper.NAV_BAR_POS_LEFT; if (ActivityManagerWrapper.getInstance().startActivityFromRecents(taskId, - ActivityOptionsCompat.makeSplitScreenOptions(true))) { + ActivityOptionsCompat.makeSplitScreenOptions(dockTopOrLeft))) { ISystemUiProxy sysUiProxy = RecentsModel.getInstance(activity).getSystemUiProxy(); try { sysUiProxy.onSplitScreenInvoked(); @@ -145,15 +169,14 @@ public class TaskSystemShortcut<T extends SystemShortcut> extends SystemShortcut // Add a device profile change listener to kick off animating the side tasks // once we enter multiwindow mode and relayout - activity.addOnDeviceProfileChangeListener(this); + activity.addOnDeviceProfileChangeListener(onDeviceProfileChangeListener); final Runnable animStartedListener = () -> { // Hide the task view and wait for the window to be resized // TODO: Consider animating in launcher and do an in-place start activity // afterwards - mRecentsView.addIgnoreResetTask(mTaskView); - mTaskView.setAlpha(0f); - mTaskView.getViewTreeObserver().addOnPreDrawListener(SplitScreen.this); + recentsView.addIgnoreResetTask(taskView); + taskView.setAlpha(0f); }; final int[] position = new int[2]; @@ -179,35 +202,12 @@ public class TaskSystemShortcut<T extends SystemShortcut> extends SystemShortcut } }); } - - @Override - public boolean onPreDraw() { - mTaskView.getViewTreeObserver().removeOnPreDrawListener(this); - WindowManagerWrapper.getInstance().endProlongedAnimations(); - return true; - } - - @Override - public void onDeviceProfileChanged(DeviceProfile dp) { - mActivity.removeOnDeviceProfileChangeListener(this); - if (dp.isMultiWindowMode) { - mTaskView.getRootView().addOnLayoutChangeListener(this); - } - } - - @Override - public void onLayoutChange(View v, int l, int t, int r, int b, - int oldL, int oldT, int oldR, int oldB) { - mTaskView.getRootView().removeOnLayoutChangeListener(this); - mRecentsView.removeIgnoreResetTask(mTaskView); - - // Start animating in the side pages once launcher has been resized - mRecentsView.dismissTask(mTaskView, false, false); - } } public static class Pin extends TaskSystemShortcut { + private static final String TAG = Pin.class.getSimpleName(); + private Handler mHandler; public Pin() { @@ -237,6 +237,8 @@ public class TaskSystemShortcut<T extends SystemShortcut> extends SystemShortcut } catch (RemoteException e) { Log.w(TAG, "Failed to start screen pinning: ", e); } + } else { + taskView.notifyTaskLaunchFailed(TAG); } }; taskView.launchTask(true, resultCallback, mHandler); diff --git a/quickstep/src/com/android/quickstep/TaskUtils.java b/quickstep/src/com/android/quickstep/TaskUtils.java index 5bf1d07db..2b0c98f93 100644 --- a/quickstep/src/com/android/quickstep/TaskUtils.java +++ b/quickstep/src/com/android/quickstep/TaskUtils.java @@ -16,25 +16,49 @@ package com.android.quickstep; +import static com.android.launcher3.anim.Interpolators.LINEAR; +import static com.android.launcher3.anim.Interpolators.TOUCH_RESPONSE_INTERPOLATOR; +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_OPENING; + +import android.animation.ValueAnimator; +import android.content.ComponentName; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; +import android.graphics.RectF; import android.os.UserHandle; import android.util.Log; +import android.view.Surface; +import android.view.View; +import com.android.launcher3.BaseDraggingActivity; +import com.android.launcher3.ItemInfo; +import com.android.launcher3.Utilities; import com.android.launcher3.compat.LauncherAppsCompat; import com.android.launcher3.compat.UserManagerCompat; import com.android.launcher3.util.ComponentKey; +import com.android.quickstep.util.ClipAnimationHelper; +import com.android.quickstep.util.MultiValueUpdateListener; +import com.android.quickstep.util.RemoteAnimationTargetSet; +import com.android.quickstep.views.RecentsView; +import com.android.quickstep.views.TaskView; import com.android.systemui.shared.recents.model.Task; +import com.android.systemui.shared.system.RemoteAnimationTargetCompat; + +import java.util.List; /** * Contains helpful methods for retrieving data from {@link Task}s. - * TODO: remove this once we switch to getting the icon and label from IconCache. */ public class TaskUtils { private static final String TAG = "TaskUtils"; + /** + * TODO: remove this once we switch to getting the icon and label from IconCache. + */ public static CharSequence getTitle(Context context, Task task) { LauncherAppsCompat launcherAppsCompat = LauncherAppsCompat.getInstance(context); UserManagerCompat userManagerCompat = UserManagerCompat.getInstance(context); @@ -53,4 +77,150 @@ public class TaskUtils { public static ComponentKey getComponentKeyForTask(Task.TaskKey taskKey) { return new ComponentKey(taskKey.getComponent(), UserHandle.of(taskKey.userId)); } + + + /** + * 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. + */ + public static TaskView findTaskViewToLaunch( + BaseDraggingActivity activity, View v, RemoteAnimationTargetCompat[] targets) { + if (v instanceof TaskView) { + return (TaskView) v; + } + RecentsView recentsView = activity.getOverviewPanel(); + + // 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. + if (v.getTag() instanceof ItemInfo) { + ItemInfo itemInfo = (ItemInfo) v.getTag(); + ComponentName componentName = itemInfo.getTargetComponent(); + int userId = itemInfo.user.getIdentifier(); + if (componentName != null) { + for (int i = 0; i < recentsView.getChildCount(); i++) { + TaskView taskView = recentsView.getPageAt(i); + if (recentsView.isTaskViewVisible(taskView)) { + Task.TaskKey key = taskView.getTask().key; + if (componentName.equals(key.getComponent()) && userId == key.userId) { + return taskView; + } + } + } + } + } + + if (targets == null) { + 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; + } + return taskView; + } + + /** + * @return Animator that controls the window of the opening targets for the recents launch + * animation. + */ + public static ValueAnimator getRecentsWindowAnimator(TaskView v, boolean skipViewChanges, + RemoteAnimationTargetCompat[] targets, final ClipAnimationHelper inOutHelper) { + final ValueAnimator appAnimator = ValueAnimator.ofFloat(0, 1); + appAnimator.setInterpolator(TOUCH_RESPONSE_INTERPOLATOR); + appAnimator.addUpdateListener(new MultiValueUpdateListener() { + + // Defer fading out the view until after the app window gets faded in + final FloatProp mViewAlpha = new FloatProp(1f, 0f, 75, 75, LINEAR); + final FloatProp mTaskAlpha = new FloatProp(0f, 1f, 0, 75, LINEAR); + + final RemoteAnimationTargetSet mTargetSet; + + final RectF mThumbnailRect; + private Surface mSurface; + private long mFrameNumber; + + { + mTargetSet = new RemoteAnimationTargetSet(targets, MODE_OPENING); + inOutHelper.setTaskTransformCallback((t, app) -> { + t.setAlpha(app.leash, mTaskAlpha.value); + + if (!skipViewChanges) { + t.deferTransactionUntil(app.leash, mSurface, mFrameNumber); + } + }); + + inOutHelper.prepareAnimation(true /* isOpening */); + inOutHelper.fromTaskThumbnailView(v.getThumbnail(), (RecentsView) v.getParent(), + mTargetSet.apps.length == 0 ? null : mTargetSet.apps[0]); + + mThumbnailRect = new RectF(inOutHelper.getTargetRect()); + mThumbnailRect.offset(-v.getTranslationX(), -v.getTranslationY()); + Utilities.scaleRectFAboutCenter(mThumbnailRect, 1 / v.getScaleX()); + } + + @Override + public void onUpdate(float percent) { + mSurface = getSurface(v); + mFrameNumber = mSurface != null ? getNextFrameNumber(mSurface) : -1; + if (mFrameNumber == -1) { + // Booo, not cool! Our surface got destroyed, so no reason to animate anything. + Log.w(TAG, "Failed to animate, surface got destroyed."); + return; + } + + RectF taskBounds = inOutHelper.applyTransform(mTargetSet, 1 - percent); + if (!skipViewChanges) { + float scale = taskBounds.width() / mThumbnailRect.width(); + v.setScaleX(scale); + v.setScaleY(scale); + v.setTranslationX(taskBounds.centerX() - mThumbnailRect.centerX()); + v.setTranslationY(taskBounds.centerY() - mThumbnailRect.centerY()); + v.setAlpha(mViewAlpha.value); + } + } + }); + return appAnimator; + } + + public static boolean taskIsATargetWithMode(RemoteAnimationTargetCompat[] targets, + int taskId, int mode) { + for (RemoteAnimationTargetCompat target : targets) { + if (target.mode == mode && target.taskId == taskId) { + return true; + } + } + return false; + } + + public static boolean checkCurrentOrManagedUserId(int currentUserId, Context context) { + if (currentUserId == UserHandle.myUserId()) { + return true; + } + List<UserHandle> allUsers = UserManagerCompat.getInstance(context).getUserProfiles(); + for (int i = allUsers.size() - 1; i >= 0; i--) { + if (currentUserId == allUsers.get(i).getIdentifier()) { + return true; + } + } + return false; + } } diff --git a/quickstep/src/com/android/quickstep/TouchConsumer.java b/quickstep/src/com/android/quickstep/TouchConsumer.java index 1290ec337..aa844d80d 100644 --- a/quickstep/src/com/android/quickstep/TouchConsumer.java +++ b/quickstep/src/com/android/quickstep/TouchConsumer.java @@ -46,7 +46,7 @@ public interface TouchConsumer extends Consumer<MotionEvent> { default void onQuickScrubProgress(float progress) { } - default void onQuickStep(float eventX, float eventY, long eventTime) { } + default void onQuickStep(MotionEvent ev) { } /** * Called on the binder thread to allow the consumer to process the motion event before it is diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java index 84d8983e0..aecb66c77 100644 --- a/quickstep/src/com/android/quickstep/TouchInteractionService.java +++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java @@ -21,7 +21,7 @@ import static android.view.MotionEvent.ACTION_MOVE; import static android.view.MotionEvent.ACTION_POINTER_DOWN; import static android.view.MotionEvent.ACTION_POINTER_UP; import static android.view.MotionEvent.ACTION_UP; - +import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS; import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_NONE; import android.annotation.TargetApi; @@ -39,13 +39,12 @@ import android.util.SparseArray; import android.view.Choreographer; import android.view.MotionEvent; import android.view.VelocityTracker; -import android.view.View; import android.view.ViewConfiguration; import com.android.launcher3.BaseDraggingActivity; import com.android.launcher3.MainThreadExecutor; -import com.android.launcher3.R; import com.android.launcher3.util.TraceHelper; +import com.android.launcher3.views.BaseDragLayer; import com.android.quickstep.views.RecentsView; import com.android.systemui.shared.recents.IOverviewProxy; import com.android.systemui.shared.recents.ISystemUiProxy; @@ -99,8 +98,6 @@ public class TouchInteractionService extends Service { public void onBind(ISystemUiProxy iSystemUiProxy) { mISystemUiProxy = iSystemUiProxy; mRecentsModel.setSystemUiProxy(mISystemUiProxy); - RemoteRunnable.executeSafely(() -> mISystemUiProxy.setRecentsOnboardingText( - getResources().getString(R.string.recents_swipe_up_onboarding))); mOverviewInteractionState.setSystemUiProxy(mISystemUiProxy); } @@ -150,6 +147,11 @@ public class TouchInteractionService extends Service { TraceHelper.endSection("SysUiBinder", "onQuickStep"); } + + @Override + public void onTip(int actionType, int viewType) { + mOverviewCommandHelper.onTip(actionType, viewType); + } }; private final TouchConsumer mNoOpTouchConsumer = (ev) -> {}; @@ -167,6 +169,7 @@ public class TouchInteractionService extends Service { private ISystemUiProxy mISystemUiProxy; private OverviewCommandHelper mOverviewCommandHelper; private OverviewInteractionState mOverviewInteractionState; + private OverviewCallbacks mOverviewCallbacks; private Choreographer mMainThreadChoreographer; private Choreographer mBackgroundThreadChoreographer; @@ -176,11 +179,13 @@ public class TouchInteractionService extends Service { super.onCreate(); mAM = ActivityManagerWrapper.getInstance(); mRecentsModel = RecentsModel.getInstance(this); + mRecentsModel.setPreloadTasksInBackground(true); mMainThreadExecutor = new MainThreadExecutor(); mOverviewCommandHelper = new OverviewCommandHelper(this); mMainThreadChoreographer = Choreographer.getInstance(); mEventQueue = new MotionEventQueue(mMainThreadChoreographer, mNoOpTouchConsumer); mOverviewInteractionState = OverviewInteractionState.getInstance(this); + mOverviewCallbacks = OverviewCallbacks.get(this); sConnected = true; @@ -191,6 +196,7 @@ public class TouchInteractionService extends Service { @Override public void onDestroy() { + mOverviewCommandHelper.onDestroy(); sConnected = false; super.onDestroy(); } @@ -222,16 +228,17 @@ public class TouchInteractionService extends Service { if (runningTaskInfo == null && !forceToLauncher) { return mNoOpTouchConsumer; } else if (forceToLauncher || - runningTaskInfo.topActivity.equals(mOverviewCommandHelper.launcher)) { + runningTaskInfo.topActivity.equals(mOverviewCommandHelper.overviewComponent)) { return getOverviewConsumer(); } else { if (tracker == null) { tracker = VelocityTracker.obtain(); } return new OtherActivityTouchConsumer(this, runningTaskInfo, mRecentsModel, - mOverviewCommandHelper.homeIntent, + mOverviewCommandHelper.overviewIntent, mOverviewCommandHelper.getActivityControlHelper(), mMainThreadExecutor, - mBackgroundThreadChoreographer, downHitTarget, tracker); + mBackgroundThreadChoreographer, downHitTarget, mOverviewCallbacks, + tracker); } } @@ -249,7 +256,7 @@ public class TouchInteractionService extends Service { private final ActivityControlHelper<T> mActivityHelper; private final T mActivity; - private final View mTarget; + private final BaseDragLayer mTarget; private final int[] mLocationOnScreen = new int[2]; private final PointF mDownPos = new PointF(); private final int mTouchSlop; @@ -257,7 +264,6 @@ public class TouchInteractionService extends Service { private boolean mTrackingStarted = false; private boolean mInvalidated = false; - private boolean mHadWindowFocusOnDown; private float mLastProgress = 0; private boolean mStartPending = false; @@ -282,8 +288,7 @@ public class TouchInteractionService extends Service { if (action == ACTION_DOWN) { mTrackingStarted = false; mDownPos.set(ev.getX(), ev.getY()); - mHadWindowFocusOnDown = mTarget.hasWindowFocus(); - } else if (!mTrackingStarted && mHadWindowFocusOnDown) { + } else if (!mTrackingStarted) { switch (action) { case ACTION_POINTER_UP: case ACTION_POINTER_DOWN: @@ -294,7 +299,6 @@ public class TouchInteractionService extends Service { case ACTION_MOVE: { float displacement = ev.getY() - mDownPos.y; if (Math.abs(displacement) >= mTouchSlop) { - mTrackingStarted = true; mTarget.getLocationOnScreen(mLocationOnScreen); // Send a down event only when mTouchSlop is crossed. @@ -302,6 +306,7 @@ public class TouchInteractionService extends Service { down.setAction(ACTION_DOWN); sendEvent(down); down.recycle(); + mTrackingStarted = true; } } } @@ -320,22 +325,45 @@ public class TouchInteractionService extends Service { int flags = ev.getEdgeFlags(); ev.setEdgeFlags(flags | EDGE_NAV_BAR); ev.offsetLocation(-mLocationOnScreen[0], -mLocationOnScreen[1]); - mTarget.dispatchTouchEvent(ev); + if (!mTrackingStarted) { + mTarget.onInterceptTouchEvent(ev); + } + mTarget.onTouchEvent(ev); ev.offsetLocation(mLocationOnScreen[0], mLocationOnScreen[1]); ev.setEdgeFlags(flags); } @Override + public void onQuickStep(MotionEvent ev) { + if (mInvalidated) { + return; + } + OverviewCallbacks.get(mActivity).closeAllWindows(); + ActivityManagerWrapper.getInstance() + .closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS); + } + + @Override public void updateTouchTracking(int interactionType) { if (mInvalidated) { return; } if (interactionType == INTERACTION_QUICK_SCRUB) { - mStartPending = true; + if (!mQuickScrubController.prepareQuickScrub(TAG)) { + mInvalidated = true; + return; + } + OverviewCallbacks.get(mActivity).closeAllWindows(); + ActivityManagerWrapper.getInstance() + .closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS); + mStartPending = true; Runnable action = () -> { - mQuickScrubController.onQuickScrubStart( - mActivityHelper.onQuickInteractionStart(mActivity, true)); + if (!mQuickScrubController.prepareQuickScrub(TAG)) { + mInvalidated = true; + return; + } + mActivityHelper.onQuickInteractionStart(mActivity, null, true); mQuickScrubController.onQuickScrubProgress(mLastProgress); mStartPending = false; @@ -343,7 +371,6 @@ public class TouchInteractionService extends Service { mQuickScrubController.onQuickScrubEnd(); mEndPending = false; } - }; mActivityHelper.executeOnWindowAvailable(mActivity, action); @@ -365,7 +392,7 @@ public class TouchInteractionService extends Service { @Override public void onQuickScrubProgress(float progress) { mLastProgress = progress; - if (mInvalidated || mEndPending) { + if (mInvalidated || mStartPending) { return; } mQuickScrubController.onQuickScrubProgress(progress); diff --git a/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java b/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java index d4c35e0b1..84b217648 100644 --- a/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java +++ b/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java @@ -15,54 +15,57 @@ */ package com.android.quickstep; -import static com.android.launcher3.anim.Interpolators.ACCEL_2; +import static com.android.launcher3.BaseActivity.INVISIBLE_BY_STATE_HANDLER; +import static com.android.launcher3.Utilities.postAsyncCallback; +import static com.android.launcher3.anim.Interpolators.DEACCEL; import static com.android.launcher3.anim.Interpolators.LINEAR; -import static com.android.quickstep.QuickScrubController.QUICK_SCRUB_START_DURATION; +import static com.android.quickstep.QuickScrubController.QUICK_SCRUB_FROM_APP_START_DURATION; import static com.android.quickstep.TouchConsumer.INTERACTION_NORMAL; import static com.android.quickstep.TouchConsumer.INTERACTION_QUICK_SCRUB; -import static com.android.systemui.shared.recents.utilities.Utilities - .postAtFrontOfQueueAsynchronously; -import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING; import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.annotation.TargetApi; import android.app.ActivityManager.RunningTaskInfo; import android.content.Context; -import android.content.res.Resources; +import android.graphics.Canvas; import android.graphics.Point; import android.graphics.Rect; import android.os.Build; import android.os.Handler; import android.os.Looper; import android.os.SystemClock; +import android.support.annotation.AnyThread; import android.support.annotation.UiThread; import android.support.annotation.WorkerThread; import android.util.Log; import android.view.View; import android.view.ViewTreeObserver.OnDrawListener; +import android.view.WindowManager; import android.view.animation.Interpolator; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.BaseDraggingActivity; import com.android.launcher3.DeviceProfile; +import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.LauncherAppState; -import com.android.launcher3.MainThreadExecutor; import com.android.launcher3.R; -import com.android.launcher3.Utilities; import com.android.launcher3.anim.AnimationSuccessListener; import com.android.launcher3.anim.AnimatorPlaybackController; -import com.android.launcher3.anim.Interpolators; import com.android.launcher3.logging.UserEventDispatcher; import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction; import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch; import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; +import com.android.launcher3.util.MultiValueAlpha; +import com.android.launcher3.util.MultiValueAlpha.AlphaProperty; import com.android.launcher3.util.TraceHelper; import com.android.quickstep.ActivityControlHelper.ActivityInitListener; +import com.android.quickstep.ActivityControlHelper.AnimationFactory; import com.android.quickstep.ActivityControlHelper.LayoutListener; import com.android.quickstep.TouchConsumer.InteractionType; import com.android.quickstep.util.ClipAnimationHelper; -import com.android.quickstep.util.SysuiEventLogger; +import com.android.quickstep.util.RemoteAnimationTargetSet; import com.android.quickstep.views.RecentsView; import com.android.quickstep.views.TaskView; import com.android.systemui.shared.recents.model.ThumbnailData; @@ -70,7 +73,7 @@ import com.android.systemui.shared.system.InputConsumerController; import com.android.systemui.shared.system.LatencyTrackerCompat; import com.android.systemui.shared.system.RecentsAnimationControllerCompat; import com.android.systemui.shared.system.RemoteAnimationTargetCompat; -import com.android.systemui.shared.system.TransactionCompat; +import com.android.systemui.shared.system.WindowCallbacksCompat; import com.android.systemui.shared.system.WindowManagerWrapper; import java.util.StringJoiner; @@ -96,16 +99,30 @@ public class WindowTransformSwipeHandler<T extends BaseDraggingActivity> { private static final int STATE_HANDLER_INVALIDATED = 1 << 7; private static final int STATE_GESTURE_STARTED = 1 << 8; private static final int STATE_GESTURE_CANCELLED = 1 << 9; + private static final int STATE_GESTURE_COMPLETED = 1 << 10; // States for quick switch/scrub - private static final int STATE_SWITCH_TO_SCREENSHOT_COMPLETE = 1 << 10; - private static final int STATE_QUICK_SCRUB_START = 1 << 11; - private static final int STATE_QUICK_SCRUB_END = 1 << 12; + private static final int STATE_CURRENT_TASK_FINISHED = 1 << 11; + private static final int STATE_QUICK_SCRUB_START = 1 << 12; + private static final int STATE_QUICK_SCRUB_END = 1 << 13; + + private static final int STATE_CAPTURE_SCREENSHOT = 1 << 14; + private static final int STATE_SCREENSHOT_CAPTURED = 1 << 15; + + private static final int STATE_RESUME_LAST_TASK = 1 << 16; private static final int LAUNCHER_UI_STATES = STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_DRAWN | STATE_ACTIVITY_MULTIPLIER_COMPLETE | STATE_LAUNCHER_STARTED; + private static final int LONG_SWIPE_ENTER_STATE = + STATE_ACTIVITY_MULTIPLIER_COMPLETE | STATE_LAUNCHER_STARTED + | STATE_APP_CONTROLLER_RECEIVED; + + private static final int LONG_SWIPE_START_STATE = + STATE_ACTIVITY_MULTIPLIER_COMPLETE | STATE_LAUNCHER_STARTED + | STATE_APP_CONTROLLER_RECEIVED | STATE_SCREENSHOT_CAPTURED; + // For debugging, keep in sync with above states private static final String[] STATES = new String[] { "STATE_LAUNCHER_PRESENT", @@ -118,16 +135,21 @@ public class WindowTransformSwipeHandler<T extends BaseDraggingActivity> { "STATE_HANDLER_INVALIDATED", "STATE_GESTURE_STARTED", "STATE_GESTURE_CANCELLED", - "STATE_SWITCH_TO_SCREENSHOT_COMPLETE", - "STATE_QUICK_SWITCH", + "STATE_GESTURE_COMPLETED", + "STATE_CURRENT_TASK_FINISHED", "STATE_QUICK_SCRUB_START", - "STATE_QUICK_SCRUB_END" + "STATE_QUICK_SCRUB_END", + "STATE_CAPTURE_SCREENSHOT", + "STATE_SCREENSHOT_CAPTURED", + "STATE_RESUME_LAST_TASK", }; - private static final long MAX_SWIPE_DURATION = 200; - private static final long MIN_SWIPE_DURATION = 80; + public static final long MAX_SWIPE_DURATION = 350; + public static final long MIN_SWIPE_DURATION = 80; private static final float MIN_PROGRESS_FOR_OVERVIEW = 0.5f; + private static final float SWIPE_DURATION_MULTIPLIER = + Math.min(1 / MIN_PROGRESS_FOR_OVERVIEW, 1 / (1 - MIN_PROGRESS_FOR_OVERVIEW)); private final ClipAnimationHelper mClipAnimationHelper = new ClipAnimationHelper(); @@ -142,13 +164,16 @@ public class WindowTransformSwipeHandler<T extends BaseDraggingActivity> { // visible. private final AnimatedFloat mCurrentShift = new AnimatedFloat(this::updateFinalShift); - private final MainThreadExecutor mMainExecutor = new MainThreadExecutor(); + private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper()); private final Context mContext; - private final int mRunningTaskId; private final ActivityControlHelper<T> mActivityControlHelper; private final ActivityInitListener mActivityInitListener; + private final int mRunningTaskId; + private final RunningTaskInfo mRunningTaskInfo; + private ThumbnailData mTaskSnapshot; + private MultiStateCallback mStateCallback; private AnimatorPlaybackController mLauncherTransitionController; @@ -156,14 +181,16 @@ public class WindowTransformSwipeHandler<T extends BaseDraggingActivity> { private LayoutListener mLayoutListener; private RecentsView mRecentsView; private QuickScrubController mQuickScrubController; + private AnimationFactory mAnimationFactory = (t) -> { }; private Runnable mLauncherDrawnCallback; private boolean mWasLauncherAlreadyVisible; - private float mCurrentDisplacement; private boolean mGestureStarted; private int mLogAction = Touch.SWIPE; + private float mCurrentQuickScrubProgress; + private boolean mQuickScrubBlocked; private @InteractionType int mInteractionType = INTERACTION_NORMAL; @@ -174,19 +201,25 @@ public class WindowTransformSwipeHandler<T extends BaseDraggingActivity> { private final long mTouchTimeMs; private long mLauncherFrameDrawnTime; + private boolean mBgLongSwipeMode = false; + private boolean mUiLongSwipeMode = false; + private float mLongSwipeDisplacement = 0; + private LongSwipeHelper mLongSwipeController; + WindowTransformSwipeHandler(RunningTaskInfo runningTaskInfo, Context context, long touchTimeMs, ActivityControlHelper<T> controller) { mContext = context; + mRunningTaskInfo = runningTaskInfo; mRunningTaskId = runningTaskInfo.id; mTouchTimeMs = touchTimeMs; mActivityControlHelper = controller; mActivityInitListener = mActivityControlHelper .createActivityInitListener(this::onActivityInit); + initStateCallbacks(); // Register the input consumer on the UI thread, to ensure that it runs after any pending // unregister calls - mMainExecutor.execute(mInputConsumer::registerInputConsumer); - initStateCallbacks(); + executeOnUiThread(mInputConsumer::registerInputConsumer); } private void initStateCallbacks() { @@ -203,45 +236,66 @@ public class WindowTransformSwipeHandler<T extends BaseDraggingActivity> { mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_DRAWN, this::launcherFrameDrawn); mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_GESTURE_STARTED, - this::notifyGestureStarted); + this::notifyGestureStartedAsync); mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_STARTED | STATE_GESTURE_CANCELLED, this::resetStateForAnimationCancel); - mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_APP_CONTROLLER_RECEIVED - | STATE_SCALED_CONTROLLER_APP, + mStateCallback.addCallback(STATE_LAUNCHER_STARTED | STATE_APP_CONTROLLER_RECEIVED, + this::sendRemoteAnimationsToAnimationFactory); + + mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_SCALED_CONTROLLER_APP, + this::resumeLastTaskForQuickstep); + mStateCallback.addCallback(STATE_RESUME_LAST_TASK | STATE_APP_CONTROLLER_RECEIVED, this::resumeLastTask); + mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_APP_CONTROLLER_RECEIVED | STATE_ACTIVITY_MULTIPLIER_COMPLETE - | STATE_SCALED_CONTROLLER_RECENTS, + | STATE_CAPTURE_SCREENSHOT, this::switchToScreenshot); + + mStateCallback.addCallback(STATE_SCREENSHOT_CAPTURED | STATE_GESTURE_COMPLETED + | STATE_SCALED_CONTROLLER_RECENTS, + this::finishCurrentTransitionToHome); + mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_APP_CONTROLLER_RECEIVED | STATE_ACTIVITY_MULTIPLIER_COMPLETE | STATE_SCALED_CONTROLLER_RECENTS - | STATE_SWITCH_TO_SCREENSHOT_COMPLETE, + | STATE_CURRENT_TASK_FINISHED + | STATE_GESTURE_COMPLETED, this::setupLauncherUiAfterSwipeUpAnimation); - mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_SCALED_CONTROLLER_APP, - this::reset); - mStateCallback.addCallback(STATE_HANDLER_INVALIDATED, this::invalidateHandler); mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED, this::invalidateHandlerWithLauncher); + mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_HANDLER_INVALIDATED + | STATE_SCALED_CONTROLLER_APP, + this::notifyTransitionCancelled); - mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_QUICK_SCRUB_START, + mStateCallback.addCallback(STATE_LAUNCHER_STARTED | STATE_QUICK_SCRUB_START, this::onQuickScrubStart); - mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_QUICK_SCRUB_START + mStateCallback.addCallback(STATE_LAUNCHER_STARTED | STATE_QUICK_SCRUB_START | STATE_SCALED_CONTROLLER_RECENTS, this::onFinishedTransitionToQuickScrub); - mStateCallback.addCallback(STATE_LAUNCHER_PRESENT | STATE_SWITCH_TO_SCREENSHOT_COMPLETE + mStateCallback.addCallback(STATE_LAUNCHER_STARTED | STATE_CURRENT_TASK_FINISHED | STATE_QUICK_SCRUB_END, this::switchToFinalAppAfterQuickScrub); + + mStateCallback.addCallback(LONG_SWIPE_ENTER_STATE, this::checkLongSwipeCanEnter); + mStateCallback.addCallback(LONG_SWIPE_START_STATE, this::checkLongSwipeCanStart); + } + + private void executeOnUiThread(Runnable action) { + if (Looper.myLooper() == mMainThreadHandler.getLooper()) { + action.run(); + } else { + postAsyncCallback(mMainThreadHandler, action); + } } private void setStateOnUiThread(int stateFlag) { - Handler handler = mMainExecutor.getHandler(); - if (Looper.myLooper() == handler.getLooper()) { + if (Looper.myLooper() == mMainThreadHandler.getLooper()) { mStateCallback.setState(stateFlag); } else { - postAtFrontOfQueueAsynchronously(handler, () -> mStateCallback.setState(stateFlag)); + postAsyncCallback(mMainThreadHandler, () -> mStateCallback.setState(stateFlag)); } } @@ -285,7 +339,11 @@ public class WindowTransformSwipeHandler<T extends BaseDraggingActivity> { mActivity = activity; // Override the visibility of the activity until the gesture actually starts and we swipe // up, or until we transition home and the home animation is composed - mActivity.setForceInvisible(true); + if (alreadyOnHome) { + mActivity.clearForceInvisibleFlag(INVISIBLE_BY_STATE_HANDLER); + } else { + mActivity.addForceInvisibleFlag(INVISIBLE_BY_STATE_HANDLER); + } mRecentsView = activity.getOverviewPanel(); mQuickScrubController = mRecentsView.getQuickScrubController(); @@ -304,33 +362,27 @@ public class WindowTransformSwipeHandler<T extends BaseDraggingActivity> { if (mActivity != activity) { return; } - if ((mStateCallback.getState() & STATE_HANDLER_INVALIDATED) != 0) { + if (mStateCallback.hasStates(STATE_HANDLER_INVALIDATED)) { return; } - mStateCallback.setState(STATE_LAUNCHER_STARTED); - mActivityControlHelper.prepareRecentsUI(mActivity, mWasLauncherAlreadyVisible); + mAnimationFactory = mActivityControlHelper.prepareRecentsUI(mActivity, + mWasLauncherAlreadyVisible, this::onAnimatorPlaybackControllerCreated); AbstractFloatingView.closeAllOpenViews(activity, mWasLauncherAlreadyVisible); if (mWasLauncherAlreadyVisible) { - mLauncherTransitionController = mActivityControlHelper - .createControllerForVisibleActivity(activity); - mLauncherTransitionController.dispatchOnStart(); - mLauncherTransitionController.setPlayFraction(mCurrentShift.value); - mStateCallback.setState(STATE_ACTIVITY_MULTIPLIER_COMPLETE | STATE_LAUNCHER_DRAWN); } else { TraceHelper.beginSection("WTS-init"); - // TODO: Implement a better animation for fading in - View rootView = activity.getRootView(); - rootView.setAlpha(0); - rootView.getViewTreeObserver().addOnDrawListener(new OnDrawListener() { + View dragLayer = activity.getDragLayer(); + mActivityControlHelper.getAlphaProperty(activity).setValue(0); + dragLayer.getViewTreeObserver().addOnDrawListener(new OnDrawListener() { @Override public void onDraw() { TraceHelper.endSection("WTS-init", "Launcher frame is drawn"); - rootView.post(() -> - rootView.getViewTreeObserver().removeOnDrawListener(this)); + dragLayer.post(() -> + dragLayer.getViewTreeObserver().removeOnDrawListener(this)); if (activity != mActivity) { return; } @@ -341,8 +393,10 @@ public class WindowTransformSwipeHandler<T extends BaseDraggingActivity> { } mRecentsView.showTask(mRunningTaskId); - mRecentsView.setFirstTaskIconScaledDown(true /* isScaledDown */, false /* animate */); + mRecentsView.setRunningTaskHidden(true); + mRecentsView.setRunningTaskIconScaledDown(true /* isScaledDown */, false /* animate */); mLayoutListener.open(); + mStateCallback.setState(STATE_LAUNCHER_STARTED); } public void setLauncherOnDrawCallback(Runnable callback) { @@ -350,15 +404,22 @@ public class WindowTransformSwipeHandler<T extends BaseDraggingActivity> { } private void launcherFrameDrawn() { - View rootView = mActivity.getRootView(); - if (rootView.getAlpha() < 1) { + AlphaProperty property = mActivityControlHelper.getAlphaProperty(mActivity); + if (property.getValue() < 1) { if (mGestureStarted) { final MultiStateCallback callback = mStateCallback; - rootView.animate().alpha(1) - .setDuration(getFadeInDuration()) - .withEndAction(() -> callback.setState(STATE_ACTIVITY_MULTIPLIER_COMPLETE)); + ObjectAnimator animator = ObjectAnimator.ofFloat( + property, MultiValueAlpha.VALUE, 1); + animator.setDuration(getFadeInDuration()).addListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + callback.setState(STATE_ACTIVITY_MULTIPLIER_COMPLETE); + } + }); + animator.start(); } else { - rootView.setAlpha(1); + property.setValue(1); mStateCallback.setState(STATE_ACTIVITY_MULTIPLIER_COMPLETE); } } @@ -368,15 +429,16 @@ public class WindowTransformSwipeHandler<T extends BaseDraggingActivity> { mLauncherFrameDrawnTime = SystemClock.uptimeMillis(); } + private void sendRemoteAnimationsToAnimationFactory() { + mAnimationFactory.onRemoteAnimationReceived(mRecentsAnimationWrapper.targetSet); + } + private void initializeLauncherAnimationController() { mLayoutListener.setHandler(this); - onLauncherLayoutChanged(); - - final long transitionDelay = mLauncherFrameDrawnTime - mTouchTimeMs; - SysuiEventLogger.writeDummyRecentsTransition(transitionDelay); + buildAnimationController(); if (LatencyTrackerCompat.isEnabled(mContext)) { - LatencyTrackerCompat.logToggleRecents((int) transitionDelay); + LatencyTrackerCompat.logToggleRecents((int) (mLauncherFrameDrawnTime - mTouchTimeMs)); } } @@ -391,110 +453,108 @@ public class WindowTransformSwipeHandler<T extends BaseDraggingActivity> { } mInteractionType = interactionType; - setStateOnUiThread(STATE_QUICK_SCRUB_START); + setStateOnUiThread(STATE_QUICK_SCRUB_START | STATE_GESTURE_COMPLETED); // Start the window animation without waiting for launcher. - animateToProgress(1f, QUICK_SCRUB_START_DURATION); + animateToProgress(1f, QUICK_SCRUB_FROM_APP_START_DURATION, LINEAR); } @WorkerThread public void updateDisplacement(float displacement) { - mCurrentDisplacement = displacement; - - float translation = Utilities.boundToRange(-mCurrentDisplacement, 0, mTransitionDragLength); - float shift = mTransitionDragLength == 0 ? 0 : translation / mTransitionDragLength; - mCurrentShift.updateValue(shift); + // We are moving in the negative x/y direction + displacement = -displacement; + if (displacement > mTransitionDragLength) { + mCurrentShift.updateValue(1); + + if (!mBgLongSwipeMode) { + mBgLongSwipeMode = true; + executeOnUiThread(this::onLongSwipeEnabledUi); + } + mLongSwipeDisplacement = displacement - mTransitionDragLength; + executeOnUiThread(this::onLongSwipeDisplacementUpdated); + } else { + if (mBgLongSwipeMode) { + mBgLongSwipeMode = false; + executeOnUiThread(this::onLongSwipeDisabledUi); + } + float translation = Math.max(displacement, 0); + float shift = mTransitionDragLength == 0 ? 0 : translation / mTransitionDragLength; + mCurrentShift.updateValue(shift); + } } /** * Called by {@link #mLayoutListener} when launcher layout changes */ - public void onLauncherLayoutChanged() { + public void buildAnimationController() { initTransitionEndpoints(mActivity.getDeviceProfile()); + mAnimationFactory.createActivityController(mTransitionDragLength); + } - if (!mWasLauncherAlreadyVisible) { - mLauncherTransitionController = mActivityControlHelper - .createControllerForHiddenActivity(mActivity, mTransitionDragLength); - mLauncherTransitionController.setPlayFraction(mCurrentShift.value); - } + private void onAnimatorPlaybackControllerCreated(AnimatorPlaybackController anim) { + mLauncherTransitionController = anim; + mLauncherTransitionController.dispatchOnStart(); + mLauncherTransitionController.setPlayFraction(mCurrentShift.value); } @WorkerThread private void updateFinalShift() { float shift = mCurrentShift.value; - synchronized (mRecentsAnimationWrapper) { - if (mRecentsAnimationWrapper.controller != null) { - Interpolator interpolator = mInteractionType == INTERACTION_QUICK_SCRUB - ? ACCEL_2 : LINEAR; - float interpolated = interpolator.getInterpolation(shift); - mClipAnimationHelper.applyTransform(mRecentsAnimationWrapper.targets, interpolated); + RecentsAnimationControllerCompat controller = mRecentsAnimationWrapper.getController(); + if (controller != null) { + mClipAnimationHelper.applyTransform(mRecentsAnimationWrapper.targetSet, shift); + + // TODO: This logic is spartanic! + boolean passedThreshold = shift > 0.12f; + mRecentsAnimationWrapper.setAnimationTargetsBehindSystemBars(!passedThreshold); + if (mActivityControlHelper.shouldMinimizeSplitScreen()) { + mRecentsAnimationWrapper.setSplitScreenMinimizedForTransaction(passedThreshold); } } - if (mLauncherTransitionController != null) { - Runnable runOnUi = () -> { - if (mLauncherTransitionController == null) { - return; - } - mLauncherTransitionController.setPlayFraction(shift); - - // Make sure the window follows the first task if it moves, e.g. during quick scrub. - View firstTask = mRecentsView.getPageAt(0); - // The first task may be null if we are swiping up from a task that does not - // appear in the list (ie. the assistant) - if (firstTask != null) { - int scrollForFirstTask = mRecentsView.getScrollForPage(0); - int offsetFromFirstTask = (scrollForFirstTask - mRecentsView.getScrollX()); - mClipAnimationHelper.offsetTarget(firstTask.getScaleX(), - offsetFromFirstTask + firstTask.getTranslationX(), - mRecentsView.getTranslationY()); - } - if (mRecentsAnimationWrapper.controller != null) { - // TODO: This logic is spartanic! - boolean passedThreshold = shift > 0.12f; - mRecentsAnimationWrapper.setAnimationTargetsBehindSystemBars(!passedThreshold); - mRecentsAnimationWrapper.setSplitScreenMinimizedForTransaction(passedThreshold); - } - }; - if (Looper.getMainLooper() == Looper.myLooper()) { - runOnUi.run(); - } else { - // The fling operation completed even before the launcher was drawn - mMainExecutor.execute(runOnUi); - } + executeOnUiThread(this::updateFinalShiftUi); + } + + private void updateFinalShiftUi() { + if (mLauncherTransitionController == null) { + return; } + mLauncherTransitionController.setPlayFraction(mCurrentShift.value); } public void onRecentsAnimationStart(RecentsAnimationControllerCompat controller, - RemoteAnimationTargetCompat[] apps, Rect homeContentInsets, Rect minimizedHomeBounds) { - if (apps != null) { - // Use the top closing app to determine the insets for the animation - for (RemoteAnimationTargetCompat target : apps) { - if (target.mode == MODE_CLOSING) { - DeviceProfile dp = LauncherAppState.getIDP(mContext).getDeviceProfile(mContext); - final Rect homeStackBounds; - - if (minimizedHomeBounds != null) { - homeStackBounds = minimizedHomeBounds; - dp = dp.getMultiWindowProfile(mContext, - new Point(minimizedHomeBounds.width(), minimizedHomeBounds.height())); - dp.updateInsets(homeContentInsets); - } else { - homeStackBounds = new Rect(0, 0, dp.widthPx, dp.heightPx); - // TODO: Workaround for an existing issue where the home content insets are - // not valid immediately after rotation, just use the stable insets for now - Rect insets = new Rect(); - WindowManagerWrapper.getInstance().getStableInsets(insets); - dp.updateInsets(insets); - } + RemoteAnimationTargetSet targets, Rect homeContentInsets, Rect minimizedHomeBounds) { + LauncherAppState appState = LauncherAppState.getInstanceNoCreate(); + InvariantDeviceProfile idp = appState == null ? + new InvariantDeviceProfile(mContext) : appState.getInvariantDeviceProfile(); + DeviceProfile dp = idp.getDeviceProfile(mContext); + final Rect overviewStackBounds; + RemoteAnimationTargetCompat runningTaskTarget = targets.findTask(mRunningTaskId); + + if (minimizedHomeBounds != null && runningTaskTarget != null) { + overviewStackBounds = mActivityControlHelper + .getOverviewWindowBounds(minimizedHomeBounds, runningTaskTarget); + dp = dp.getMultiWindowProfile(mContext, + new Point(minimizedHomeBounds.width(), minimizedHomeBounds.height())); + dp.updateInsets(homeContentInsets); + } else { + overviewStackBounds = new Rect(0, 0, dp.widthPx, dp.heightPx); + // If we are not in multi-window mode, home insets should be same as system insets. + Rect insets = new Rect(); + WindowManagerWrapper.getInstance().getStableInsets(insets); + dp = dp.copy(mContext); + dp.updateInsets(insets); + } + dp.updateIsSeascape(mContext.getSystemService(WindowManager.class)); - mClipAnimationHelper.updateSource(homeStackBounds, target); - initTransitionEndpoints(dp); - } - } + if (runningTaskTarget != null) { + mClipAnimationHelper.updateSource(overviewStackBounds, runningTaskTarget); } - mRecentsAnimationWrapper.setController(controller, apps); + mClipAnimationHelper.prepareAnimation(false /* isOpening */); + initTransitionEndpoints(dp); + + mRecentsAnimationWrapper.setController(controller, targets); setStateOnUiThread(STATE_APP_CONTROLLER_RECEIVED); } @@ -505,9 +565,10 @@ public class WindowTransformSwipeHandler<T extends BaseDraggingActivity> { } public void onGestureStarted() { - notifyGestureStarted(); + notifyGestureStartedAsync(); setStateOnUiThread(STATE_GESTURE_STARTED); mGestureStarted = true; + mRecentsAnimationWrapper.hideCurrentInputMethod(); mRecentsAnimationWrapper.enableInputConsumer(); } @@ -515,43 +576,56 @@ public class WindowTransformSwipeHandler<T extends BaseDraggingActivity> { * Notifies the launcher that the swipe gesture has started. This can be called multiple times * on both background and UI threads */ - private void notifyGestureStarted() { + @AnyThread + private void notifyGestureStartedAsync() { final T curActivity = mActivity; if (curActivity != null) { // Once the gesture starts, we can no longer transition home through the button, so // reset the force override of the activity visibility - mActivity.setForceInvisible(false); - mActivityControlHelper.onQuickstepGestureStarted( - curActivity, mWasLauncherAlreadyVisible); + mActivity.clearForceInvisibleFlag(INVISIBLE_BY_STATE_HANDLER); } } @WorkerThread public void onGestureEnded(float endVelocity) { - Resources res = mContext.getResources(); - float flingThreshold = res.getDimension(R.dimen.quickstep_fling_threshold_velocity); - boolean isFling = Math.abs(endVelocity) > flingThreshold; + float flingThreshold = mContext.getResources() + .getDimension(R.dimen.quickstep_fling_threshold_velocity); + boolean isFling = mGestureStarted && Math.abs(endVelocity) > flingThreshold; + setStateOnUiThread(STATE_GESTURE_COMPLETED); + + mLogAction = isFling ? Touch.FLING : Touch.SWIPE; + + if (mBgLongSwipeMode) { + executeOnUiThread(() -> onLongSwipeGestureFinishUi(endVelocity, isFling)); + } else { + handleNormalGestureEnd(endVelocity, isFling); + } + } + private void handleNormalGestureEnd(float endVelocity, boolean isFling) { long duration = MAX_SWIPE_DURATION; final float endShift; if (!isFling) { - endShift = mCurrentShift.value >= MIN_PROGRESS_FOR_OVERVIEW ? 1 : 0; - mLogAction = Touch.SWIPE; + endShift = mCurrentShift.value >= MIN_PROGRESS_FOR_OVERVIEW && mGestureStarted ? 1 : 0; + long expectedDuration = Math.abs(Math.round((endShift - mCurrentShift.value) + * MAX_SWIPE_DURATION * SWIPE_DURATION_MULTIPLIER)); + duration = Math.min(MAX_SWIPE_DURATION, expectedDuration); } else { endShift = endVelocity < 0 ? 1 : 0; - float minFlingVelocity = res.getDimension(R.dimen.quickstep_fling_min_velocity); + float minFlingVelocity = mContext.getResources() + .getDimension(R.dimen.quickstep_fling_min_velocity); if (Math.abs(endVelocity) > minFlingVelocity && mTransitionDragLength > 0) { float distanceToTravel = (endShift - mCurrentShift.value) * mTransitionDragLength; // we want the page's snap velocity to approximately match the velocity at // which the user flings, so we scale the duration by a value near to the - // derivative of the scroll interpolator at zero, ie. 5. - duration = 5 * Math.round(1000 * Math.abs(distanceToTravel / endVelocity)); + // derivative of the scroll interpolator at zero, ie. 2. + long baseDuration = Math.round(1000 * Math.abs(distanceToTravel / endVelocity)); + duration = Math.min(MAX_SWIPE_DURATION, 2 * baseDuration); } - mLogAction = Touch.FLING; } - animateToProgress(endShift, duration); + animateToProgress(endShift, duration, DEACCEL); } private void doLogGesture(boolean toLauncher) { @@ -571,24 +645,31 @@ public class WindowTransformSwipeHandler<T extends BaseDraggingActivity> { } /** Animates to the given progress, where 0 is the current app and 1 is overview. */ - private void animateToProgress(float progress, long duration) { + private void animateToProgress(float progress, long duration, Interpolator interpolator) { mIsGoingToHome = Float.compare(progress, 1) == 0; ObjectAnimator anim = mCurrentShift.animateToValue(progress).setDuration(duration); - anim.setInterpolator(Interpolators.SCROLL); + anim.setInterpolator(interpolator); anim.addListener(new AnimationSuccessListener() { @Override public void onAnimationSuccess(Animator animator) { - setStateOnUiThread(mIsGoingToHome ? - STATE_SCALED_CONTROLLER_RECENTS : STATE_SCALED_CONTROLLER_APP); + setStateOnUiThread(mIsGoingToHome + ? (STATE_SCALED_CONTROLLER_RECENTS | STATE_CAPTURE_SCREENSHOT) + : STATE_SCALED_CONTROLLER_APP); } }); anim.start(); } @UiThread + private void resumeLastTaskForQuickstep() { + setStateOnUiThread(STATE_RESUME_LAST_TASK); + doLogGesture(false /* toLauncher */); + reset(); + } + + @UiThread private void resumeLastTask() { mRecentsAnimationWrapper.finish(false /* toHome */, null); - doLogGesture(false /* toLauncher */); } public void reset() { @@ -608,13 +689,20 @@ public class WindowTransformSwipeHandler<T extends BaseDraggingActivity> { mActivityInitListener.unregister(); mInputConsumer.unregisterInputConsumer(); + mTaskSnapshot = null; } private void invalidateHandlerWithLauncher() { mLauncherTransitionController = null; mLayoutListener.finish(); + mActivityControlHelper.getAlphaProperty(mActivity).setValue(1); + + mRecentsView.setRunningTaskHidden(false); + mRecentsView.setRunningTaskIconScaledDown(false /* isScaledDown */, false /* animate */); + } - mRecentsView.setFirstTaskIconScaledDown(false /* isScaledDown */, false /* animate */); + private void notifyTransitionCancelled() { + mAnimationFactory.onTransitionCancelled(); } private void resetStateForAnimationCancel() { @@ -630,40 +718,38 @@ public class WindowTransformSwipeHandler<T extends BaseDraggingActivity> { private void switchToScreenshot() { boolean finishTransitionPosted = false; - final Runnable finishTransitionRunnable = () -> { - synchronized (mRecentsAnimationWrapper) { - mRecentsAnimationWrapper.finish(true /* toHome */, - () -> setStateOnUiThread(STATE_SWITCH_TO_SCREENSHOT_COMPLETE)); + RecentsAnimationControllerCompat controller = mRecentsAnimationWrapper.getController(); + if (controller != null) { + // Update the screenshot of the task + if (mTaskSnapshot == null) { + mTaskSnapshot = controller.screenshotTask(mRunningTaskId); } - }; - - synchronized (mRecentsAnimationWrapper) { - if (mRecentsAnimationWrapper.controller != null) { - TransactionCompat transaction = new TransactionCompat(); - for (RemoteAnimationTargetCompat app : mRecentsAnimationWrapper.targets) { - if (app.mode == MODE_CLOSING) { - // Update the screenshot of the task - ThumbnailData thumbnail = - mRecentsAnimationWrapper.controller.screenshotTask(app.taskId); - TaskView taskView = mRecentsView.updateThumbnail(app.taskId, thumbnail); - if (taskView != null) { - taskView.setAlpha(1); - // Defer finishing the animation until the next launcher frame with the - // new thumbnail - mActivityControlHelper.executeOnNextDraw(mActivity, taskView, - finishTransitionRunnable); - finishTransitionPosted = true; - } + TaskView taskView = mRecentsView.updateThumbnail(mRunningTaskId, mTaskSnapshot); + mRecentsView.setRunningTaskHidden(false); + if (taskView != null) { + // Defer finishing the animation until the next launcher frame with the + // new thumbnail + finishTransitionPosted = new WindowCallbacksCompat(taskView) { + + @Override + public void onPostDraw(Canvas canvas) { + setStateOnUiThread(STATE_SCREENSHOT_CAPTURED); + detach(); } - } - transaction.apply(); + }.attach(); } } if (!finishTransitionPosted) { - // If we haven't posted the transition end runnable, run it now - finishTransitionRunnable.run(); + // If we haven't posted a draw callback, set the state immediately. + setStateOnUiThread(STATE_SCREENSHOT_CAPTURED); + } + } + + private void finishCurrentTransitionToHome() { + synchronized (mRecentsAnimationWrapper) { + mRecentsAnimationWrapper.finish(true /* toHome */, + () -> setStateOnUiThread(STATE_CURRENT_TASK_FINISHED)); } - doLogGesture(true /* toLauncher */); } private void setupLauncherUiAfterSwipeUpAnimation() { @@ -674,27 +760,64 @@ public class WindowTransformSwipeHandler<T extends BaseDraggingActivity> { mActivityControlHelper.onSwipeUpComplete(mActivity); // Animate the first icon. - mRecentsView.setFirstTaskIconScaledDown(false /* isScaledDown */, true /* animate */); - + mRecentsView.setRunningTaskIconScaledDown(false /* isScaledDown */, true /* animate */); mRecentsView.setSwipeDownShouldLaunchApp(true); + RecentsModel.getInstance(mContext).onOverviewShown(false, TAG); + + doLogGesture(true /* toLauncher */); reset(); } private void onQuickScrubStart() { - mActivityControlHelper.onQuickInteractionStart(mActivity, mWasLauncherAlreadyVisible); - mQuickScrubController.onQuickScrubStart(false); + if (!mQuickScrubController.prepareQuickScrub(TAG)) { + mQuickScrubBlocked = true; + setStateOnUiThread(STATE_RESUME_LAST_TASK | STATE_HANDLER_INVALIDATED); + return; + } + if (mLauncherTransitionController != null) { + mLauncherTransitionController.getAnimationPlayer().end(); + mLauncherTransitionController = null; + } + + mActivityControlHelper.onQuickInteractionStart(mActivity, mRunningTaskInfo, false); + + // Inform the last progress in case we skipped before. + mQuickScrubController.onQuickScrubProgress(mCurrentQuickScrubProgress); + + // Make sure the window follows the first task if it moves, e.g. during quick scrub. + TaskView firstTask = mRecentsView.getPageAt(0); + // The first task may be null if we are swiping up from a task that does not + // appear in the list (i.e. the assistant) + if (firstTask != null) { + int scrollForFirstTask = mRecentsView.getScrollForPage(0); + int scrollForSecondTask = mRecentsView.getChildCount() > 1 + ? mRecentsView.getScrollForPage(1) : scrollForFirstTask; + int offsetFromFirstTask = scrollForFirstTask - scrollForSecondTask; + final float interpolation; + if (mRecentsView.getWidth() == 0) { + interpolation = scrollForSecondTask == scrollForFirstTask ? 0 : 1; + } else { + interpolation = (float) offsetFromFirstTask / (mRecentsView.getWidth() / 2); + } + mClipAnimationHelper.offsetTarget( + firstTask.getCurveScaleForInterpolation(interpolation), offsetFromFirstTask, + mActivityControlHelper.getTranslationYForQuickScrub(mActivity), + QuickScrubController.QUICK_SCRUB_START_INTERPOLATOR); + } } private void onFinishedTransitionToQuickScrub() { + if (mQuickScrubBlocked) { + return; + } mQuickScrubController.onFinishedTransitionToQuickScrub(); } public void onQuickScrubProgress(float progress) { - if (Looper.myLooper() != Looper.getMainLooper() || mQuickScrubController == null) { - // TODO: We can still get progress events while launcher is not ready on the worker - // thread. Keep track of last received progress and apply that progress when launcher - // is ready + mCurrentQuickScrubProgress = progress; + if (Looper.myLooper() != Looper.getMainLooper() || mQuickScrubController == null + || mQuickScrubBlocked) { return; } mQuickScrubController.onQuickScrubProgress(progress); @@ -705,6 +828,9 @@ public class WindowTransformSwipeHandler<T extends BaseDraggingActivity> { } private void switchToFinalAppAfterQuickScrub() { + if (mQuickScrubBlocked) { + return; + } mQuickScrubController.onQuickScrubEnd(); // Normally this is handled in reset(), but since we are still scrubbing after the @@ -736,4 +862,72 @@ public class WindowTransformSwipeHandler<T extends BaseDraggingActivity> { public void setGestureEndCallback(Runnable gestureEndCallback) { mGestureEndCallback = gestureEndCallback; } + + // Handling long swipe + private void onLongSwipeEnabledUi() { + mUiLongSwipeMode = true; + checkLongSwipeCanEnter(); + checkLongSwipeCanStart(); + } + + private void onLongSwipeDisabledUi() { + mUiLongSwipeMode = false; + + if (mLongSwipeController != null) { + mLongSwipeController.destroy(); + + // Rebuild animations + buildAnimationController(); + } + } + + private void onLongSwipeDisplacementUpdated() { + if (!mUiLongSwipeMode || mLongSwipeController == null) { + return; + } + + mLongSwipeController.onMove(mLongSwipeDisplacement); + } + + private void checkLongSwipeCanEnter() { + if (!mUiLongSwipeMode || !mStateCallback.hasStates(LONG_SWIPE_ENTER_STATE) + || !mActivityControlHelper.supportsLongSwipe(mActivity)) { + return; + } + + // We are entering long swipe mode, make sure the screen shot is captured. + mStateCallback.setState(STATE_CAPTURE_SCREENSHOT); + + } + + private void checkLongSwipeCanStart() { + if (!mUiLongSwipeMode || !mStateCallback.hasStates(LONG_SWIPE_START_STATE) + || !mActivityControlHelper.supportsLongSwipe(mActivity)) { + return; + } + + RemoteAnimationTargetSet targetSet = mRecentsAnimationWrapper.targetSet; + if (targetSet == null) { + // This can happen when cancelAnimation comes on the background thread, while we are + // processing the long swipe on the UI thread. + return; + } + + mLongSwipeController = mActivityControlHelper.getLongSwipeController( + mActivity, mRecentsAnimationWrapper.targetSet); + onLongSwipeDisplacementUpdated(); + } + + private void onLongSwipeGestureFinishUi(float velocity, boolean isFling) { + if (!mUiLongSwipeMode || mLongSwipeController == null) { + mUiLongSwipeMode = false; + handleNormalGestureEnd(velocity, isFling); + return; + } + mUiLongSwipeMode = false; + finishCurrentTransitionToHome(); + mLongSwipeController.end(velocity, isFling, + () -> setStateOnUiThread(STATE_HANDLER_INVALIDATED)); + + } } diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java index 4ed165699..9e2de3395 100644 --- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java +++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java @@ -18,7 +18,6 @@ package com.android.quickstep.fallback; import android.content.Context; import android.graphics.Canvas; import android.graphics.Rect; -import android.support.annotation.AnyThread; import android.util.AttributeSet; import android.view.View; @@ -36,12 +35,12 @@ public class FallbackRecentsView extends RecentsView<RecentsActivity> { public FallbackRecentsView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); setOverviewStateEnabled(true); - updateEmptyMessage(); + getQuickScrubController().onFinishedTransitionToQuickScrub(); } @Override protected void onAllTasksRemoved() { - mActivity.finish(); + mActivity.startHome(); } @Override @@ -64,11 +63,12 @@ public class FallbackRecentsView extends RecentsView<RecentsActivity> { @Override protected void getTaskSize(DeviceProfile dp, Rect outRect) { - LayoutUtils.calculateTaskSize(getContext(), dp, 0, outRect); + LayoutUtils.calculateFallbackTaskSize(getContext(), dp, outRect); } - @AnyThread - public static void getPageRect(DeviceProfile grid, Context context, Rect outRect) { - LayoutUtils.calculateTaskSize(context, grid, 0, outRect); + @Override + public boolean shouldUseMultiWindowTaskSizeStrategy() { + // Just use the activity task size for multi-window as well. + return false; } } diff --git a/quickstep/src/com/android/quickstep/fallback/RecentsRootView.java b/quickstep/src/com/android/quickstep/fallback/RecentsRootView.java index 7aaa88c38..ca8c2520c 100644 --- a/quickstep/src/com/android/quickstep/fallback/RecentsRootView.java +++ b/quickstep/src/com/android/quickstep/fallback/RecentsRootView.java @@ -17,6 +17,7 @@ package com.android.quickstep.fallback; import android.annotation.TargetApi; import android.content.Context; +import android.graphics.Point; import android.graphics.Rect; import android.util.AttributeSet; @@ -31,18 +32,37 @@ public class RecentsRootView extends BaseDragLayer<RecentsActivity> { private final RecentsActivity mActivity; + private final Point mLastKnownSize = new Point(10, 10); + public RecentsRootView(Context context, AttributeSet attrs) { - super(context, attrs); + super(context, attrs, 1 /* alphaChannelCount */); mActivity = (RecentsActivity) BaseActivity.fromContext(context); setSystemUiVisibility(SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | SYSTEM_UI_FLAG_LAYOUT_STABLE); } + public Point getLastKnownSize() { + return mLastKnownSize; + } + public void setup() { mControllers = new TouchController[] { new RecentsTaskController(mActivity) }; } + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // Check size changes before the actual measure, to avoid multiple measure calls. + int width = MeasureSpec.getSize(widthMeasureSpec); + int height = MeasureSpec.getSize(heightMeasureSpec); + if (mLastKnownSize.x != width || mLastKnownSize.y != height) { + mLastKnownSize.set(width, height); + mActivity.onRootViewSizeChanged(); + } + + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + @TargetApi(23) @Override protected boolean fitSystemWindows(Rect insets) { @@ -62,4 +82,9 @@ public class RecentsRootView extends BaseDragLayer<RecentsActivity> { setBackground(insets.top == 0 ? null : Themes.getAttrDrawable(getContext(), R.attr.workspaceStatusBarScrim)); } + + public void dispatchInsets() { + mActivity.getDeviceProfile().updateInsets(mInsets); + super.setInsets(mInsets); + } }
\ No newline at end of file diff --git a/quickstep/src/com/android/quickstep/logging/UserEventDispatcherExtension.java b/quickstep/src/com/android/quickstep/logging/UserEventDispatcherExtension.java new file mode 100644 index 000000000..04153cc99 --- /dev/null +++ b/quickstep/src/com/android/quickstep/logging/UserEventDispatcherExtension.java @@ -0,0 +1,85 @@ +/* + * 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.logging; + +import android.util.Log; + +import static com.android.launcher3.logging.LoggerUtils.newAction; +import static com.android.launcher3.logging.LoggerUtils.newContainerTarget; +import static com.android.launcher3.logging.LoggerUtils.newLauncherEvent; +import static com.android.launcher3.userevent.nano.LauncherLogProto.ControlType.CANCEL_TARGET; +import static com.android.systemui.shared.system.LauncherEventUtil.VISIBLE; +import static com.android.systemui.shared.system.LauncherEventUtil.DISMISS; +import static com.android.systemui.shared.system.LauncherEventUtil.RECENTS_QUICK_SCRUB_ONBOARDING_TIP; +import static com.android.systemui.shared.system.LauncherEventUtil.RECENTS_SWIPE_UP_ONBOARDING_TIP; + +import com.android.launcher3.logging.UserEventDispatcher; +import com.android.launcher3.model.nano.LauncherDumpProto; +import com.android.launcher3.userevent.nano.LauncherLogExtensions; +import com.android.launcher3.userevent.nano.LauncherLogProto; +import com.android.systemui.shared.system.LauncherEventUtil; +import com.android.systemui.shared.system.MetricsLoggerCompat; + +/** + * This class handles AOSP MetricsLogger function calls and logging around + * quickstep interactions. + */ +public class UserEventDispatcherExtension extends UserEventDispatcher { + + private static final String TAG = "UserEventDispatcher"; + + public void logStateChangeAction(int action, int dir, int srcChildTargetType, + int srcParentContainerType, int dstContainerType, + int pageIndex) { + new MetricsLoggerCompat().visibility(MetricsLoggerCompat.OVERVIEW_ACTIVITY, + dstContainerType == LauncherLogProto.ContainerType.TASKSWITCHER); + super.logStateChangeAction(action, dir, srcChildTargetType, srcParentContainerType, + dstContainerType, pageIndex); + } + + public void logActionTip(int actionType, int viewType) { + LauncherLogProto.Action action = new LauncherLogProto.Action(); + LauncherLogProto.Target target = new LauncherLogProto.Target(); + switch(actionType) { + case VISIBLE: + action.type = LauncherLogProto.Action.Type.TIP; + target.type = LauncherLogProto.Target.Type.CONTAINER; + target.containerType = LauncherLogProto.ContainerType.TIP; + break; + case DISMISS: + action.type = LauncherLogProto.Action.Type.TOUCH; + action.touch = LauncherLogProto.Action.Touch.TAP; + target.type = LauncherLogProto.Target.Type.CONTROL; + target.controlType = CANCEL_TARGET; + break; + default: + Log.e(TAG, "Unexpected action type = " + actionType); + } + + switch(viewType) { + case RECENTS_QUICK_SCRUB_ONBOARDING_TIP: + target.tipType = LauncherLogProto.TipType.QUICK_SCRUB_TEXT; + break; + case RECENTS_SWIPE_UP_ONBOARDING_TIP: + target.tipType = LauncherLogProto.TipType.SWIPE_UP_TEXT; + break; + default: + Log.e(TAG, "Unexpected viewType = " + viewType); + } + LauncherLogProto.LauncherEvent event = newLauncherEvent(action, target); + dispatchUserEvent(event, null); + } +} diff --git a/quickstep/src/com/android/quickstep/util/ClipAnimationHelper.java b/quickstep/src/com/android/quickstep/util/ClipAnimationHelper.java index 493e9e238..8c7f104a6 100644 --- a/quickstep/src/com/android/quickstep/util/ClipAnimationHelper.java +++ b/quickstep/src/com/android/quickstep/util/ClipAnimationHelper.java @@ -15,21 +15,44 @@ */ package com.android.quickstep.util; +import static com.android.launcher3.anim.Interpolators.LINEAR; +import static com.android.quickstep.QuickScrubController.QUICK_SCRUB_TRANSLATION_Y_FACTOR; import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING; +import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING; +import android.annotation.TargetApi; +import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Matrix.ScaleToFit; +import android.graphics.PointF; import android.graphics.Rect; import android.graphics.RectF; +import android.os.Build; +import android.os.RemoteException; +import android.support.annotation.Nullable; +import android.view.animation.Interpolator; +import com.android.launcher3.BaseDraggingActivity; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.R; import com.android.launcher3.Utilities; +import com.android.launcher3.anim.Interpolators; +import com.android.launcher3.views.BaseDragLayer; +import com.android.quickstep.RecentsModel; +import com.android.quickstep.views.RecentsView; +import com.android.quickstep.views.TaskThumbnailView; +import com.android.systemui.shared.recents.ISystemUiProxy; import com.android.systemui.shared.recents.utilities.RectFEvaluator; import com.android.systemui.shared.system.RemoteAnimationTargetCompat; import com.android.systemui.shared.system.TransactionCompat; +import com.android.systemui.shared.system.WindowManagerWrapper; + +import java.util.function.BiConsumer; /** * Utility class to handle window clip animation */ +@TargetApi(Build.VERSION_CODES.P) public class ClipAnimationHelper { // The bounds of the source app in device coordinates @@ -40,8 +63,8 @@ public class ClipAnimationHelper { private final RectF mSourceRect = new RectF(); // The bounds of the task view in launcher window coordinates private final RectF mTargetRect = new RectF(); - // Doesn't change after initialized, used as an anchor when changing mTargetRect - private final RectF mInitialTargetRect = new RectF(); + // Set when the final window destination is changed, such as offsetting for quick scrub + private final PointF mTargetOffset = new PointF(); // The insets to be used for clipping the app window, which can be larger than mSourceInsets // if the aspect ratio of the target is smaller than the aspect ratio of the source rect. In // app window coordinates. @@ -54,15 +77,33 @@ public class ClipAnimationHelper { private final Rect mClipRect = new Rect(); private final RectFEvaluator mRectFEvaluator = new RectFEvaluator(); private final Matrix mTmpMatrix = new Matrix(); + private final RectF mTmpRectF = new RectF(); + private float mTargetScale = 1f; + private Interpolator mInterpolator = LINEAR; + // We translate y slightly faster than the rest of the animation for quick scrub. + private Interpolator mOffsetYInterpolator = LINEAR; - public void updateSource(Rect homeStackBounds, RemoteAnimationTargetCompat target) { - mHomeStackBounds.set(homeStackBounds); - mSourceInsets.set(target.getContentInsets()); + // Whether to boost the opening animation target layers, or the closing + private int mBoostModeTargetLayers = -1; + // Wether or not applyTransform has been called yet since prepareAnimation() + private boolean mIsFirstFrame = true; + + private BiConsumer<TransactionCompat, RemoteAnimationTargetCompat> mTaskTransformCallback = + (t, a) -> { }; + + private void updateSourceStack(RemoteAnimationTargetCompat target) { + mSourceInsets.set(target.contentInsets); mSourceStackBounds.set(target.sourceContainerBounds); // TODO: Should sourceContainerBounds already have this offset? mSourceStackBounds.offsetTo(target.position.x, target.position.y); + + } + + public void updateSource(Rect homeStackBounds, RemoteAnimationTargetCompat target) { + mHomeStackBounds.set(homeStackBounds); + updateSourceStack(target); } public void updateTargetRect(Rect targetRect) { @@ -73,8 +114,6 @@ public class ClipAnimationHelper { mTargetRect.offset(mHomeStackBounds.left - mSourceStackBounds.left, mHomeStackBounds.top - mSourceStackBounds.top); - mInitialTargetRect.set(mTargetRect); - // Calculate the clip based on the target rect (since the content insets and the // launcher insets may differ, so the aspect ratio of the target rect can differ // from the source rect. The difference between the target rect (scaled to the @@ -91,12 +130,22 @@ public class ClipAnimationHelper { mSourceRect.set(scaledTargetRect); } - public void applyTransform(RemoteAnimationTargetCompat[] targets, float progress) { + public void prepareAnimation(boolean isOpening) { + mIsFirstFrame = true; + mBoostModeTargetLayers = isOpening ? MODE_OPENING : MODE_CLOSING; + } + + public RectF applyTransform(RemoteAnimationTargetSet targetSet, float progress) { RectF currentRect; - synchronized (mTargetRect) { - currentRect = mRectFEvaluator.evaluate(progress, mSourceRect, mTargetRect); + mTmpRectF.set(mTargetRect); + Utilities.scaleRectFAboutCenter(mTmpRectF, mTargetScale); + float offsetYProgress = mOffsetYInterpolator.getInterpolation(progress); + progress = mInterpolator.getInterpolation(progress); + currentRect = mRectFEvaluator.evaluate(progress, mSourceRect, mTmpRectF); + + synchronized (mTargetOffset) { // Stay lined up with the center of the target, since it moves for quick scrub. - currentRect.offset(mTargetRect.centerX() - currentRect.centerX(), 0); + currentRect.offset(mTargetOffset.x * progress, mTargetOffset.y * offsetYProgress); } mClipRect.left = (int) (mSourceWindowClipInsets.left * progress); @@ -106,30 +155,140 @@ public class ClipAnimationHelper { mClipRect.bottom = (int) (mSourceStackBounds.height() - (mSourceWindowClipInsets.bottom * progress)); - mTmpMatrix.setRectToRect(mSourceRect, currentRect, ScaleToFit.FILL); - TransactionCompat transaction = new TransactionCompat(); - for (RemoteAnimationTargetCompat app : targets) { - if (app.mode == MODE_CLOSING) { + if (mIsFirstFrame) { + RemoteAnimationProvider.prepareTargetsForFirstFrame(targetSet.unfilteredApps, + transaction, mBoostModeTargetLayers); + mIsFirstFrame = false; + } + for (RemoteAnimationTargetCompat app : targetSet.apps) { + if (app.activityType != RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME) { + mTmpMatrix.setRectToRect(mSourceRect, currentRect, ScaleToFit.FILL); mTmpMatrix.postTranslate(app.position.x, app.position.y); transaction.setMatrix(app.leash, mTmpMatrix) .setWindowCrop(app.leash, mClipRect); - if (app.isNotInRecents) { - transaction.setAlpha(app.leash, 1 - progress); - } + } - transaction.show(app.leash); + if (app.isNotInRecents + || app.activityType == RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME) { + transaction.setAlpha(app.leash, 1 - progress); } + + mTaskTransformCallback.accept(transaction, app); } transaction.setEarlyWakeup(); transaction.apply(); + return currentRect; } - public void offsetTarget(float scale, float offsetX, float offsetY) { - synchronized (mTargetRect) { - mTargetRect.set(mInitialTargetRect); - Utilities.scaleRectFAboutCenter(mTargetRect, scale); - mTargetRect.offset(offsetX, offsetY); + public void setTaskTransformCallback + (BiConsumer<TransactionCompat, RemoteAnimationTargetCompat> callback) { + mTaskTransformCallback = callback; + } + + public void offsetTarget(float scale, float offsetX, float offsetY, Interpolator interpolator) { + synchronized (mTargetOffset) { + mTargetOffset.set(offsetX, offsetY); } + mTargetScale = scale; + mInterpolator = interpolator; + mOffsetYInterpolator = Interpolators.clampToProgress(mInterpolator, 0, + QUICK_SCRUB_TRANSLATION_Y_FACTOR); + } + + public void fromTaskThumbnailView(TaskThumbnailView ttv, RecentsView rv) { + fromTaskThumbnailView(ttv, rv, null); + } + + public void fromTaskThumbnailView(TaskThumbnailView ttv, RecentsView rv, + @Nullable RemoteAnimationTargetCompat target) { + BaseDraggingActivity activity = BaseDraggingActivity.fromContext(ttv.getContext()); + BaseDragLayer dl = activity.getDragLayer(); + + int[] pos = new int[2]; + dl.getLocationOnScreen(pos); + mHomeStackBounds.set(0, 0, dl.getWidth(), dl.getHeight()); + mHomeStackBounds.offset(pos[0], pos[1]); + + if (target != null) { + updateSourceStack(target); + } else if (rv.shouldUseMultiWindowTaskSizeStrategy()) { + updateStackBoundsToMultiWindowTaskSize(activity); + } else { + mSourceStackBounds.set(mHomeStackBounds); + mSourceInsets.set(activity.getDeviceProfile().getInsets()); + } + + Rect targetRect = new Rect(); + dl.getDescendantRectRelativeToSelf(ttv, targetRect); + updateTargetRect(targetRect); + + // Transform the clip relative to the target rect. + float scale = mTargetRect.width() / mSourceRect.width(); + mSourceWindowClipInsets.left = mSourceWindowClipInsets.left * scale; + mSourceWindowClipInsets.top = mSourceWindowClipInsets.top * scale; + mSourceWindowClipInsets.right = mSourceWindowClipInsets.right * scale; + mSourceWindowClipInsets.bottom = mSourceWindowClipInsets.bottom * scale; + } + + private void updateStackBoundsToMultiWindowTaskSize(BaseDraggingActivity activity) { + ISystemUiProxy sysUiProxy = RecentsModel.getInstance(activity).getSystemUiProxy(); + if (sysUiProxy != null) { + try { + mSourceStackBounds.set(sysUiProxy.getNonMinimizedSplitScreenSecondaryBounds()); + return; + } catch (RemoteException e) { + // Use half screen size + } + } + + // Assume that the task size is half screen size (minus the insets and the divider size) + DeviceProfile fullDp = activity.getDeviceProfile().getFullScreenProfile(); + // Use availableWidthPx and availableHeightPx instead of widthPx and heightPx to + // account for system insets + int taskWidth = fullDp.availableWidthPx; + int taskHeight = fullDp.availableHeightPx; + int halfDividerSize = activity.getResources() + .getDimensionPixelSize(R.dimen.multi_window_task_divider_size) / 2; + + Rect insets = new Rect(); + WindowManagerWrapper.getInstance().getStableInsets(insets); + if (fullDp.isLandscape) { + taskWidth = taskWidth / 2 - halfDividerSize; + } else { + taskHeight = taskHeight / 2 - halfDividerSize; + } + + // Align the task to bottom left/right edge (closer to nav bar). + int left = activity.getDeviceProfile().isSeascape() ? insets.left + : (insets.left + fullDp.availableWidthPx - taskWidth); + mSourceStackBounds.set(0, 0, taskWidth, taskHeight); + mSourceStackBounds.offset(left, insets.top + fullDp.availableHeightPx - taskHeight); + } + + public void drawForProgress(TaskThumbnailView ttv, Canvas canvas, float progress) { + RectF currentRect = mRectFEvaluator.evaluate(progress, mSourceRect, mTargetRect); + canvas.translate(mSourceStackBounds.left - mHomeStackBounds.left, + mSourceStackBounds.top - mHomeStackBounds.top); + mTmpMatrix.setRectToRect(mTargetRect, currentRect, ScaleToFit.FILL); + + canvas.concat(mTmpMatrix); + canvas.translate(mTargetRect.left, mTargetRect.top); + + float insetProgress = (1 - progress); + ttv.drawOnCanvas(canvas, + -mSourceWindowClipInsets.left * insetProgress, + -mSourceWindowClipInsets.top * insetProgress, + ttv.getMeasuredWidth() + mSourceWindowClipInsets.right * insetProgress, + ttv.getMeasuredHeight() + mSourceWindowClipInsets.bottom * insetProgress, + ttv.getCornerRadius() * progress); + } + + public RectF getTargetRect() { + return mTargetRect; + } + + public RectF getSourceRect() { + return mSourceRect; } } diff --git a/quickstep/src/com/android/quickstep/util/LayoutUtils.java b/quickstep/src/com/android/quickstep/util/LayoutUtils.java index f29f9e4e9..ec9c7eaed 100644 --- a/quickstep/src/com/android/quickstep/util/LayoutUtils.java +++ b/quickstep/src/com/android/quickstep/util/LayoutUtils.java @@ -15,39 +15,68 @@ */ package com.android.quickstep.util; +import static java.lang.annotation.RetentionPolicy.SOURCE; + import android.content.Context; import android.content.res.Resources; import android.graphics.Rect; -import android.graphics.RectF; +import android.support.annotation.AnyThread; +import android.support.annotation.IntDef; import com.android.launcher3.DeviceProfile; import com.android.launcher3.R; +import java.lang.annotation.Retention; + public class LayoutUtils { + private static final int MULTI_WINDOW_STRATEGY_HALF_SCREEN = 1; + private static final int MULTI_WINDOW_STRATEGY_DEVICE_PROFILE = 2; + + @Retention(SOURCE) + @IntDef({MULTI_WINDOW_STRATEGY_HALF_SCREEN, MULTI_WINDOW_STRATEGY_DEVICE_PROFILE}) + private @interface MultiWindowStrategy {} + public static void calculateLauncherTaskSize(Context context, DeviceProfile dp, Rect outRect) { - float extraSpace = dp.isVerticalBarLayout() ? 0 : dp.hotseatBarSizePx; - calculateTaskSize(context, dp, extraSpace, outRect); + float extraSpace; + if (dp.isVerticalBarLayout()) { + extraSpace = 0; + } else { + extraSpace = dp.hotseatBarSizePx + dp.verticalDragHandleSizePx; + } + calculateTaskSize(context, dp, extraSpace, MULTI_WINDOW_STRATEGY_HALF_SCREEN, outRect); + } + + public static void calculateFallbackTaskSize(Context context, DeviceProfile dp, Rect outRect) { + calculateTaskSize(context, dp, 0, MULTI_WINDOW_STRATEGY_DEVICE_PROFILE, outRect); } + @AnyThread public static void calculateTaskSize(Context context, DeviceProfile dp, - float extraVerticalSpace, Rect outRect) { + float extraVerticalSpace, @MultiWindowStrategy int multiWindowStrategy, Rect outRect) { float taskWidth, taskHeight, paddingHorz; Resources res = context.getResources(); Rect insets = dp.getInsets(); if (dp.isMultiWindowMode) { - DeviceProfile fullDp = dp.getFullScreenProfile(); - // Use availableWidthPx and availableHeightPx instead of widthPx and heightPx to - // account for system insets - taskWidth = fullDp.availableWidthPx; - taskHeight = fullDp.availableHeightPx; - float halfDividerSize = res.getDimension(R.dimen.multi_window_task_divider_size) / 2; - - if (fullDp.isLandscape) { - taskWidth = taskWidth / 2 - halfDividerSize; + if (multiWindowStrategy == MULTI_WINDOW_STRATEGY_HALF_SCREEN) { + DeviceProfile fullDp = dp.getFullScreenProfile(); + // Use availableWidthPx and availableHeightPx instead of widthPx and heightPx to + // account for system insets + taskWidth = fullDp.availableWidthPx; + taskHeight = fullDp.availableHeightPx; + float halfDividerSize = res.getDimension(R.dimen.multi_window_task_divider_size) + / 2; + + if (fullDp.isLandscape) { + taskWidth = taskWidth / 2 - halfDividerSize; + } else { + taskHeight = taskHeight / 2 - halfDividerSize; + } } else { - taskHeight = taskHeight / 2 - halfDividerSize; + // multiWindowStrategy == MULTI_WINDOW_STRATEGY_DEVICE_PROFILE + taskWidth = dp.widthPx; + taskHeight = dp.heightPx; } paddingHorz = res.getDimension(R.dimen.multi_window_task_card_horz_space); } else { @@ -75,7 +104,7 @@ public class LayoutUtils { float outHeight = scale * taskHeight; // Center in the visible space - float x = insets.left + (taskWidth - outWidth) / 2; + float x = insets.left + (launcherVisibleWidth - outWidth) / 2; float y = insets.top + Math.max(topIconMargin, (launcherVisibleHeight - extraVerticalSpace - outHeight) / 2); outRect.set(Math.round(x), Math.round(y), diff --git a/quickstep/src/com/android/quickstep/util/MultiValueUpdateListener.java b/quickstep/src/com/android/quickstep/util/MultiValueUpdateListener.java new file mode 100644 index 000000000..e798d5cbc --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/MultiValueUpdateListener.java @@ -0,0 +1,68 @@ +/* + * 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.util; + +import android.animation.ValueAnimator; +import android.view.animation.Interpolator; + +import java.util.ArrayList; + +/** + * Utility class to update multiple values with different interpolators and durations during + * the same animation. + */ +public abstract class MultiValueUpdateListener implements ValueAnimator.AnimatorUpdateListener { + + private final ArrayList<FloatProp> mAllProperties = new ArrayList<>(); + + @Override + public final void onAnimationUpdate(ValueAnimator animator) { + final float percent = animator.getAnimatedFraction(); + final float currentPlayTime = percent * animator.getDuration(); + + for (int i = mAllProperties.size() - 1; i >= 0; i--) { + FloatProp prop = mAllProperties.get(i); + float time = Math.max(0, currentPlayTime - prop.mDelay); + float newPercent = Math.min(1f, time / prop.mDuration); + newPercent = prop.mInterpolator.getInterpolation(newPercent); + prop.value = prop.mEnd * newPercent + prop.mStart * (1 - newPercent); + } + onUpdate(percent); + } + + public abstract void onUpdate(float percent); + + public final class FloatProp { + + public float value; + + private final float mStart; + private final float mEnd; + private final float mDelay; + private final float mDuration; + private final Interpolator mInterpolator; + + public FloatProp(float start, float end, float delay, float duration, Interpolator i) { + value = mStart = start; + mEnd = end; + mDelay = delay; + mDuration = duration; + mInterpolator = i; + + mAllProperties.add(this); + } + } +} diff --git a/quickstep/src/com/android/quickstep/util/RemoteAnimationProvider.java b/quickstep/src/com/android/quickstep/util/RemoteAnimationProvider.java index 2ffcae38e..bbf223d1e 100644 --- a/quickstep/src/com/android/quickstep/util/RemoteAnimationProvider.java +++ b/quickstep/src/com/android/quickstep/util/RemoteAnimationProvider.java @@ -31,25 +31,34 @@ public interface RemoteAnimationProvider { AnimatorSet createWindowAnimation(RemoteAnimationTargetCompat[] targets); default ActivityOptions toActivityOptions(Handler handler, long duration) { - LauncherAnimationRunner runner = new LauncherAnimationRunner(handler) { + LauncherAnimationRunner runner = new LauncherAnimationRunner(handler, + false /* startAtFrontOfQueue */) { + @Override - public AnimatorSet getAnimator(RemoteAnimationTargetCompat[] targetCompats) { - return createWindowAnimation(targetCompats); + public void onCreateAnimation(RemoteAnimationTargetCompat[] targetCompats, + AnimationResult result) { + result.setAnimation(createWindowAnimation(targetCompats)); } }; return ActivityOptionsCompat.makeRemoteAnimation( new RemoteAnimationAdapterCompat(runner, duration, 0)); } - static void showOpeningTarget(RemoteAnimationTargetCompat[] targetCompats) { - TransactionCompat t = new TransactionCompat(); - for (RemoteAnimationTargetCompat target : targetCompats) { - int layer = target.mode == RemoteAnimationTargetCompat.MODE_CLOSING + /** + * Prepares the given {@param targets} for a remote animation, and should be called with the + * transaction from the first frame of animation. + * + * @param boostModeTargets The mode indicating which targets to boost in z-order above other + * targets. + */ + static void prepareTargetsForFirstFrame(RemoteAnimationTargetCompat[] targets, + TransactionCompat t, int boostModeTargets) { + for (RemoteAnimationTargetCompat target : targets) { + int layer = target.mode == boostModeTargets ? Integer.MAX_VALUE : target.prefixOrderIndex; t.setLayer(target.leash, layer); t.show(target.leash); } - t.apply(); } } diff --git a/quickstep/src/com/android/quickstep/util/RemoteAnimationTargetSet.java b/quickstep/src/com/android/quickstep/util/RemoteAnimationTargetSet.java new file mode 100644 index 000000000..04b8be58b --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/RemoteAnimationTargetSet.java @@ -0,0 +1,61 @@ +/* + * 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.util; + +import com.android.systemui.shared.system.RemoteAnimationTargetCompat; + +import java.util.ArrayList; + +/** + * Holds a collection of RemoteAnimationTargets, filtered by different properties. + */ +public class RemoteAnimationTargetSet { + + public final RemoteAnimationTargetCompat[] unfilteredApps; + public final RemoteAnimationTargetCompat[] apps; + + public RemoteAnimationTargetSet(RemoteAnimationTargetCompat[] apps, int targetMode) { + ArrayList<RemoteAnimationTargetCompat> filteredApps = new ArrayList<>(); + if (apps != null) { + for (RemoteAnimationTargetCompat target : apps) { + if (target.mode == targetMode) { + filteredApps.add(target); + } + } + } + + this.unfilteredApps = apps; + this.apps = filteredApps.toArray(new RemoteAnimationTargetCompat[filteredApps.size()]); + } + + public RemoteAnimationTargetCompat findTask(int taskId) { + for (RemoteAnimationTargetCompat target : apps) { + if (target.taskId == taskId) { + return target; + } + } + return null; + } + + public boolean isAnimatingHome() { + for (RemoteAnimationTargetCompat target : apps) { + if (target.activityType == RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME) { + return true; + } + } + return false; + } +} diff --git a/quickstep/src/com/android/quickstep/util/SysuiEventLogger.java b/quickstep/src/com/android/quickstep/util/SysuiEventLogger.java deleted file mode 100644 index d474ded90..000000000 --- a/quickstep/src/com/android/quickstep/util/SysuiEventLogger.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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.util; - -import android.metrics.LogMaker; -import android.util.EventLog; - -/** - * Utility class for writing logs on behalf of systemUI - */ -public class SysuiEventLogger { - - /** 524292 sysui_multi_action (content|4) */ - public static final int SYSUI_MULTI_ACTION = 524292; - - private static void write(LogMaker content) { - if (content.getType() == 0/*MetricsEvent.TYPE_UNKNOWN*/) { - content.setType(4/*MetricsEvent.TYPE_ACTION*/); - } - EventLog.writeEvent(SYSUI_MULTI_ACTION, content.serialize()); - } - - public static void writeDummyRecentsTransition(long transitionDelay) { - // Mimic ActivityMetricsLogger.logAppTransitionMultiEvents() logging for - // "Recents" activity for app transition tests for the app-to-recents case. - final LogMaker builder = new LogMaker(761/*APP_TRANSITION*/); - builder.setPackageName("com.android.systemui"); - builder.addTaggedData(871/*FIELD_CLASS_NAME*/, - "com.android.systemui.recents.RecentsActivity"); - builder.addTaggedData(319/*APP_TRANSITION_DELAY_MS*/, - transitionDelay); - write(builder); - } -} diff --git a/quickstep/src/com/android/quickstep/util/TaskViewDrawable.java b/quickstep/src/com/android/quickstep/util/TaskViewDrawable.java new file mode 100644 index 000000000..48b07a714 --- /dev/null +++ b/quickstep/src/com/android/quickstep/util/TaskViewDrawable.java @@ -0,0 +1,142 @@ +/* + * 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.util; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.PixelFormat; +import android.graphics.drawable.Drawable; +import android.util.FloatProperty; +import android.view.View; + +import com.android.launcher3.Utilities; +import com.android.quickstep.views.RecentsView; +import com.android.quickstep.views.TaskThumbnailView; +import com.android.quickstep.views.TaskView; + +public class TaskViewDrawable extends Drawable { + + public static final FloatProperty<TaskViewDrawable> PROGRESS = + new FloatProperty<TaskViewDrawable>("progress") { + @Override + public void setValue(TaskViewDrawable taskViewDrawable, float v) { + taskViewDrawable.setProgress(v); + } + + @Override + public Float get(TaskViewDrawable taskViewDrawable) { + return taskViewDrawable.mProgress; + } + }; + + /** + * The progress at which we play the atomic icon scale animation. + */ + private static final float ICON_SCALE_THRESHOLD = 0.95f; + + private final RecentsView mParent; + private final View mIconView; + private final int[] mIconPos; + + private final TaskThumbnailView mThumbnailView; + + private final ClipAnimationHelper mClipAnimationHelper; + + private float mProgress = 1; + private boolean mPassedIconScaleThreshold; + private ValueAnimator mIconScaleAnimator; + private float mIconScale; + + public TaskViewDrawable(TaskView tv, RecentsView parent) { + mParent = parent; + mIconView = tv.getIconView(); + mIconPos = new int[2]; + mIconScale = mIconView.getScaleX(); + Utilities.getDescendantCoordRelativeToAncestor(mIconView, parent, mIconPos, true); + + mThumbnailView = tv.getThumbnail(); + mClipAnimationHelper = new ClipAnimationHelper(); + mClipAnimationHelper.fromTaskThumbnailView(mThumbnailView, parent); + } + + public void setProgress(float progress) { + mProgress = progress; + mParent.invalidate(); + boolean passedIconScaleThreshold = progress <= ICON_SCALE_THRESHOLD; + if (mPassedIconScaleThreshold != passedIconScaleThreshold) { + mPassedIconScaleThreshold = passedIconScaleThreshold; + animateIconScale(mPassedIconScaleThreshold ? 0 : 1); + } + } + + private void animateIconScale(float toScale) { + if (mIconScaleAnimator != null) { + mIconScaleAnimator.cancel(); + } + mIconScaleAnimator = ValueAnimator.ofFloat(mIconScale, toScale); + mIconScaleAnimator.addUpdateListener(valueAnimator -> { + mIconScale = (float) valueAnimator.getAnimatedValue(); + if (mProgress > ICON_SCALE_THRESHOLD) { + // Speed up the icon scale to ensure it is 1 when progress is 1. + float iconProgress = (mProgress - ICON_SCALE_THRESHOLD) / (1 - ICON_SCALE_THRESHOLD); + if (iconProgress > mIconScale) { + mIconScale = iconProgress; + } + } + invalidateSelf(); + }); + mIconScaleAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mIconScaleAnimator = null; + } + }); + mIconScaleAnimator.setDuration(TaskView.SCALE_ICON_DURATION); + mIconScaleAnimator.start(); + } + + @Override + public void draw(Canvas canvas) { + canvas.save(); + canvas.translate(mParent.getScrollX(), mParent.getScrollY()); + mClipAnimationHelper.drawForProgress(mThumbnailView, canvas, mProgress); + canvas.restore(); + + canvas.save(); + canvas.translate(mIconPos[0], mIconPos[1]); + canvas.scale(mIconScale, mIconScale, mIconView.getWidth() / 2, mIconView.getHeight() / 2); + mIconView.draw(canvas); + canvas.restore(); + } + + public ClipAnimationHelper getClipAnimationHelper() { + return mClipAnimationHelper; + } + + @Override + public void setAlpha(int i) { } + + @Override + public void setColorFilter(ColorFilter colorFilter) { } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } +} diff --git a/quickstep/src/com/android/quickstep/views/ClearAllButton.java b/quickstep/src/com/android/quickstep/views/ClearAllButton.java new file mode 100644 index 000000000..d5c43a0f5 --- /dev/null +++ b/quickstep/src/com/android/quickstep/views/ClearAllButton.java @@ -0,0 +1,46 @@ +/* + * 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.views; + +import static android.view.accessibility.AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS; + +import android.content.Context; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.util.AttributeSet; +import android.widget.Button; + +public class ClearAllButton extends Button { + RecentsView mRecentsView; + + public ClearAllButton(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public void setRecentsView(RecentsView recentsView) { + mRecentsView = recentsView; + } + + @Override + public boolean performAccessibilityAction(int action, Bundle arguments) { + final boolean res = super.performAccessibilityAction(action, arguments); + if (action == ACTION_ACCESSIBILITY_FOCUS) { + mRecentsView.revealClearAllButton(); + } + return res; + } +} diff --git a/quickstep/src/com/android/quickstep/views/IconView.java b/quickstep/src/com/android/quickstep/views/IconView.java new file mode 100644 index 000000000..c359966df --- /dev/null +++ b/quickstep/src/com/android/quickstep/views/IconView.java @@ -0,0 +1,91 @@ +/* + * 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.views; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.View; + +/** + * A view which draws a drawable stretched to fit its size. Unlike ImageView, it avoids relayout + * when the drawable changes. + */ +public class IconView extends View { + + private Drawable mDrawable; + + public IconView(Context context) { + super(context); + } + + public IconView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public IconView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public void setDrawable(Drawable d) { + if (mDrawable != null) { + mDrawable.setCallback(null); + } + mDrawable = d; + if (mDrawable != null) { + mDrawable.setCallback(this); + mDrawable.setBounds(0, 0, getWidth(), getHeight()); + } + invalidate(); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + if (mDrawable != null) { + mDrawable.setBounds(0, 0, w, h); + } + } + + @Override + protected boolean verifyDrawable(Drawable who) { + return super.verifyDrawable(who) || who == mDrawable; + } + + @Override + protected void drawableStateChanged() { + super.drawableStateChanged(); + + final Drawable drawable = mDrawable; + if (drawable != null && drawable.isStateful() + && drawable.setState(getDrawableState())) { + invalidateDrawable(drawable); + } + } + + @Override + protected void onDraw(Canvas canvas) { + if (mDrawable != null) { + mDrawable.draw(canvas); + } + } + + @Override + public boolean hasOverlappingRendering() { + return false; + } +} diff --git a/quickstep/src/com/android/quickstep/views/LauncherLayoutListener.java b/quickstep/src/com/android/quickstep/views/LauncherLayoutListener.java index ac34d90b1..c149de54f 100644 --- a/quickstep/src/com/android/quickstep/views/LauncherLayoutListener.java +++ b/quickstep/src/com/android/quickstep/views/LauncherLayoutListener.java @@ -54,7 +54,7 @@ public class LauncherLayoutListener extends AbstractFloatingView @Override public void setInsets(Rect insets) { if (mHandler != null) { - mHandler.onLauncherLayoutChanged(); + mHandler.buildAnimationController(); } } diff --git a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java index 4b4af3fa7..950f7fb99 100644 --- a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java +++ b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java @@ -27,7 +27,6 @@ import android.content.Context; import android.graphics.Canvas; import android.graphics.Rect; import android.os.Build; -import android.support.annotation.AnyThread; import android.util.AttributeSet; import android.util.FloatProperty; import android.view.View; @@ -36,6 +35,8 @@ import android.view.ViewDebug; import com.android.launcher3.DeviceProfile; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherState; +import com.android.quickstep.OverviewInteractionState; +import com.android.quickstep.util.ClipAnimationHelper; import com.android.quickstep.util.LayoutUtils; /** @@ -87,7 +88,11 @@ public class LauncherRecentsView extends RecentsView<Launcher> { public void setTranslationYFactor(float translationFactor) { mTranslationYFactor = translationFactor; - setTranslationY(mTranslationYFactor * (getPaddingBottom() - getPaddingTop())); + setTranslationY(computeTranslationYForFactor(mTranslationYFactor)); + } + + public float computeTranslationYForFactor(float translationYFactor) { + return translationYFactor * (getPaddingBottom() - getPaddingTop()); } @Override @@ -112,8 +117,15 @@ public class LauncherRecentsView extends RecentsView<Launcher> { * Animates adjacent tasks and translate hotseat off screen as well. */ @Override - public AnimatorSet createAdjacentPageAnimForTaskLaunch(TaskView tv) { - AnimatorSet anim = super.createAdjacentPageAnimForTaskLaunch(tv); + public AnimatorSet createAdjacentPageAnimForTaskLaunch(TaskView tv, + ClipAnimationHelper helper) { + AnimatorSet anim = super.createAdjacentPageAnimForTaskLaunch(tv, helper); + + if (!OverviewInteractionState.getInstance(mActivity).isSwipeUpGestureEnabled()) { + // Hotseat doesn't move when opening recents with the button, + // so don't animate it here either. + return anim; + } float allAppsProgressOffscreen = ALL_APPS_PROGRESS_OFF_SCREEN; LauncherState state = mActivity.getStateManager().getState(); @@ -132,8 +144,19 @@ public class LauncherRecentsView extends RecentsView<Launcher> { LayoutUtils.calculateLauncherTaskSize(getContext(), dp, outRect); } - @AnyThread - public static void getPageRect(DeviceProfile grid, Context context, Rect outRect) { - LayoutUtils.calculateLauncherTaskSize(context, grid, outRect); + @Override + protected void onTaskLaunched(boolean success) { + if (success) { + mActivity.getStateManager().goToState(NORMAL, false /* animate */); + } else { + LauncherState state = mActivity.getStateManager().getState(); + mActivity.getAllAppsController().setProgress(state.getVerticalProgress(mActivity)); + } + super.onTaskLaunched(success); + } + + @Override + public boolean shouldUseMultiWindowTaskSizeStrategy() { + return mActivity.isInMultiWindowModeCompat(); } } diff --git a/quickstep/src/com/android/quickstep/views/QuickstepDragIndicator.java b/quickstep/src/com/android/quickstep/views/QuickstepDragIndicator.java deleted file mode 100644 index 5e9cd6e4f..000000000 --- a/quickstep/src/com/android/quickstep/views/QuickstepDragIndicator.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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.views; - -import static com.android.launcher3.LauncherState.ALL_APPS; -import static com.android.launcher3.LauncherState.OVERVIEW; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.View; -import android.view.accessibility.AccessibilityNodeInfo; - -import com.android.launcher3.R; -import com.android.launcher3.userevent.nano.LauncherLogProto.Action; -import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; -import com.android.launcher3.userevent.nano.LauncherLogProto.ControlType; -import com.android.launcher3.views.LauncherDragIndicator; - -public class QuickstepDragIndicator extends LauncherDragIndicator { - - public QuickstepDragIndicator(Context context) { - super(context); - } - - public QuickstepDragIndicator(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public QuickstepDragIndicator(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - } - - private boolean isInOverview() { - return mLauncher.isInState(OVERVIEW); - } - - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setContentDescription(getContext().getString(R.string.all_apps_button_label)); - } - - @Override - protected void initCustomActions(AccessibilityNodeInfo info) { - if (!isInOverview()) { - super.initCustomActions(info); - } - } - - @Override - public void onClick(View view) { - mLauncher.getUserEventDispatcher().logActionOnControl(Action.Touch.TAP, - ControlType.ALL_APPS_BUTTON, - isInOverview() ? ContainerType.TASKSWITCHER : ContainerType.WORKSPACE); - mLauncher.getStateManager().goToState(ALL_APPS); - } -} diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java index 7e81ba902..a6da89f20 100644 --- a/quickstep/src/com/android/quickstep/views/RecentsView.java +++ b/quickstep/src/com/android/quickstep/views/RecentsView.java @@ -16,11 +16,15 @@ package com.android.quickstep.views; +import static com.android.launcher3.BaseActivity.INVISIBLE_BY_STATE_HANDLER; import static com.android.launcher3.anim.Interpolators.ACCEL; import static com.android.launcher3.anim.Interpolators.ACCEL_2; import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN; import static com.android.launcher3.anim.Interpolators.LINEAR; +import static com.android.launcher3.util.SystemUiController.UI_STATE_OVERVIEW; +import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId; +import android.animation.Animator; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.TimeInterpolator; @@ -30,13 +34,13 @@ import android.app.ActivityManager; import android.content.ComponentName; import android.content.Context; import android.content.Intent; -import android.content.SharedPreferences; -import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.graphics.Canvas; import android.graphics.Point; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Build; +import android.os.Handler; +import android.os.UserHandle; import android.text.Layout; import android.text.StaticLayout; import android.text.TextPaint; @@ -49,7 +53,6 @@ import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewDebug; -import android.view.accessibility.AccessibilityEvent; import com.android.launcher3.BaseActivity; import com.android.launcher3.DeviceProfile; @@ -64,42 +67,34 @@ import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction; import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch; import com.android.launcher3.util.PendingAnimation; import com.android.launcher3.util.Themes; +import com.android.quickstep.OverviewCallbacks; import com.android.quickstep.QuickScrubController; -import com.android.quickstep.RecentsAnimationInterpolator; -import com.android.quickstep.RecentsAnimationInterpolator.TaskWindowBounds; import com.android.quickstep.RecentsModel; import com.android.quickstep.TaskUtils; +import com.android.quickstep.util.ClipAnimationHelper; +import com.android.quickstep.util.TaskViewDrawable; import com.android.systemui.shared.recents.model.RecentsTaskLoadPlan; import com.android.systemui.shared.recents.model.RecentsTaskLoader; import com.android.systemui.shared.recents.model.Task; import com.android.systemui.shared.recents.model.TaskStack; import com.android.systemui.shared.recents.model.ThumbnailData; import com.android.systemui.shared.system.ActivityManagerWrapper; +import com.android.systemui.shared.system.BackgroundExecutor; +import com.android.systemui.shared.system.PackageManagerWrapper; import com.android.systemui.shared.system.TaskStackChangeListener; import java.util.ArrayList; +import java.util.function.Consumer; /** * A list of recent tasks. */ @TargetApi(Build.VERSION_CODES.P) -public abstract class RecentsView<T extends BaseActivity> - extends PagedView implements OnSharedPreferenceChangeListener, Insettable { +public abstract class RecentsView<T extends BaseActivity> extends PagedView implements Insettable { - private final Rect mTempRect = new Rect(); - - public static final FloatProperty<RecentsView> CONTENT_ALPHA = - new FloatProperty<RecentsView>("contentAlpha") { - @Override - public void setValue(RecentsView recentsView, float v) { - recentsView.setContentAlpha(v); - } + private static final String TAG = RecentsView.class.getSimpleName(); - @Override - public Float get(RecentsView recentsView) { - return recentsView.mContentAlpha; - } - }; + private final Rect mTempRect = new Rect(); public static final FloatProperty<RecentsView> ADJACENT_SCALE = new FloatProperty<RecentsView>("adjacentScale") { @@ -113,8 +108,12 @@ public abstract class RecentsView<T extends BaseActivity> return recentsView.mAdjacentScale; } }; - private static final String PREF_FLIP_RECENTS = "pref_flip_recents"; + public static final boolean FLIP_RECENTS = true; private static final int DISMISS_TASK_DURATION = 300; + // The threshold at which we update the SystemUI flags when animating from the task into the app + private static final float UPDATE_SYSUI_FLAGS_THRESHOLD = 0.6f; + + private static final float[] sTempFloatArray = new float[3]; protected final T mActivity; private final QuickScrubController mQuickScrubController; @@ -125,19 +124,27 @@ public abstract class RecentsView<T extends BaseActivity> // Keeps track of the previously known visible tasks for purposes of loading/unloading task data private final SparseBooleanArray mHasVisibleTaskData = new SparseBooleanArray(); + private boolean mIsClearAllButtonFullyRevealed; + /** * TODO: Call reloadIdNeeded in onTaskStackChanged. */ private final TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() { @Override public void onTaskSnapshotChanged(int taskId, ThumbnailData snapshot) { + if (!mHandleTaskStackChanges) { + return; + } updateThumbnail(taskId, snapshot); } @Override public void onActivityPinned(String packageName, int userId, int taskId, int stackId) { + if (!mHandleTaskStackChanges) { + return; + } // Check this is for the right user - if (!checkCurrentUserId(userId, false /* debug */)) { + if (!checkCurrentOrManagedUserId(userId, getContext())) { return; } @@ -150,21 +157,71 @@ public abstract class RecentsView<T extends BaseActivity> @Override public void onActivityUnpinned() { + if (!mHandleTaskStackChanges) { + return; + } // TODO: Re-enable layout transitions for addition of the unpinned task reloadIfNeeded(); } + + @Override + public void onTaskRemoved(int taskId) { + if (!mHandleTaskStackChanges) { + return; + } + BackgroundExecutor.get().submit(() -> { + TaskView taskView = getTaskView(taskId); + if (taskView == null) { + return; + } + Handler handler = taskView.getHandler(); + if (handler == null) { + return; + } + + // TODO: Add callbacks from AM reflecting adding/removing from the recents list, and + // remove all these checks + Task.TaskKey taskKey = taskView.getTask().key; + if (PackageManagerWrapper.getInstance().getActivityInfo(taskKey.getComponent(), + taskKey.userId) == null) { + // The package was uninstalled + handler.post(() -> + dismissTask(taskView, true /* animate */, false /* removeTask */)); + } else { + RecentsTaskLoadPlan loadPlan = new RecentsTaskLoadPlan(getContext()); + RecentsTaskLoadPlan.PreloadOptions opts = + new RecentsTaskLoadPlan.PreloadOptions(); + opts.loadTitles = false; + loadPlan.preloadPlan(opts, mModel.getRecentsTaskLoader(), -1, + UserHandle.myUserId()); + if (loadPlan.getTaskStack().findTaskWithId(taskId) == null) { + // The task was removed from the recents list + handler.post(() -> + dismissTask(taskView, true /* animate */, false /* removeTask */)); + } + } + }); + } + + @Override + public void onPinnedStackAnimationStarted() { + // Needed for activities that auto-enter PiP, which will not trigger a remote + // animation to be created + mActivity.clearForceInvisibleFlag(INVISIBLE_BY_STATE_HANDLER); + } }; private int mLoadPlanId = -1; // Only valid until the launcher state changes to NORMAL private int mRunningTaskId = -1; + private boolean mRunningTaskTileHidden; private Task mTmpRunningTask; - private boolean mFirstTaskIconScaledDown = false; + private boolean mRunningTaskIconScaledDown = false; private boolean mOverviewStateEnabled; - private boolean mTaskStackListenerRegistered; + private boolean mHandleTaskStackChanges; private Runnable mNextPageSwitchRunnable; private boolean mSwipeDownShouldLaunchApp; @@ -178,6 +235,8 @@ public abstract class RecentsView<T extends BaseActivity> // Keeps track of task views whose visual state should not be reset private ArraySet<TaskView> mIgnoreResetTaskViews = new ArraySet<>(); + private View mClearAllButton; + // Variables for empty state private final Drawable mEmptyIcon; private final CharSequence mEmptyMessage; @@ -187,6 +246,14 @@ public abstract class RecentsView<T extends BaseActivity> private boolean mShowEmptyMessage; private Layout mEmptyTextLayout; + private BaseActivity.MultiWindowModeChangedListener mMultiWindowModeChangedListener = + (inMultiWindowMode) -> { + if (!inMultiWindowMode && mOverviewStateEnabled) { + // TODO: Re-enable layout transitions for addition of the unpinned task + reloadIfNeeded(); + } + }; + public RecentsView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); setPageSpacing(getResources().getDimensionPixelSize(R.dimen.recents_page_spacing)); @@ -199,7 +266,11 @@ public abstract class RecentsView<T extends BaseActivity> mQuickScrubController = new QuickScrubController(mActivity, this); mModel = RecentsModel.getInstance(context); - onSharedPreferenceChanged(Utilities.getPrefs(context), PREF_FLIP_RECENTS); + mIsRtl = Utilities.isRtl(getResources()); + if (FLIP_RECENTS) { + mIsRtl = !mIsRtl; + } + setLayoutDirection(mIsRtl ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR); mEmptyIcon = context.getDrawable(R.drawable.ic_empty_recents); mEmptyIcon.setCallback(this); @@ -211,17 +282,7 @@ public abstract class RecentsView<T extends BaseActivity> mEmptyMessagePadding = getResources() .getDimensionPixelSize(R.dimen.recents_empty_message_text_padding); setWillNotDraw(false); - } - - @Override - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String s) { - if (s.equals(PREF_FLIP_RECENTS)) { - mIsRtl = Utilities.isRtl(getResources()); - if (sharedPreferences.getBoolean(PREF_FLIP_RECENTS, false)) { - mIsRtl = !mIsRtl; - } - setLayoutDirection(mIsRtl ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR); - } + updateEmptyMessage(); } public boolean isRtl() { @@ -229,14 +290,11 @@ public abstract class RecentsView<T extends BaseActivity> } public TaskView updateThumbnail(int taskId, ThumbnailData thumbnailData) { - for (int i = 0; i < getChildCount(); i++) { - final TaskView taskView = (TaskView) getChildAt(i); - if (taskView.getTask().key.id == taskId) { - taskView.onTaskDataLoaded(taskView.getTask(), thumbnailData); - return taskView; - } + TaskView taskView = getTaskView(taskId); + if (taskView != null) { + taskView.onTaskDataLoaded(taskView.getTask(), thumbnailData); } - return null; + return taskView; } @Override @@ -249,14 +307,16 @@ public abstract class RecentsView<T extends BaseActivity> protected void onAttachedToWindow() { super.onAttachedToWindow(); updateTaskStackListenerState(); - Utilities.getPrefs(getContext()).registerOnSharedPreferenceChangeListener(this); + mActivity.addMultiWindowModeChangedListener(mMultiWindowModeChangedListener); + ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); updateTaskStackListenerState(); - Utilities.getPrefs(getContext()).unregisterOnSharedPreferenceChangeListener(this); + mActivity.removeMultiWindowModeChangedListener(mMultiWindowModeChangedListener); + ActivityManagerWrapper.getInstance().unregisterTaskStackListener(mTaskStackListener); } @Override @@ -271,6 +331,7 @@ public abstract class RecentsView<T extends BaseActivity> loader.unloadTaskData(task); loader.getHighResThumbnailLoader().onTaskInvisible(task); } + onChildViewsChanged(); } public boolean isTaskViewVisible(TaskView tv) { @@ -309,11 +370,69 @@ public abstract class RecentsView<T extends BaseActivity> } } + private int getScrollEnd() { + return mIsRtl ? 0 : mMaxScrollX; + } + + private float calculateClearAllButtonAlpha() { + final int childCount = getChildCount(); + if (mShowEmptyMessage || childCount == 0 || mPageScrolls == null + || childCount != mPageScrolls.length) { + return 0; + } + + final int scrollEnd = getScrollEnd(); + final int oldestChildScroll = getScrollForPage(childCount - 1); + + final int clearAllButtonMotionRange = scrollEnd - oldestChildScroll; + if (clearAllButtonMotionRange == 0) return 0; + + final float alphaUnbound = ((float) (getScrollX() - oldestChildScroll)) / + clearAllButtonMotionRange; + if (alphaUnbound > 1) return 0; + + return Math.max(alphaUnbound, 0); + } + + private void updateClearAllButtonAlpha() { + if (mClearAllButton != null) { + final float alpha = calculateClearAllButtonAlpha(); + mIsClearAllButtonFullyRevealed = alpha == 1; + mClearAllButton.setAlpha(alpha * mContentAlpha); + } + } + + @Override + protected void onScrollChanged(int l, int t, int oldl, int oldt) { + super.onScrollChanged(l, t, oldl, oldt); + updateClearAllButtonAlpha(); + } + + @Override + protected void restoreScrollOnLayout() { + if (mIsClearAllButtonFullyRevealed) { + scrollAndForceFinish(getScrollEnd()); + } else { + super.restoreScrollOnLayout(); + } + } + @Override public boolean onTouchEvent(MotionEvent ev) { - super.onTouchEvent(ev); - // Do not let touch escape to siblings below this view. - return true; + if (ev.getAction() == MotionEvent.ACTION_DOWN && mTouchState == TOUCH_STATE_REST + && mScroller.isFinished() && mIsClearAllButtonFullyRevealed) { + mClearAllButton.getHitRect(mTempRect); + mTempRect.offset(-getLeft(), -getTop()); + if (mTempRect.contains((int) ev.getX(), (int) ev.getY())) { + // If nothing is in motion, let the Clear All button process the event. + return false; + } + } + + if (ev.getAction() == MotionEvent.ACTION_UP && mShowEmptyMessage) { + onAllTasksRemoved(); + } + return super.onTouchEvent(ev); } private void applyLoadPlan(RecentsTaskLoadPlan loadPlan) { @@ -356,7 +475,6 @@ public abstract class RecentsView<T extends BaseActivity> taskView.bind(task); } resetTaskVisuals(); - applyIconScale(false /* animate */); if (oldChildCount != getChildCount()) { mQuickScrubController.snapToNextTaskIfAvailable(); @@ -373,6 +491,10 @@ public abstract class RecentsView<T extends BaseActivity> taskView.resetVisualProperties(); } } + if (mRunningTaskTileHidden) { + setRunningTaskHidden(mRunningTaskTileHidden); + } + applyIconScale(false /* animate */); updateCurveProperties(); // Update the set of visible task's data @@ -380,18 +502,13 @@ public abstract class RecentsView<T extends BaseActivity> } private void updateTaskStackListenerState() { - boolean registerStackListener = mOverviewStateEnabled && isAttachedToWindow() + boolean handleTaskStackChanges = mOverviewStateEnabled && isAttachedToWindow() && getWindowVisibility() == VISIBLE; - if (registerStackListener != mTaskStackListenerRegistered) { - if (registerStackListener) { - ActivityManagerWrapper.getInstance() - .registerTaskStackListener(mTaskStackListener); + if (handleTaskStackChanges != mHandleTaskStackChanges) { + mHandleTaskStackChanges = handleTaskStackChanges; + if (handleTaskStackChanges) { reloadIfNeeded(); - } else { - ActivityManagerWrapper.getInstance() - .unregisterTaskStackListener(mTaskStackListener); } - mTaskStackListenerRegistered = registerStackListener; } } @@ -400,15 +517,20 @@ public abstract class RecentsView<T extends BaseActivity> mInsets.set(insets); DeviceProfile dp = mActivity.getDeviceProfile(); getTaskSize(dp, mTempRect); + mTempRect.top -= getResources() .getDimensionPixelSize(R.dimen.task_thumbnail_top_margin); setPadding(mTempRect.left - mInsets.left, mTempRect.top - mInsets.top, - dp.widthPx - mTempRect.right - mInsets.right, - dp.heightPx - mTempRect.bottom - mInsets.bottom); + dp.availableWidthPx + mInsets.left - mTempRect.right, + dp.availableHeightPx + mInsets.top - mTempRect.bottom); } protected abstract void getTaskSize(DeviceProfile dp, Rect outRect); + public void getTaskSize(Rect outRect) { + getTaskSize(mActivity.getDeviceProfile(), outRect); + } + @Override protected boolean computeScrollHelper() { boolean scrolling = super.computeScrollHelper(); @@ -506,13 +628,16 @@ public abstract class RecentsView<T extends BaseActivity> mHasVisibleTaskData.clear(); } - protected abstract void onAllTasksRemoved(); public void reset() { - unloadVisibleTaskData(); mRunningTaskId = -1; + mRunningTaskTileHidden = false; + + unloadVisibleTaskData(); setCurrentPage(0); + + OverviewCallbacks.get(getContext()).onResetOverview(); } /** @@ -546,12 +671,17 @@ public abstract class RecentsView<T extends BaseActivity> new ActivityManager.TaskDescription(), 0, new ComponentName("", ""), false); taskView.bind(mTmpRunningTask); } - setCurrentTask(mRunningTaskId); + setCurrentTask(runningTaskId); + } - // Hide the task that we are animating into, ignore if there is no associated task (ie. the - // assistant) - if (getPageAt(mCurrentPage) != null) { - getPageAt(mCurrentPage).setAlpha(0); + /** + * Hides the tile associated with {@link #mRunningTaskId} + */ + public void setRunningTaskHidden(boolean isHidden) { + mRunningTaskTileHidden = isHidden; + TaskView runningTask = getTaskView(mRunningTaskId); + if (runningTask != null) { + runningTask.setAlpha(isHidden ? 0 : mContentAlpha); } } @@ -559,7 +689,15 @@ public abstract class RecentsView<T extends BaseActivity> * Similar to {@link #showTask(int)} but does not put any restrictions on the first tile. */ public void setCurrentTask(int runningTaskId) { + boolean runningTaskTileHidden = mRunningTaskTileHidden; + boolean runningTaskIconScaledDown = mRunningTaskIconScaledDown; + + setRunningTaskIconScaledDown(false, false); + setRunningTaskHidden(false); mRunningTaskId = runningTaskId; + setRunningTaskIconScaledDown(runningTaskIconScaledDown, false); + setRunningTaskHidden(runningTaskTileHidden); + setCurrentPage(0); // Load the tasks (if the loading is already @@ -587,22 +725,22 @@ public abstract class RecentsView<T extends BaseActivity> return mQuickScrubController; } - public void setFirstTaskIconScaledDown(boolean isScaledDown, boolean animate) { - if (mFirstTaskIconScaledDown == isScaledDown) { + public void setRunningTaskIconScaledDown(boolean isScaledDown, boolean animate) { + if (mRunningTaskIconScaledDown == isScaledDown) { return; } - mFirstTaskIconScaledDown = isScaledDown; + mRunningTaskIconScaledDown = isScaledDown; applyIconScale(animate); } private void applyIconScale(boolean animate) { - float scale = mFirstTaskIconScaledDown ? 0 : 1; - TaskView firstTask = (TaskView) getChildAt(0); + float scale = mRunningTaskIconScaledDown ? 0 : 1; + TaskView firstTask = getTaskView(mRunningTaskId); if (firstTask != null) { if (animate) { - firstTask.animateIconToScale(scale); + firstTask.animateIconToScaleAndDim(scale); } else { - firstTask.setIconScale(scale); + firstTask.setIconScaleAndDim(scale); } } } @@ -640,8 +778,26 @@ public abstract class RecentsView<T extends BaseActivity> mIgnoreResetTaskViews.remove(taskView); } + private void addDismissedTaskAnimations(View taskView, AnimatorSet anim, long duration) { + addAnim(ObjectAnimator.ofFloat(taskView, ALPHA, 0), duration, ACCEL_2, anim); + addAnim(ObjectAnimator.ofFloat(taskView, TRANSLATION_Y, -taskView.getHeight()), + duration, LINEAR, anim); + } + + private void removeTask(Task task, int index, PendingAnimation.OnEndListener onEndListener, + boolean shouldLog) { + if (task != null) { + ActivityManagerWrapper.getInstance().removeTask(task.key.id); + if (shouldLog) { + mActivity.getUserEventDispatcher().logTaskLaunchOrDismiss( + onEndListener.logAction, Direction.UP, index, + TaskUtils.getComponentKeyForTask(task.key)); + } + } + } + public PendingAnimation createTaskDismissAnimation(TaskView taskView, boolean animateTaskView, - boolean removeTask, long duration) { + boolean shouldRemoveTask, long duration) { if (FeatureFlags.IS_DOGFOOD_BUILD && mPendingAnimation != null) { throw new IllegalStateException("Another pending animation is still running"); } @@ -659,26 +815,43 @@ public abstract class RecentsView<T extends BaseActivity> int[] newScroll = new int[count]; getPageScrolls(newScroll, false, (v) -> v.getVisibility() != GONE && v != taskView); - int maxScrollDiff = 0; - int lastPage = mIsRtl ? 0 : count - 1; - if (getChildAt(lastPage) == taskView) { - if (count > 1) { - int secondLastPage = mIsRtl ? 1 : count - 2; - maxScrollDiff = oldScroll[lastPage] - newScroll[secondLastPage]; - } + int scrollDiffPerPage = 0; + int leftmostPage = mIsRtl ? count -1 : 0; + int rightmostPage = mIsRtl ? 0 : count - 1; + if (count > 1) { + int secondRightmostPage = mIsRtl ? 1 : count - 2; + scrollDiffPerPage = oldScroll[rightmostPage] - oldScroll[secondRightmostPage]; } + int draggedIndex = indexOfChild(taskView); boolean needsCurveUpdates = false; for (int i = 0; i < count; i++) { View child = getChildAt(i); if (child == taskView) { if (animateTaskView) { - addAnim(ObjectAnimator.ofFloat(taskView, ALPHA, 0), duration, ACCEL_2, anim); - addAnim(ObjectAnimator.ofFloat(taskView, TRANSLATION_Y, -taskView.getHeight()), - duration, LINEAR, anim); + addDismissedTaskAnimations(taskView, anim, duration); } } else { - int scrollDiff = newScroll[i] - oldScroll[i] + maxScrollDiff; + // If we just take newScroll - oldScroll, everything to the right of dragged task + // translates to the left. We need to offset this in some cases: + // - In RTL, add page offset to all pages, since we want pages to move to the right + // Additionally, add a page offset if: + // - Current page is rightmost page (leftmost for RTL) + // - Dragging an adjacent page on the left side (right side for RTL) + int offset = mIsRtl ? scrollDiffPerPage : 0; + if (mCurrentPage == draggedIndex) { + int lastPage = mIsRtl ? leftmostPage : rightmostPage; + if (mCurrentPage == lastPage) { + offset += mIsRtl ? -scrollDiffPerPage : scrollDiffPerPage; + } + } else { + // Dragging an adjacent page. + int negativeAdjacent = mCurrentPage - 1; // (Right in RTL, left in LTR) + if (draggedIndex == negativeAdjacent) { + offset += mIsRtl ? -scrollDiffPerPage : scrollDiffPerPage; + } + } + int scrollDiff = newScroll[i] - oldScroll[i] + offset; if (scrollDiff != 0) { addAnim(ObjectAnimator.ofFloat(child, TRANSLATION_X, scrollDiff), duration, ACCEL, anim); @@ -701,18 +874,18 @@ public abstract class RecentsView<T extends BaseActivity> mPendingAnimation = pendingAnimation; mPendingAnimation.addEndListener((onEndListener) -> { if (onEndListener.isSuccess) { - if (removeTask) { - Task task = taskView.getTask(); - if (task != null) { - ActivityManagerWrapper.getInstance().removeTask(task.key.id); - mActivity.getUserEventDispatcher().logTaskLaunchOrDismiss( - onEndListener.logAction, Direction.UP, - TaskUtils.getComponentKeyForTask(task.key)); - } + if (shouldRemoveTask) { + removeTask(taskView.getTask(), draggedIndex, onEndListener, true); + } + int pageToSnapTo = mCurrentPage; + if (draggedIndex < pageToSnapTo) { + pageToSnapTo -= 1; } removeView(taskView); if (getChildCount() == 0) { onAllTasksRemoved(); + } else if (!mIsClearAllButtonFullyRevealed) { + snapToPageImmediately(pageToSnapTo); } } resetTaskVisuals(); @@ -721,6 +894,33 @@ public abstract class RecentsView<T extends BaseActivity> return pendingAnimation; } + public PendingAnimation createAllTasksDismissAnimation(long duration) { + if (FeatureFlags.IS_DOGFOOD_BUILD && mPendingAnimation != null) { + throw new IllegalStateException("Another pending animation is still running"); + } + AnimatorSet anim = new AnimatorSet(); + PendingAnimation pendingAnimation = new PendingAnimation(anim); + + int count = getChildCount(); + for (int i = 0; i < count; i++) { + addDismissedTaskAnimations(getChildAt(i), anim, duration); + } + + mPendingAnimation = pendingAnimation; + mPendingAnimation.addEndListener((onEndListener) -> { + if (onEndListener.isSuccess) { + while (getChildCount() != 0) { + TaskView taskView = getPageAt(getChildCount() - 1); + removeTask(taskView.getTask(), -1, onEndListener, false); + removeView(taskView); + } + onAllTasksRemoved(); + } + mPendingAnimation = null; + }); + return pendingAnimation; + } + private static void addAnim(ObjectAnimator anim, long duration, TimeInterpolator interpolator, AnimatorSet set) { anim.setDuration(duration).setInterpolator(interpolator); @@ -744,9 +944,7 @@ public abstract class RecentsView<T extends BaseActivity> } } - public void dismissTask(TaskView taskView, boolean animateTaskView, boolean removeTask) { - PendingAnimation pendingAnim = createTaskDismissAnimation(taskView, animateTaskView, - removeTask, DISMISS_TASK_DURATION); + private void runDismissAnimation(PendingAnimation pendingAnim) { AnimatorPlaybackController controller = AnimatorPlaybackController.wrap( pendingAnim.anim, DISMISS_TASK_DURATION); controller.dispatchOnStart(); @@ -755,6 +953,15 @@ public abstract class RecentsView<T extends BaseActivity> controller.start(); } + public void dismissTask(TaskView taskView, boolean animateTaskView, boolean removeTask) { + runDismissAnimation(createTaskDismissAnimation(taskView, animateTaskView, removeTask, + DISMISS_TASK_DURATION)); + } + + public void dismissAllTasks() { + runDismissAnimation(createAllTasksDismissAnimation(DISMISS_TASK_DURATION)); + } + @Override public boolean dispatchKeyEvent(KeyEvent event) { if (event.getAction() == KeyEvent.ACTION_DOWN) { @@ -789,21 +996,24 @@ public abstract class RecentsView<T extends BaseActivity> snapToPageRelative(1); } - public void setContentAlpha(float alpha) { - if (mContentAlpha == alpha) { - return; - } + public float getContentAlpha() { + return mContentAlpha; + } + public void setContentAlpha(float alpha) { + alpha = Utilities.boundToRange(alpha, 0, 1); mContentAlpha = alpha; for (int i = getChildCount() - 1; i >= 0; i--) { - getChildAt(i).setAlpha(alpha); + TaskView child = getPageAt(i); + if (!mRunningTaskTileHidden || child.getTask().key.id != mRunningTaskId) { + getChildAt(i).setAlpha(alpha); + } } int alphaInt = Math.round(alpha * 255); mEmptyMessagePaint.setAlpha(alphaInt); mEmptyIcon.setAlpha(alphaInt); - - setVisibility(alpha > 0 ? VISIBLE : GONE); + updateClearAllButtonAlpha(); } public void setAdjacentScale(float adjacentScale) { @@ -815,15 +1025,13 @@ public abstract class RecentsView<T extends BaseActivity> if (currTask == null) { return; } - currTask.setScaleX(mAdjacentScale); - currTask.setScaleY(mAdjacentScale); + currTask.setZoomScale(mAdjacentScale); if (mCurrentPage - 1 >= 0) { TaskView adjacentTask = getPageAt(mCurrentPage - 1); float[] scaleAndTranslation = getAdjacentScaleAndTranslation(currTask, adjacentTask, mAdjacentScale, 0); - adjacentTask.setScaleX(scaleAndTranslation[0]); - adjacentTask.setScaleY(scaleAndTranslation[0]); + adjacentTask.setZoomScale(scaleAndTranslation[0]); adjacentTask.setTranslationX(-scaleAndTranslation[1]); adjacentTask.setTranslationY(scaleAndTranslation[2]); } @@ -831,8 +1039,7 @@ public abstract class RecentsView<T extends BaseActivity> TaskView adjacentTask = getPageAt(mCurrentPage + 1); float[] scaleAndTranslation = getAdjacentScaleAndTranslation(currTask, adjacentTask, mAdjacentScale, 0); - adjacentTask.setScaleX(scaleAndTranslation[0]); - adjacentTask.setScaleY(scaleAndTranslation[0]); + adjacentTask.setZoomScale(scaleAndTranslation[0]); adjacentTask.setTranslationX(scaleAndTranslation[1]); adjacentTask.setTranslationY(scaleAndTranslation[2]); } @@ -841,11 +1048,10 @@ public abstract class RecentsView<T extends BaseActivity> private float[] getAdjacentScaleAndTranslation(TaskView currTask, TaskView adjacentTask, float currTaskToScale, float currTaskToTranslationY) { float displacement = currTask.getWidth() * (currTaskToScale - currTask.getCurveScale()); - return new float[] { - currTaskToScale * adjacentTask.getCurveScale(), - mIsRtl ? -displacement : displacement, - currTaskToTranslationY - }; + sTempFloatArray[0] = currTaskToScale; + sTempFloatArray[1] = mIsRtl ? -displacement : displacement; + sTempFloatArray[2] = currTaskToTranslationY; + return sTempFloatArray; } @Override @@ -853,6 +1059,7 @@ public abstract class RecentsView<T extends BaseActivity> super.onViewAdded(child); child.setAlpha(mContentAlpha); setAdjacentScale(mAdjacentScale); + onChildViewsChanged(); } @Override @@ -883,10 +1090,11 @@ public abstract class RecentsView<T extends BaseActivity> boolean hasValidSize = getWidth() > 0 && getHeight() > 0; if (sizeChanged && hasValidSize) { mEmptyTextLayout = null; + mLastMeasureSize.set(getWidth(), getHeight()); } + updateClearAllButtonAlpha(); if (mShowEmptyMessage && hasValidSize && mEmptyTextLayout == null) { - mLastMeasureSize.set(getWidth(), getHeight()); int availableWidth = mLastMeasureSize.x - mEmptyMessagePadding - mEmptyMessagePadding; mEmptyTextLayout = StaticLayout.Builder.obtain(mEmptyMessage, 0, mEmptyMessage.length(), mEmptyMessagePaint, availableWidth) @@ -909,8 +1117,13 @@ public abstract class RecentsView<T extends BaseActivity> protected void maybeDrawEmptyMessage(Canvas canvas) { if (mShowEmptyMessage && mEmptyTextLayout != null) { - mEmptyIcon.draw(canvas); + // Offset to center in the visible (non-padded) part of RecentsView + mTempRect.set(mInsets.left + getPaddingLeft(), mInsets.top + getPaddingTop(), + mInsets.right + getPaddingRight(), mInsets.bottom + getPaddingBottom()); canvas.save(); + canvas.translate(getScrollX() + (mTempRect.left - mTempRect.right) / 2, + (mTempRect.top - mTempRect.bottom) / 2); + mEmptyIcon.draw(canvas); canvas.translate(mEmptyMessagePadding, mEmptyIcon.getBounds().bottom + mEmptyMessagePadding); mEmptyTextLayout.draw(canvas); @@ -924,17 +1137,18 @@ public abstract class RecentsView<T extends BaseActivity> * If launching one of the adjacent tasks, parallax the center task and other adjacent task * to the right. */ - public AnimatorSet createAdjacentPageAnimForTaskLaunch(TaskView tv) { + public AnimatorSet createAdjacentPageAnimForTaskLaunch( + TaskView tv, ClipAnimationHelper clipAnimationHelper) { AnimatorSet anim = new AnimatorSet(); int taskIndex = indexOfChild(tv); int centerTaskIndex = getCurrentPage(); boolean launchingCenterTask = taskIndex == centerTaskIndex; - TaskWindowBounds endInterpolation = tv.getRecentsInterpolator().interpolate(1); - float toScale = endInterpolation.taskScale; - float toTranslationY = endInterpolation.taskY; - + float toScale = clipAnimationHelper.getSourceRect().width() + / clipAnimationHelper.getTargetRect().width(); + float toTranslationY = clipAnimationHelper.getSourceRect().centerY() + - clipAnimationHelper.getTargetRect().centerY(); if (launchingCenterTask) { TaskView centerTask = getPageAt(centerTaskIndex); if (taskIndex - 1 >= 0) { @@ -968,68 +1182,143 @@ public abstract class RecentsView<T extends BaseActivity> return anim; } - private ObjectAnimator createAnimForChild(View child, float[] toScaleAndTranslation) { - return ObjectAnimator.ofPropertyValuesHolder(child, + private Animator createAnimForChild(TaskView child, float[] toScaleAndTranslation) { + AnimatorSet anim = new AnimatorSet(); + anim.play(ObjectAnimator.ofFloat(child, TaskView.ZOOM_SCALE, toScaleAndTranslation[0])); + anim.play(ObjectAnimator.ofPropertyValuesHolder(child, new PropertyListBuilder() - .scale(child.getScaleX() * toScaleAndTranslation[0]) .translationX(toScaleAndTranslation[1]) .translationY(toScaleAndTranslation[2]) - .build()); + .build())); + return anim; } public PendingAnimation createTaskLauncherAnimation(TaskView tv, long duration) { if (FeatureFlags.IS_DOGFOOD_BUILD && mPendingAnimation != null) { throw new IllegalStateException("Another pending animation is still running"); } - AnimatorSet anim = createAdjacentPageAnimForTaskLaunch(tv); int count = getChildCount(); if (count == 0) { - return new PendingAnimation(anim); - } - - final RecentsAnimationInterpolator recentsInterpolator = tv.getRecentsInterpolator(); - ValueAnimator targetViewAnim = ValueAnimator.ofFloat(0, 1); - targetViewAnim.addUpdateListener((animation) -> { - float percent = animation.getAnimatedFraction(); - TaskWindowBounds tw = recentsInterpolator.interpolate(percent); - tv.setScaleX(tw.taskScale); - tv.setScaleY(tw.taskScale); - tv.setTranslationX(tw.taskX); - tv.setTranslationY(tw.taskY); + return new PendingAnimation(new AnimatorSet()); + } + + tv.setVisibility(INVISIBLE); + int targetSysUiFlags = tv.getThumbnail().getSysUiStatusNavFlags(); + TaskViewDrawable drawable = new TaskViewDrawable(tv, this); + getOverlay().add(drawable); + + ObjectAnimator drawableAnim = + ObjectAnimator.ofFloat(drawable, TaskViewDrawable.PROGRESS, 1, 0); + drawableAnim.setInterpolator(LINEAR); + drawableAnim.addUpdateListener((animator) -> { + // Once we pass a certain threshold, update the sysui flags to match the target tasks' + // flags + mActivity.getSystemUiController().updateUiState(UI_STATE_OVERVIEW, + animator.getAnimatedFraction() > UPDATE_SYSUI_FLAGS_THRESHOLD + ? targetSysUiFlags + : 0); }); - anim.play(targetViewAnim); + + AnimatorSet anim = createAdjacentPageAnimForTaskLaunch(tv, + drawable.getClipAnimationHelper()); + anim.play(drawableAnim); anim.setDuration(duration); + Consumer<Boolean> onTaskLaunchFinish = (result) -> { + onTaskLaunched(result); + tv.setVisibility(VISIBLE); + getOverlay().remove(drawable); + }; + mPendingAnimation = new PendingAnimation(anim); mPendingAnimation.addEndListener((onEndListener) -> { if (onEndListener.isSuccess) { - tv.launchTask(false); + Consumer<Boolean> onLaunchResult = (result) -> { + onTaskLaunchFinish.accept(result); + if (!result) { + tv.notifyTaskLaunchFailed(TAG); + } + }; + tv.launchTask(false, onLaunchResult, getHandler()); Task task = tv.getTask(); if (task != null) { mActivity.getUserEventDispatcher().logTaskLaunchOrDismiss( - onEndListener.logAction, Direction.DOWN, + onEndListener.logAction, Direction.DOWN, indexOfChild(tv), TaskUtils.getComponentKeyForTask(task.key)); } } else { - resetTaskVisuals(); + onTaskLaunchFinish.accept(false); } mPendingAnimation = null; }); return mPendingAnimation; } + public abstract boolean shouldUseMultiWindowTaskSizeStrategy(); + + protected void onTaskLaunched(boolean success) { + resetTaskVisuals(); + } + @Override protected void notifyPageSwitchListener(int prevPage) { super.notifyPageSwitchListener(prevPage); - View currChild = getChildAt(mCurrentPage); - if (currChild != null) { - currChild.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); - } + loadVisibleTaskData(); } @Override protected String getCurrentPageDescription() { return ""; } + + private int additionalScrollForClearAllButton() { + return (int) getResources().getDimension( + R.dimen.clear_all_container_width) - getPaddingEnd(); + } + + @Override + protected int computeMaxScrollX() { + if (getChildCount() == 0) { + return super.computeMaxScrollX(); + } + + // Allow a clear_all_container_width-sized gap after the last task. + return super.computeMaxScrollX() + (mIsRtl ? 0 : additionalScrollForClearAllButton()); + } + + @Override + protected int offsetForPageScrolls() { + return mIsRtl ? additionalScrollForClearAllButton() : 0; + } + + public void setClearAllButton(View clearAllButton) { + mClearAllButton = clearAllButton; + updateClearAllButtonAlpha(); + } + + private void onChildViewsChanged() { + final int childCount = getChildCount(); + mClearAllButton.setVisibility(childCount == 0 ? INVISIBLE : VISIBLE); + } + + public void revealClearAllButton() { + scrollTo(mIsRtl ? 0 : computeMaxScrollX(), 0); + } + + @Override + public void addChildrenForAccessibility(ArrayList<View> outChildren) { + if (FLIP_RECENTS) { + for (int i = getChildCount() - 1; i >= 0; --i) { + outChildren.add(getChildAt(i)); + } + } else { + super.addChildrenForAccessibility(outChildren); + } + } + + @Override + protected boolean isPageOrderFlipped() { + return FLIP_RECENTS; + } } diff --git a/quickstep/src/com/android/quickstep/views/RecentsViewContainer.java b/quickstep/src/com/android/quickstep/views/RecentsViewContainer.java new file mode 100644 index 000000000..429432b2f --- /dev/null +++ b/quickstep/src/com/android/quickstep/views/RecentsViewContainer.java @@ -0,0 +1,107 @@ +/* + * 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.views; + +import static com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch.TAP; +import static com.android.launcher3.userevent.nano.LauncherLogProto.ControlType.CLEAR_ALL_BUTTON; + +import android.content.Context; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.util.FloatProperty; +import android.view.Gravity; +import android.view.MotionEvent; + +import com.android.launcher3.InsettableFrameLayout; +import com.android.launcher3.R; + +public class RecentsViewContainer extends InsettableFrameLayout { + public static final FloatProperty<RecentsViewContainer> CONTENT_ALPHA = + new FloatProperty<RecentsViewContainer>("contentAlpha") { + @Override + public void setValue(RecentsViewContainer view, float v) { + view.setContentAlpha(v); + } + + @Override + public Float get(RecentsViewContainer view) { + return view.mRecentsView.getContentAlpha(); + } + }; + + private final Rect mTempRect = new Rect(); + + private RecentsView mRecentsView; + private ClearAllButton mClearAllButton; + + public RecentsViewContainer(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + mClearAllButton = findViewById(R.id.clear_all_button); + mClearAllButton.setOnClickListener((v) -> { + mRecentsView.mActivity.getUserEventDispatcher() + .logActionOnControl(TAP, CLEAR_ALL_BUTTON); + mRecentsView.dismissAllTasks(); + }); + + mRecentsView = findViewById(R.id.overview_panel); + final InsettableFrameLayout.LayoutParams params = + (InsettableFrameLayout.LayoutParams) mClearAllButton.getLayoutParams(); + params.gravity = Gravity.TOP | (RecentsView.FLIP_RECENTS ? Gravity.START : Gravity.END); + mClearAllButton.setLayoutParams(params); + mClearAllButton.forceHasOverlappingRendering(false); + + mRecentsView.setClearAllButton(mClearAllButton); + mClearAllButton.setRecentsView(mRecentsView); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + + mRecentsView.getTaskSize(mTempRect); + + mClearAllButton.setTranslationX( + (mRecentsView.isRtl() ? 1 : -1) * + (getResources().getDimension(R.dimen.clear_all_container_width) + - mClearAllButton.getMeasuredWidth()) / 2); + mClearAllButton.setTranslationY( + mTempRect.top + (mTempRect.height() - mClearAllButton.getMeasuredHeight()) / 2 + - mClearAllButton.getTop()); + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + super.onTouchEvent(ev); + // Do not let touch escape to siblings below this view. This prevents scrolling of the + // workspace while in Recents. + return true; + } + + public void setContentAlpha(float alpha) { + if (alpha == mRecentsView.getContentAlpha()) { + return; + } + mRecentsView.setContentAlpha(alpha); + setVisibility(alpha > 0 ? VISIBLE : GONE); + } +}
\ No newline at end of file diff --git a/quickstep/src/com/android/quickstep/views/ShelfScrimView.java b/quickstep/src/com/android/quickstep/views/ShelfScrimView.java new file mode 100644 index 000000000..24afd4868 --- /dev/null +++ b/quickstep/src/com/android/quickstep/views/ShelfScrimView.java @@ -0,0 +1,259 @@ +/* + * 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.views; + +import static android.support.v4.graphics.ColorUtils.compositeColors; +import static android.support.v4.graphics.ColorUtils.setAlphaComponent; + +import static com.android.launcher3.LauncherState.OVERVIEW; +import static com.android.launcher3.anim.Interpolators.ACCEL_2; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Path.Direction; +import android.graphics.Path.Op; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; +import android.util.AttributeSet; + +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.R; +import com.android.launcher3.uioverrides.OverviewState; +import com.android.launcher3.util.Themes; +import com.android.launcher3.views.ScrimView; + +/** + * Scrim used for all-apps and shelf in Overview + * In transposed layout, it behaves as a simple color scrim. + * In portrait layout, it draws a rounded rect such that + * From normal state to overview state, the shelf just fades in and does not move + * From overview state to all-apps state the shelf moves up and fades in to cover the screen + */ +public class ShelfScrimView extends ScrimView { + + private static final int THRESHOLD_ALPHA_DARK = 102; + private static final int THRESHOLD_ALPHA_LIGHT = 46; + private static final int THRESHOLD_ALPHA_SUPER_LIGHT = 128; + private static final int CLEAR_ALL_TASKS = R.string.recents_clear_all; + + // In transposed layout, we simply draw a flat color. + private boolean mDrawingFlatColor; + + // For shelf mode + private final int mEndAlpha; + private final int mThresholdAlpha; + private final float mRadius; + private final float mMaxScrimAlpha; + private final Paint mPaint; + + // Max vertical progress after which the scrim stops moving. + private float mMoveThreshold; + // Minimum visible size of the scrim. + private int mMinSize; + + private float mScrimMoveFactor = 0; + private int mShelfColor; + private int mRemainingScreenColor; + + private final Path mTempPath = new Path(); + private final Path mRemainingScreenPath = new Path(); + private boolean mRemainingScreenPathValid = false; + + public ShelfScrimView(Context context, AttributeSet attrs) { + super(context, attrs); + mMaxScrimAlpha = OVERVIEW.getWorkspaceScrimAlpha(mLauncher); + + mEndAlpha = Color.alpha(mEndScrim); + if (Themes.getAttrBoolean(mLauncher, R.attr.isMainColorDark)) { + mThresholdAlpha = THRESHOLD_ALPHA_DARK; + } else if (Themes.getAttrBoolean(mLauncher, R.attr.isWorkspaceDarkText)) { + mThresholdAlpha = THRESHOLD_ALPHA_SUPER_LIGHT; + } else { + mThresholdAlpha = THRESHOLD_ALPHA_LIGHT; + } + mRadius = mLauncher.getResources().getDimension(R.dimen.shelf_surface_radius); + mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + + // Just assume the easiest UI for now, until we have the proper layout information. + mDrawingFlatColor = true; + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + mRemainingScreenPathValid = false; + } + + @Override + public void reInitUi() { + DeviceProfile dp = mLauncher.getDeviceProfile(); + mDrawingFlatColor = dp.isVerticalBarLayout(); + + if (!mDrawingFlatColor) { + float swipeLength = OverviewState.getDefaultSwipeHeight(mLauncher); + mMoveThreshold = 1 - swipeLength / mLauncher.getAllAppsController().getShiftRange(); + mMinSize = dp.hotseatBarSizePx + dp.getInsets().bottom; + mRemainingScreenPathValid = false; + updateColors(); + } + updateDragHandleAlpha(); + invalidate(); + } + + @Override + public void updateColors() { + super.updateColors(); + if (mDrawingFlatColor) { + return; + } + + if (mProgress >= mMoveThreshold) { + mScrimMoveFactor = 1; + + if (mProgress >= 1) { + mShelfColor = 0; + } else { + int alpha = Math.round(mThresholdAlpha * ACCEL_2.getInterpolation( + (1 - mProgress) / (1 - mMoveThreshold))); + mShelfColor = setAlphaComponent(mEndScrim, alpha); + } + + mRemainingScreenColor = 0; + } else if (mProgress <= 0) { + mScrimMoveFactor = 0; + mShelfColor = mCurrentFlatColor; + mRemainingScreenColor = 0; + + } else { + mScrimMoveFactor = mProgress / mMoveThreshold; + mRemainingScreenColor = setAlphaComponent(mScrimColor, + Math.round((1 - mScrimMoveFactor) * mMaxScrimAlpha * 255)); + + // Merge the remainingScreenColor and shelfColor in one to avoid overdraw. + int alpha = mEndAlpha - Math.round((mEndAlpha - mThresholdAlpha) * mScrimMoveFactor); + mShelfColor = compositeColors(setAlphaComponent(mEndScrim, alpha), + mRemainingScreenColor); + } + } + + @Override + protected void updateDragHandleAlpha() { + if (mDrawingFlatColor) { + super.updateDragHandleAlpha(); + } else if (mDragHandle != null) { + mDragHandle.setAlpha(255); + } + } + + @Override + protected void onDraw(Canvas canvas) { + float translate = drawBackground(canvas); + + if (mDragHandle != null) { + canvas.translate(0, -translate); + mDragHandle.draw(canvas); + canvas.translate(0, translate); + } + } + + private float drawBackground(Canvas canvas) { + if (mDrawingFlatColor) { + if (mCurrentFlatColor != 0) { + canvas.drawColor(mCurrentFlatColor); + } + return 0; + } + + if (mShelfColor == 0) { + return 0; + } else if (mScrimMoveFactor <= 0) { + canvas.drawColor(mShelfColor); + return getHeight(); + } + + float minTop = getHeight() - mMinSize; + float top = minTop * mScrimMoveFactor - mDragHandleSize; + + // Draw the scrim over the remaining screen if needed. + if (mRemainingScreenColor != 0) { + if (!mRemainingScreenPathValid) { + mTempPath.reset(); + // Using a arbitrary '+10' in the bottom to avoid any left-overs at the + // corners due to rounding issues. + mTempPath.addRoundRect(0, minTop, getWidth(), getHeight() + mRadius + 10, + mRadius, mRadius, Direction.CW); + + mRemainingScreenPath.reset(); + mRemainingScreenPath.addRect(0, 0, getWidth(), getHeight(), Direction.CW); + mRemainingScreenPath.op(mTempPath, Op.DIFFERENCE); + } + + float offset = minTop - top; + canvas.translate(0, -offset); + mPaint.setColor(mRemainingScreenColor); + canvas.drawPath(mRemainingScreenPath, mPaint); + canvas.translate(0, offset); + } + + mPaint.setColor(mShelfColor); + canvas.drawRoundRect(0, top, getWidth(), getHeight() + mRadius, + mRadius, mRadius, mPaint); + return minTop - mDragHandleSize - top; + } + + @NonNull + @Override + protected AccessibilityHelper createAccessibilityHelper() { + return new ShelfScrimAccessibilityHelper(); + } + + protected class ShelfScrimAccessibilityHelper extends AccessibilityHelper { + @Override + protected void onPopulateNodeForVirtualView(int virtualViewId, + AccessibilityNodeInfoCompat node) { + super.onPopulateNodeForVirtualView(virtualViewId, node); + + if (mLauncher.isInState(OVERVIEW)) { + final RecentsView overviewPanel = mLauncher.getOverviewPanel(); + if (overviewPanel.getChildCount() != 0) { + node.addAction( + new AccessibilityNodeInfoCompat.AccessibilityActionCompat( + CLEAR_ALL_TASKS, + getContext().getText(CLEAR_ALL_TASKS))); + } + } + } + + @Override + protected boolean onPerformActionForVirtualView( + int virtualViewId, int action, Bundle arguments) { + if (super.onPerformActionForVirtualView(virtualViewId, action, arguments)) return true; + + if (action == CLEAR_ALL_TASKS) { + if (mLauncher.isInState(OVERVIEW)) { + mLauncher.<RecentsView>getOverviewPanel().dismissAllTasks(); + } + return true; + } + + return false; + } + } +} diff --git a/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java b/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java index 58b7db7b2..128a19e06 100644 --- a/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java +++ b/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java @@ -16,6 +16,8 @@ package com.android.quickstep.views; +import static com.android.systemui.shared.system.WindowManagerWrapper.WINDOWING_MODE_FULLSCREEN; + import android.content.Context; import android.content.res.Configuration; import android.graphics.Bitmap; @@ -28,12 +30,15 @@ import android.graphics.Paint; import android.graphics.Rect; import android.graphics.Shader; import android.util.AttributeSet; +import android.util.FloatProperty; +import android.util.Property; import android.view.View; import com.android.launcher3.BaseActivity; import com.android.launcher3.DeviceProfile; import com.android.launcher3.R; import com.android.launcher3.config.FeatureFlags; +import com.android.launcher3.util.SystemUiController; import com.android.quickstep.TaskOverlayFactory; import com.android.quickstep.TaskOverlayFactory.TaskOverlay; import com.android.systemui.shared.recents.model.Task; @@ -46,6 +51,19 @@ public class TaskThumbnailView extends View { private static final LightingColorFilter[] sDimFilterCache = new LightingColorFilter[256]; + public static final Property<TaskThumbnailView, Float> DIM_ALPHA_MULTIPLIER = + new FloatProperty<TaskThumbnailView>("dimAlphaMultiplier") { + @Override + public void setValue(TaskThumbnailView thumbnail, float dimAlphaMultiplier) { + thumbnail.setDimAlphaMultipler(dimAlphaMultiplier); + } + + @Override + public Float get(TaskThumbnailView thumbnailView) { + return thumbnailView.mDimAlphaMultiplier; + } + }; + private final float mCornerRadius; private final BaseActivity mActivity; @@ -62,6 +80,7 @@ public class TaskThumbnailView extends View { protected BitmapShader mBitmapShader; private float mDimAlpha = 1f; + private float mDimAlphaMultiplier = 1f; public TaskThumbnailView(Context context) { this(context, null); @@ -109,8 +128,15 @@ public class TaskThumbnailView extends View { updateThumbnailPaintFilter(); } + public void setDimAlphaMultipler(float dimAlphaMultipler) { + mDimAlphaMultiplier = dimAlphaMultipler; + setDimAlpha(mDimAlpha); + } + /** * Sets the alpha of the dim layer on top of this view. + * + * If dimAlpha is 0, no dimming is applied; if dimAlpha is 1, the thumbnail will be black. */ public void setDimAlpha(float dimAlpha) { mDimAlpha = dimAlpha; @@ -124,32 +150,53 @@ public class TaskThumbnailView extends View { return new Rect(); } + public int getSysUiStatusNavFlags() { + if (mThumbnailData != null) { + int flags = 0; + flags |= (mThumbnailData.systemUiVisibility & SYSTEM_UI_FLAG_LIGHT_STATUS_BAR) != 0 + ? SystemUiController.FLAG_LIGHT_STATUS + : SystemUiController.FLAG_DARK_STATUS; + flags |= (mThumbnailData.systemUiVisibility & SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR) != 0 + ? SystemUiController.FLAG_LIGHT_NAV + : SystemUiController.FLAG_DARK_NAV; + return flags; + } + return 0; + } + @Override protected void onDraw(Canvas canvas) { - if (mTask == null) { - return; + drawOnCanvas(canvas, 0, 0, getMeasuredWidth(), getMeasuredHeight(), mCornerRadius); + } + + public float getCornerRadius() { + return mCornerRadius; + } + + public void drawOnCanvas(Canvas canvas, float x, float y, float width, float height, + float cornerRadius) { + // Draw the background in all cases, except when the thumbnail data is opaque + final boolean drawBackgroundOnly = mTask == null || mTask.isLocked || mBitmapShader == null + || mThumbnailData == null; + if (drawBackgroundOnly || mClipBottom > 0 || mThumbnailData.isTranslucent) { + canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mBackgroundPaint); + if (drawBackgroundOnly) { + return; + } } - int width = getMeasuredWidth(); - int height = getMeasuredHeight(); - if (mClipBottom > 0 && !mTask.isLocked) { - canvas.save(); - canvas.clipRect(0, 0, width, mClipBottom); - canvas.drawRoundRect(0, 0, width, height, mCornerRadius, mCornerRadius, mPaint); - canvas.restore(); + if (mClipBottom > 0) { canvas.save(); - canvas.clipRect(0, mClipBottom, width, height); - canvas.drawRoundRect(0, 0, width, height, mCornerRadius, mCornerRadius, - mBackgroundPaint); + canvas.clipRect(x, y, width, mClipBottom); + canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mPaint); canvas.restore(); } else { - canvas.drawRoundRect(0, 0, width, height, mCornerRadius, - mCornerRadius, mTask.isLocked ? mBackgroundPaint : mPaint); + canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mPaint); } } private void updateThumbnailPaintFilter() { - int mul = (int) (mDimAlpha * 255); + int mul = (int) ((1 - mDimAlpha * mDimAlphaMultiplier) * 255); if (mBitmapShader != null) { LightingColorFilter filter = getLightingColorFilter(mul); mPaint.setColorFilter(filter); @@ -167,9 +214,9 @@ public class TaskThumbnailView extends View { if (mBitmapShader != null && mThumbnailData != null) { float scale = mThumbnailData.scale; Rect thumbnailInsets = mThumbnailData.insets; - float thumbnailWidth = mThumbnailData.thumbnail.getWidth() - + final float thumbnailWidth = mThumbnailData.thumbnail.getWidth() - (thumbnailInsets.left + thumbnailInsets.right) * scale; - float thumbnailHeight = mThumbnailData.thumbnail.getHeight() - + final float thumbnailHeight = mThumbnailData.thumbnail.getHeight() - (thumbnailInsets.top + thumbnailInsets.bottom) * scale; final float thumbnailScale; @@ -185,7 +232,8 @@ public class TaskThumbnailView extends View { // Rotate the screenshot if not in multi-window mode rotate = FeatureFlags.OVERVIEW_USE_SCREENSHOT_ORIENTATION && configuration.orientation != mThumbnailData.orientation && - !mActivity.isInMultiWindowModeCompat(); + !mActivity.isInMultiWindowModeCompat() && + mThumbnailData.windowingMode == WINDOWING_MODE_FULLSCREEN; // Scale the screenshot to always fit the width of the card. thumbnailScale = rotate ? getMeasuredWidth() / thumbnailHeight @@ -216,7 +264,8 @@ public class TaskThumbnailView extends View { mMatrix.postScale(thumbnailScale, thumbnailScale); mBitmapShader.setLocalMatrix(mMatrix); - float bitmapHeight = Math.max(thumbnailHeight * thumbnailScale, 0); + float bitmapHeight = Math.max((rotate ? thumbnailWidth : thumbnailHeight) + * thumbnailScale, 0); if (Math.round(bitmapHeight) < getMeasuredHeight()) { mClipBottom = bitmapHeight; } diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java index f04acaf12..82aa45a18 100644 --- a/quickstep/src/com/android/quickstep/views/TaskView.java +++ b/quickstep/src/com/android/quickstep/views/TaskView.java @@ -16,28 +16,34 @@ package com.android.quickstep.views; +import static android.widget.Toast.LENGTH_SHORT; +import static com.android.quickstep.views.TaskThumbnailView.DIM_ALPHA_MULTIPLIER; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; import android.animation.TimeInterpolator; import android.app.ActivityOptions; import android.content.Context; import android.content.res.Resources; import android.graphics.Outline; -import android.graphics.Rect; import android.os.Bundle; import android.os.Handler; import android.util.AttributeSet; +import android.util.FloatProperty; +import android.util.Log; +import android.util.Property; import android.view.View; import android.view.ViewOutlineProvider; import android.view.accessibility.AccessibilityNodeInfo; import android.widget.FrameLayout; -import android.widget.ImageView; +import android.widget.Toast; import com.android.launcher3.BaseActivity; import com.android.launcher3.BaseDraggingActivity; -import com.android.launcher3.DeviceProfile; import com.android.launcher3.R; import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction; import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch; -import com.android.quickstep.RecentsAnimationInterpolator; import com.android.quickstep.TaskSystemShortcut; import com.android.quickstep.TaskUtils; import com.android.quickstep.views.RecentsView.PageCallbacks; @@ -54,6 +60,8 @@ import java.util.function.Consumer; */ public class TaskView extends FrameLayout implements TaskCallbacks, PageCallbacks { + private static final String TAG = TaskView.class.getSimpleName(); + /** A curve of x from 0 to 1, where 0 is the center of the screen and 1 is the edge. */ private static final TimeInterpolator CURVE_INTERPOLATOR = x -> (float) -Math.cos(x * Math.PI) / 2f + .5f; @@ -69,12 +77,28 @@ public class TaskView extends FrameLayout implements TaskCallbacks, PageCallback */ private static final float EDGE_SCALE_DOWN_FACTOR = 0.03f; - private static final long SCALE_ICON_DURATION = 120; + public static final long SCALE_ICON_DURATION = 120; + private static final long DIM_ANIM_DURATION = 700; + + public static final Property<TaskView, Float> ZOOM_SCALE = + new FloatProperty<TaskView>("zoomScale") { + @Override + public void setValue(TaskView taskView, float v) { + taskView.setZoomScale(v); + } + + @Override + public Float get(TaskView taskView) { + return taskView.mZoomScale; + } + }; private Task mTask; private TaskThumbnailView mSnapshotView; - private ImageView mIconView; + private IconView mIconView; private float mCurveScale; + private float mZoomScale; + private Animator mDimAlphaAnim; public TaskView(Context context) { this(context, null); @@ -87,11 +111,13 @@ public class TaskView extends FrameLayout implements TaskCallbacks, PageCallback public TaskView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); setOnClickListener((view) -> { - if (mTask != null) { - launchTask(true /* animate */); - BaseActivity.fromContext(context).getUserEventDispatcher().logTaskLaunchOrDismiss( - Touch.TAP, Direction.NONE, TaskUtils.getComponentKeyForTask(mTask.key)); + if (getTask() == null) { + return; } + launchTask(true /* animate */); + BaseActivity.fromContext(context).getUserEventDispatcher().logTaskLaunchOrDismiss( + Touch.TAP, Direction.NONE, ((RecentsView) getParent()).indexOfChild(this), + TaskUtils.getComponentKeyForTask(getTask().key)); }); setOutlineProvider(new TaskOutlineProvider(getResources())); } @@ -124,8 +150,16 @@ public class TaskView extends FrameLayout implements TaskCallbacks, PageCallback return mSnapshotView; } + public IconView getIconView() { + return mIconView; + } + public void launchTask(boolean animate) { - launchTask(animate, null, null); + launchTask(animate, (result) -> { + if (!result) { + notifyTaskLaunchFailed(TAG); + } + }, getHandler()); } public void launchTask(boolean animate, Consumer<Boolean> resultCallback, @@ -146,7 +180,7 @@ public class TaskView extends FrameLayout implements TaskCallbacks, PageCallback @Override public void onTaskDataLoaded(Task task, ThumbnailData thumbnailData) { mSnapshotView.setThumbnail(task, thumbnailData); - mIconView.setImageDrawable(task.icon); + mIconView.setDrawable(task.icon); mIconView.setOnClickListener(icon -> TaskMenuView.showForTask(this)); mIconView.setOnLongClickListener(icon -> { requestDisallowInterceptTouchEvent(true); @@ -157,7 +191,7 @@ public class TaskView extends FrameLayout implements TaskCallbacks, PageCallback @Override public void onTaskDataUnloaded() { mSnapshotView.setThumbnail(null, null); - mIconView.setImageDrawable(null); + mIconView.setDrawable(null); mIconView.setOnLongClickListener(null); } @@ -166,23 +200,37 @@ public class TaskView extends FrameLayout implements TaskCallbacks, PageCallback // Do nothing } - public void animateIconToScale(float scale) { + public void animateIconToScaleAndDim(float scale) { mIconView.animate().scaleX(scale).scaleY(scale).setDuration(SCALE_ICON_DURATION).start(); + mDimAlphaAnim = ObjectAnimator.ofFloat(mSnapshotView, DIM_ALPHA_MULTIPLIER, 1 - scale, + scale); + mDimAlphaAnim.setDuration(DIM_ANIM_DURATION); + mDimAlphaAnim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mDimAlphaAnim = null; + } + }); + mDimAlphaAnim.start(); } - protected void setIconScale(float iconScale) { + protected void setIconScaleAndDim(float iconScale) { mIconView.animate().cancel(); mIconView.setScaleX(iconScale); mIconView.setScaleY(iconScale); + if (mDimAlphaAnim != null) { + mDimAlphaAnim.cancel(); + } + mSnapshotView.setDimAlphaMultipler(iconScale); } public void resetVisualProperties() { - setScaleX(1f); - setScaleY(1f); + setZoomScale(1); setTranslationX(0f); setTranslationY(0f); setTranslationZ(0); setAlpha(1f); + setIconScaleAndDim(1); } @Override @@ -190,40 +238,52 @@ public class TaskView extends FrameLayout implements TaskCallbacks, PageCallback float curveInterpolation = CURVE_INTERPOLATOR.getInterpolation(scrollState.linearInterpolation); - mSnapshotView.setDimAlpha(1 - curveInterpolation * MAX_PAGE_SCRIM_ALPHA); + mSnapshotView.setDimAlpha(curveInterpolation * MAX_PAGE_SCRIM_ALPHA); + setCurveScale(getCurveScaleForCurveInterpolation(curveInterpolation)); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + setPivotX((right - left) * 0.5f); + setPivotY(mSnapshotView.getTop() + mSnapshotView.getHeight() * 0.5f); + } + + public float getCurveScaleForInterpolation(float linearInterpolation) { + float curveInterpolation = CURVE_INTERPOLATOR.getInterpolation(linearInterpolation); + return getCurveScaleForCurveInterpolation(curveInterpolation); + } + + private float getCurveScaleForCurveInterpolation(float curveInterpolation) { + return 1 - curveInterpolation * EDGE_SCALE_DOWN_FACTOR; + } - mCurveScale = 1 - curveInterpolation * EDGE_SCALE_DOWN_FACTOR; - setScaleX(mCurveScale); - setScaleY(mCurveScale); + private void setCurveScale(float curveScale) { + mCurveScale = curveScale; + onScaleChanged(); } public float getCurveScale() { return mCurveScale; } + public void setZoomScale(float adjacentScale) { + mZoomScale = adjacentScale; + onScaleChanged(); + } + + private void onScaleChanged() { + float scale = mCurveScale * mZoomScale; + setScaleX(scale); + setScaleY(scale); + } + @Override public boolean hasOverlappingRendering() { // TODO: Clip-out the icon region from the thumbnail, since they are overlapping. return false; } - public RecentsAnimationInterpolator getRecentsInterpolator() { - Rect taskViewBounds = new Rect(); - BaseDraggingActivity activity = BaseDraggingActivity.fromContext(getContext()); - DeviceProfile dp = activity.getDeviceProfile(); - activity.getDragLayer().getDescendantRectRelativeToSelf(this, taskViewBounds); - - // TODO: Use the actual target insets instead of the current thumbnail insets in case the - // device state has changed - return new RecentsAnimationInterpolator( - new Rect(0, 0, dp.widthPx, dp.heightPx), - getThumbnail().getInsets(), - taskViewBounds, - new Rect(0, getThumbnail().getTop(), 0, 0), - getScaleX(), - getTranslationX()); - } - private static final class TaskOutlineProvider extends ViewOutlineProvider { private final int mMarginTop; @@ -281,4 +341,13 @@ public class TaskView extends FrameLayout implements TaskCallbacks, PageCallback return super.performAccessibilityAction(action, arguments); } + + public void notifyTaskLaunchFailed(String tag) { + String msg = "Failed to launch task"; + if (mTask != null) { + msg += " (task=" + mTask.key.baseIntent + " userId=" + mTask.key.userId + ")"; + } + Log.w(tag, msg); + Toast.makeText(getContext(), R.string.activity_not_available, LENGTH_SHORT).show(); + } } |