Animates a fake TaskView in Home gesture tutorial.

The "TaskView" morphs as you drag up from the
nav bar, and transforms into a "home icon" or
final "TaskView" when you release your finger.

If you cancel the gesture (e.g. drag back down),
the view simply fades out and provides a hint
to swipe straight up.

Demo: https://drive.google.com/open?id=1NXlO1W6IhLoX0k2K6b-QZ5IXp0o7OYqO

Bug: 148542211
Change-Id: I3d5e9a45fcadc6a498941ea17813b9487720a504
This commit is contained in:
Andy Wickham 2020-05-21 18:04:18 -07:00
parent 625bd51fe3
commit f8a8117676
12 changed files with 674 additions and 344 deletions

View File

@ -15,58 +15,38 @@
*/
package com.android.quickstep;
import static com.android.launcher3.anim.Interpolators.ACCEL_1_5;
import static com.android.launcher3.anim.Interpolators.DEACCEL;
import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC;
import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION;
import android.animation.Animator;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.Intent;
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.util.Log;
import android.view.MotionEvent;
import android.view.animation.Interpolator;
import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimationSuccessListener;
import com.android.launcher3.anim.AnimatorPlaybackController;
import com.android.launcher3.anim.PendingAnimation;
import com.android.launcher3.statemanager.StatefulActivity;
import com.android.launcher3.testing.TestProtocol;
import com.android.launcher3.touch.PagedOrientationHandler;
import com.android.launcher3.util.VibratorWrapper;
import com.android.launcher3.util.WindowBounds;
import com.android.launcher3.views.FloatingIconView;
import com.android.quickstep.RecentsAnimationCallbacks.RecentsAnimationListener;
import com.android.quickstep.util.ActiveGestureLog;
import com.android.quickstep.util.ActivityInitListener;
import com.android.quickstep.util.RectFSpringAnim;
import com.android.quickstep.util.TaskViewSimulator;
import com.android.quickstep.util.TransformParams;
import com.android.quickstep.util.TransformParams.BuilderProxy;
import com.android.quickstep.views.RecentsView;
import com.android.quickstep.views.TaskView;
import com.android.systemui.shared.recents.model.ThumbnailData;
import com.android.systemui.shared.system.InputConsumerController;
import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat;
import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams.Builder;
import java.util.ArrayList;
import java.util.function.Consumer;
@ -76,40 +56,13 @@ import java.util.function.Consumer;
*/
@TargetApi(Build.VERSION_CODES.Q)
public abstract class BaseSwipeUpHandler<T extends StatefulActivity<?>, Q extends RecentsView>
implements RecentsAnimationListener {
extends SwipeUpAnimationLogic implements RecentsAnimationListener {
private static final String TAG = "BaseSwipeUpHandler";
protected static final Rect TEMP_RECT = new Rect();
public static final float MIN_PROGRESS_FOR_OVERVIEW = 0.7f;
private static final Interpolator PULLBACK_INTERPOLATOR = DEACCEL;
// The distance needed to drag to reach the task size in recents.
protected int mTransitionDragLength;
// How much further we can drag past recents, as a factor of mTransitionDragLength.
protected float mDragLengthFactor = 1;
// Start resisting when swiping past this factor of mTransitionDragLength.
private float mDragLengthFactorStartPullback = 1f;
// This is how far down we can scale down, where 0f is full screen and 1f is recents.
private float mDragLengthFactorMaxPullback = 1f;
protected final Context mContext;
protected final RecentsAnimationDeviceState mDeviceState;
protected final GestureState mGestureState;
protected final BaseActivityInterface<?, T> mActivityInterface;
protected final InputConsumerController mInputConsumer;
protected final TaskViewSimulator mTaskViewSimulator;
private AnimatorPlaybackController mWindowTransitionController;
protected final TransformParams mTransformParams = new TransformParams();
// Shift in the range of [0, 1].
// 0 => preview snapShot is completely visible, and hotseat is completely translated down
// 1 => preview snapShot is completely aligned with the recents view and hotseat is completely
// visible.
protected final AnimatedFloat mCurrentShift = new AnimatedFloat(this::updateFinalShift);
protected final ActivityInitListener mActivityInitListener;
protected RecentsAnimationController mRecentsAnimationController;
@ -120,7 +73,6 @@ public abstract class BaseSwipeUpHandler<T extends StatefulActivity<?>, Q extend
protected T mActivity;
protected Q mRecentsView;
protected DeviceProfile mDp;
protected Runnable mGestureEndCallback;
@ -132,13 +84,10 @@ public abstract class BaseSwipeUpHandler<T extends StatefulActivity<?>, Q extend
protected BaseSwipeUpHandler(Context context, RecentsAnimationDeviceState deviceState,
GestureState gestureState, InputConsumerController inputConsumer) {
mContext = context;
mDeviceState = deviceState;
mGestureState = gestureState;
super(context, deviceState, gestureState, new TransformParams());
mActivityInterface = gestureState.getActivityInterface();
mActivityInitListener = mActivityInterface.createActivityInitListener(this::onActivityInit);
mInputConsumer = inputConsumer;
mTaskViewSimulator = new TaskViewSimulator(context, gestureState.getActivityInterface());
}
/**
@ -158,28 +107,6 @@ public abstract class BaseSwipeUpHandler<T extends StatefulActivity<?>, Q extend
return mRecentsView != null ? mRecentsView.getEventDispatcher(navbarRotation) : null;
}
@UiThread
public void updateDisplacement(float displacement) {
// We are moving in the negative x/y direction
displacement = -displacement;
float shift;
if (displacement > mTransitionDragLength * mDragLengthFactor && mTransitionDragLength > 0) {
shift = mDragLengthFactor;
} else {
float translation = Math.max(displacement, 0);
shift = mTransitionDragLength == 0 ? 0 : translation / mTransitionDragLength;
if (shift > mDragLengthFactorStartPullback) {
float pullbackProgress = Utilities.getProgress(shift,
mDragLengthFactorStartPullback, mDragLengthFactor);
pullbackProgress = PULLBACK_INTERPOLATOR.getInterpolation(pullbackProgress);
shift = mDragLengthFactorStartPullback + pullbackProgress
* (mDragLengthFactorMaxPullback - mDragLengthFactorStartPullback);
}
}
mCurrentShift.updateValue(shift);
}
public void setGestureEndCallback(Runnable gestureEndCallback) {
mGestureEndCallback = gestureEndCallback;
}
@ -276,6 +203,7 @@ public abstract class BaseSwipeUpHandler<T extends StatefulActivity<?>, Q extend
RecentsAnimationTargets targets) {
mRecentsAnimationController = recentsAnimationController;
mRecentsAnimationTargets = targets;
mTransformParams.setTargetSet(mRecentsAnimationTargets);
DeviceProfile dp = InvariantDeviceProfile.INSTANCE.get(mContext).getDeviceProfile(mContext);
RemoteAnimationTargetCompat runningTaskTarget = targets.findTask(
mGestureState.getRunningTaskId());
@ -357,35 +285,6 @@ public abstract class BaseSwipeUpHandler<T extends StatefulActivity<?>, Q extend
return mGestureState.getLastStartedTaskId() != -1;
}
protected void initTransitionEndpoints(DeviceProfile dp) {
mDp = dp;
mTaskViewSimulator.setDp(dp);
mTaskViewSimulator.setLayoutRotation(
mDeviceState.getCurrentActiveRotation(),
mDeviceState.getDisplayRotation());
mTransitionDragLength = mActivityInterface.getSwipeUpDestinationAndLength(
dp, mContext, TEMP_RECT,
mTaskViewSimulator.getOrientationState().getOrientationHandler());
if (mDeviceState.isFullyGesturalNavMode()) {
// We can drag all the way to the top of the screen.
mDragLengthFactor = (float) dp.heightPx / mTransitionDragLength;
float startScale = mTaskViewSimulator.getFullScreenScale();
// Start pulling back when RecentsView scale is 0.75f, and let it go down to 0.5f.
mDragLengthFactorStartPullback = (0.75f - startScale) / (1 - startScale);
mDragLengthFactorMaxPullback = (0.5f - startScale) / (1 - startScale);
} else {
mDragLengthFactor = 1;
mDragLengthFactorStartPullback = mDragLengthFactorMaxPullback = 1;
}
PendingAnimation pa = new PendingAnimation(mTransitionDragLength * 2);
mTaskViewSimulator.addAppToOverviewAnim(pa, t -> t * mDragLengthFactor);
mWindowTransitionController = pa.createPlaybackController();
}
/**
* Return true if the window should be translated horizontally if the recents view scrolls
*/
@ -457,7 +356,6 @@ public abstract class BaseSwipeUpHandler<T extends StatefulActivity<?>, Q extend
if (mWindowTransitionController != null) {
float progress = mCurrentShift.value / mDragLengthFactor;
mWindowTransitionController.setPlayFraction(progress);
mTransformParams.setTargetSet(mRecentsAnimationTargets);
if (mRecentsViewScrollLinked) {
mTaskViewSimulator.setScroll(mRecentsView.getScrollOffset());
@ -466,217 +364,9 @@ public abstract class BaseSwipeUpHandler<T extends StatefulActivity<?>, Q extend
}
}
protected PagedOrientationHandler getOrientationHandler() {
return mTaskViewSimulator.getOrientationState().getOrientationHandler();
}
/**
* Creates an animation that transforms the current app window into the home app.
* @param startProgress The progress of {@link #mCurrentShift} to start the window from.
* @param homeAnimationFactory The home animation factory.
*/
protected RectFSpringAnim createWindowAnimationToHome(float startProgress,
HomeAnimationFactory homeAnimationFactory) {
final RectF targetRect = homeAnimationFactory.getWindowTargetRect();
final FloatingIconView fiv = homeAnimationFactory.mIconView;
final boolean isFloatingIconView = fiv != null;
mWindowTransitionController.setPlayFraction(startProgress / mDragLengthFactor);
mTaskViewSimulator.apply(mTransformParams
.setProgress(startProgress)
.setTargetSet(mRecentsAnimationTargets));
RectF cropRectF = new RectF(mTaskViewSimulator.getCurrentCropRect());
// Matrix to map a rect in Launcher space to window space
Matrix homeToWindowPositionMap = new Matrix();
mTaskViewSimulator.applyWindowToHomeRotation(homeToWindowPositionMap);
final RectF startRect = new RectF(cropRectF);
mTaskViewSimulator.getCurrentMatrix().mapRect(startRect);
// Move the startRect to Launcher space as floatingIconView runs in Launcher
Matrix windowToHomePositionMap = new Matrix();
homeToWindowPositionMap.invert(windowToHomePositionMap);
windowToHomePositionMap.mapRect(startRect);
RectFSpringAnim anim = new RectFSpringAnim(startRect, targetRect, mContext);
if (isFloatingIconView) {
anim.addAnimatorListener(fiv);
fiv.setOnTargetChangeListener(anim::onTargetPositionChanged);
fiv.setFastFinishRunnable(anim::end);
}
SpringAnimationRunner runner = new SpringAnimationRunner(
homeAnimationFactory, cropRectF, homeToWindowPositionMap);
anim.addOnUpdateListener(runner);
anim.addAnimatorListener(runner);
return anim;
}
public interface Factory {
BaseSwipeUpHandler newHandler(
GestureState gestureState, long touchTimeMs, boolean continuingLastGesture);
}
protected interface RunningWindowAnim {
void end();
void cancel();
static RunningWindowAnim wrap(Animator animator) {
return new RunningWindowAnim() {
@Override
public void end() {
animator.end();
}
@Override
public void cancel() {
animator.cancel();
}
};
}
static RunningWindowAnim wrap(RectFSpringAnim rectFSpringAnim) {
return new RunningWindowAnim() {
@Override
public void end() {
rectFSpringAnim.end();
}
@Override
public void cancel() {
rectFSpringAnim.cancel();
}
};
}
}
/**
* @param progress The progress of the animation to the home screen.
* @return The current alpha to set on the animating app window.
*/
protected float getWindowAlpha(float progress) {
// Alpha interpolates between [1, 0] between progress values [start, end]
final float start = 0f;
final float end = 0.85f;
if (progress <= start) {
return 1f;
}
if (progress >= end) {
return 0f;
}
return Utilities.mapToRange(progress, start, end, 1, 0, ACCEL_1_5);
}
protected abstract class HomeAnimationFactory {
private FloatingIconView mIconView;
public HomeAnimationFactory(@Nullable FloatingIconView iconView) {
mIconView = iconView;
}
public @NonNull RectF getWindowTargetRect() {
PagedOrientationHandler orientationHandler = getOrientationHandler();
DeviceProfile dp = mDp;
final int halfIconSize = dp.iconSizePx / 2;
float primaryDimension = orientationHandler
.getPrimaryValue(dp.availableWidthPx, dp.availableHeightPx);
float secondaryDimension = orientationHandler
.getSecondaryValue(dp.availableWidthPx, dp.availableHeightPx);
final float targetX = primaryDimension / 2f;
final float targetY = secondaryDimension - dp.hotseatBarSizePx;
// Fallback to animate to center of screen.
return new RectF(targetX - halfIconSize, targetY - halfIconSize,
targetX + halfIconSize, targetY + halfIconSize);
}
public abstract @NonNull AnimatorPlaybackController createActivityAnimationToHome();
public void playAtomicAnimation(float velocity) {
// No-op
}
}
private class SpringAnimationRunner extends AnimationSuccessListener
implements RectFSpringAnim.OnUpdateListener, BuilderProxy {
final Rect mCropRect = new Rect();
final Matrix mMatrix = new Matrix();
final RectF mWindowCurrentRect = new RectF();
final Matrix mHomeToWindowPositionMap;
final FloatingIconView mFIV;
final AnimatorPlaybackController mHomeAnim;
final RectF mCropRectF;
final float mStartRadius;
final float mEndRadius;
final float mWindowAlphaThreshold;
SpringAnimationRunner(HomeAnimationFactory factory, RectF cropRectF,
Matrix homeToWindowPositionMap) {
mHomeAnim = factory.createActivityAnimationToHome();
mCropRectF = cropRectF;
mHomeToWindowPositionMap = homeToWindowPositionMap;
cropRectF.roundOut(mCropRect);
mFIV = factory.mIconView;
// End on a "round-enough" radius so that the shape reveal doesn't have to do too much
// rounding at the end of the animation.
mStartRadius = mTaskViewSimulator.getCurrentCornerRadius();
mEndRadius = cropRectF.width() / 2f;
// We want the window alpha to be 0 once this threshold is met, so that the
// FolderIconView can be seen morphing into the icon shape.
mWindowAlphaThreshold = mFIV != null ? 1f - SHAPE_PROGRESS_DURATION : 1f;
}
@Override
public void onUpdate(RectF currentRect, float progress) {
mHomeAnim.setPlayFraction(progress);
mHomeToWindowPositionMap.mapRect(mWindowCurrentRect, currentRect);
mMatrix.setRectToRect(mCropRectF, mWindowCurrentRect, ScaleToFit.FILL);
float cornerRadius = Utilities.mapRange(progress, mStartRadius, mEndRadius);
mTransformParams
.setTargetAlpha(getWindowAlpha(progress))
.setCornerRadius(cornerRadius);
mTransformParams.applySurfaceParams(mTransformParams.createSurfaceParams(this));
if (mFIV != null) {
mFIV.update(currentRect, 1f, progress,
mWindowAlphaThreshold, mMatrix.mapRadius(cornerRadius), false);
}
}
@Override
public void onBuildTargetParams(
Builder builder, RemoteAnimationTargetCompat app, TransformParams params) {
builder.withMatrix(mMatrix)
.withWindowCrop(mCropRect)
.withCornerRadius(params.getCornerRadius());
}
@Override
public void onCancel() {
if (mFIV != null) {
mFIV.fastFinish();
}
}
@Override
public void onAnimationStart(Animator animation) {
mHomeAnim.dispatchOnStart();
}
@Override
public void onAnimationSuccess(Animator animator) {
mHomeAnim.getAnimationPlayer().end();
}
}
}

View File

@ -0,0 +1,350 @@
/*
* Copyright (C) 2020 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.anim.Interpolators.ACCEL_1_5;
import static com.android.launcher3.anim.Interpolators.DEACCEL;
import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION;
import android.animation.Animator;
import android.content.Context;
import android.graphics.Matrix;
import android.graphics.Matrix.ScaleToFit;
import android.graphics.Rect;
import android.graphics.RectF;
import android.view.animation.Interpolator;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimationSuccessListener;
import com.android.launcher3.anim.AnimatorPlaybackController;
import com.android.launcher3.anim.PendingAnimation;
import com.android.launcher3.touch.PagedOrientationHandler;
import com.android.launcher3.views.FloatingIconView;
import com.android.quickstep.util.RectFSpringAnim;
import com.android.quickstep.util.TaskViewSimulator;
import com.android.quickstep.util.TransformParams;
import com.android.quickstep.util.TransformParams.BuilderProxy;
import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams.Builder;
public abstract class SwipeUpAnimationLogic {
protected static final Rect TEMP_RECT = new Rect();
private static final Interpolator PULLBACK_INTERPOLATOR = DEACCEL;
protected DeviceProfile mDp;
protected final Context mContext;
protected final RecentsAnimationDeviceState mDeviceState;
protected final GestureState mGestureState;
protected final TaskViewSimulator mTaskViewSimulator;
protected final TransformParams mTransformParams;
// Shift in the range of [0, 1].
// 0 => preview snapShot is completely visible, and hotseat is completely translated down
// 1 => preview snapShot is completely aligned with the recents view and hotseat is completely
// visible.
protected final AnimatedFloat mCurrentShift = new AnimatedFloat(this::updateFinalShift);
// The distance needed to drag to reach the task size in recents.
protected int mTransitionDragLength;
// How much further we can drag past recents, as a factor of mTransitionDragLength.
protected float mDragLengthFactor = 1;
// Start resisting when swiping past this factor of mTransitionDragLength.
private float mDragLengthFactorStartPullback = 1f;
// This is how far down we can scale down, where 0f is full screen and 1f is recents.
private float mDragLengthFactorMaxPullback = 1f;
protected AnimatorPlaybackController mWindowTransitionController;
public SwipeUpAnimationLogic(Context context, RecentsAnimationDeviceState deviceState,
GestureState gestureState, TransformParams transformParams) {
mContext = context;
mDeviceState = deviceState;
mGestureState = gestureState;
mTaskViewSimulator = new TaskViewSimulator(context, gestureState.getActivityInterface());
mTransformParams = transformParams;
}
protected void initTransitionEndpoints(DeviceProfile dp) {
mDp = dp;
mTaskViewSimulator.setDp(dp);
mTaskViewSimulator.setLayoutRotation(
mDeviceState.getCurrentActiveRotation(),
mDeviceState.getDisplayRotation());
mTransitionDragLength = mGestureState.getActivityInterface().getSwipeUpDestinationAndLength(
dp, mContext, TEMP_RECT,
mTaskViewSimulator.getOrientationState().getOrientationHandler());
if (mDeviceState.isFullyGesturalNavMode()) {
// We can drag all the way to the top of the screen.
mDragLengthFactor = (float) dp.heightPx / mTransitionDragLength;
float startScale = mTaskViewSimulator.getFullScreenScale();
// Start pulling back when RecentsView scale is 0.75f, and let it go down to 0.5f.
mDragLengthFactorStartPullback = (0.75f - startScale) / (1 - startScale);
mDragLengthFactorMaxPullback = (0.5f - startScale) / (1 - startScale);
} else {
mDragLengthFactor = 1;
mDragLengthFactorStartPullback = mDragLengthFactorMaxPullback = 1;
}
PendingAnimation pa = new PendingAnimation(mTransitionDragLength * 2);
mTaskViewSimulator.addAppToOverviewAnim(pa, t -> t * mDragLengthFactor);
mWindowTransitionController = pa.createPlaybackController();
}
@UiThread
public void updateDisplacement(float displacement) {
// We are moving in the negative x/y direction
displacement = -displacement;
float shift;
if (displacement > mTransitionDragLength * mDragLengthFactor && mTransitionDragLength > 0) {
shift = mDragLengthFactor;
} else {
float translation = Math.max(displacement, 0);
shift = mTransitionDragLength == 0 ? 0 : translation / mTransitionDragLength;
if (shift > mDragLengthFactorStartPullback) {
float pullbackProgress = Utilities.getProgress(shift,
mDragLengthFactorStartPullback, mDragLengthFactor);
pullbackProgress = PULLBACK_INTERPOLATOR.getInterpolation(pullbackProgress);
shift = mDragLengthFactorStartPullback + pullbackProgress
* (mDragLengthFactorMaxPullback - mDragLengthFactorStartPullback);
}
}
mCurrentShift.updateValue(shift);
}
/**
* Called when the value of {@link #mCurrentShift} changes
*/
@UiThread
public abstract void updateFinalShift();
protected PagedOrientationHandler getOrientationHandler() {
return mTaskViewSimulator.getOrientationState().getOrientationHandler();
}
protected abstract class HomeAnimationFactory {
public FloatingIconView mIconView;
public HomeAnimationFactory(@Nullable FloatingIconView iconView) {
mIconView = iconView;
}
public @NonNull RectF getWindowTargetRect() {
PagedOrientationHandler orientationHandler = getOrientationHandler();
DeviceProfile dp = mDp;
final int halfIconSize = dp.iconSizePx / 2;
float primaryDimension = orientationHandler
.getPrimaryValue(dp.availableWidthPx, dp.availableHeightPx);
float secondaryDimension = orientationHandler
.getSecondaryValue(dp.availableWidthPx, dp.availableHeightPx);
final float targetX = primaryDimension / 2f;
final float targetY = secondaryDimension - dp.hotseatBarSizePx;
// Fallback to animate to center of screen.
return new RectF(targetX - halfIconSize, targetY - halfIconSize,
targetX + halfIconSize, targetY + halfIconSize);
}
public abstract @NonNull AnimatorPlaybackController createActivityAnimationToHome();
public void playAtomicAnimation(float velocity) {
// No-op
}
}
/**
* Creates an animation that transforms the current app window into the home app.
* @param startProgress The progress of {@link #mCurrentShift} to start the window from.
* @param homeAnimationFactory The home animation factory.
*/
protected RectFSpringAnim createWindowAnimationToHome(float startProgress,
HomeAnimationFactory homeAnimationFactory) {
final RectF targetRect = homeAnimationFactory.getWindowTargetRect();
final FloatingIconView fiv = homeAnimationFactory.mIconView;
final boolean isFloatingIconView = fiv != null;
mWindowTransitionController.setPlayFraction(startProgress / mDragLengthFactor);
mTaskViewSimulator.apply(mTransformParams.setProgress(startProgress));
RectF cropRectF = new RectF(mTaskViewSimulator.getCurrentCropRect());
// Matrix to map a rect in Launcher space to window space
Matrix homeToWindowPositionMap = new Matrix();
mTaskViewSimulator.applyWindowToHomeRotation(homeToWindowPositionMap);
final RectF startRect = new RectF(cropRectF);
mTaskViewSimulator.getCurrentMatrix().mapRect(startRect);
// Move the startRect to Launcher space as floatingIconView runs in Launcher
Matrix windowToHomePositionMap = new Matrix();
homeToWindowPositionMap.invert(windowToHomePositionMap);
windowToHomePositionMap.mapRect(startRect);
RectFSpringAnim anim = new RectFSpringAnim(startRect, targetRect, mContext);
if (isFloatingIconView) {
anim.addAnimatorListener(fiv);
fiv.setOnTargetChangeListener(anim::onTargetPositionChanged);
fiv.setFastFinishRunnable(anim::end);
}
SpringAnimationRunner runner = new SpringAnimationRunner(
homeAnimationFactory, cropRectF, homeToWindowPositionMap);
anim.addOnUpdateListener(runner);
anim.addAnimatorListener(runner);
return anim;
}
/**
* @param progress The progress of the animation to the home screen.
* @return The current alpha to set on the animating app window.
*/
protected float getWindowAlpha(float progress) {
// Alpha interpolates between [1, 0] between progress values [start, end]
final float start = 0f;
final float end = 0.85f;
if (progress <= start) {
return 1f;
}
if (progress >= end) {
return 0f;
}
return Utilities.mapToRange(progress, start, end, 1, 0, ACCEL_1_5);
}
protected class SpringAnimationRunner extends AnimationSuccessListener
implements RectFSpringAnim.OnUpdateListener, BuilderProxy {
final Rect mCropRect = new Rect();
final Matrix mMatrix = new Matrix();
final RectF mWindowCurrentRect = new RectF();
final Matrix mHomeToWindowPositionMap;
final FloatingIconView mFIV;
final AnimatorPlaybackController mHomeAnim;
final RectF mCropRectF;
final float mStartRadius;
final float mEndRadius;
final float mWindowAlphaThreshold;
SpringAnimationRunner(HomeAnimationFactory factory, RectF cropRectF,
Matrix homeToWindowPositionMap) {
mHomeAnim = factory.createActivityAnimationToHome();
mCropRectF = cropRectF;
mHomeToWindowPositionMap = homeToWindowPositionMap;
cropRectF.roundOut(mCropRect);
mFIV = factory.mIconView;
// End on a "round-enough" radius so that the shape reveal doesn't have to do too much
// rounding at the end of the animation.
mStartRadius = mTaskViewSimulator.getCurrentCornerRadius();
mEndRadius = cropRectF.width() / 2f;
// We want the window alpha to be 0 once this threshold is met, so that the
// FolderIconView can be seen morphing into the icon shape.
mWindowAlphaThreshold = mFIV != null ? 1f - SHAPE_PROGRESS_DURATION : 1f;
}
@Override
public void onUpdate(RectF currentRect, float progress) {
mHomeAnim.setPlayFraction(progress);
mHomeToWindowPositionMap.mapRect(mWindowCurrentRect, currentRect);
mMatrix.setRectToRect(mCropRectF, mWindowCurrentRect, ScaleToFit.FILL);
float cornerRadius = Utilities.mapRange(progress, mStartRadius, mEndRadius);
mTransformParams
.setTargetAlpha(getWindowAlpha(progress))
.setCornerRadius(cornerRadius);
mTransformParams.applySurfaceParams(mTransformParams.createSurfaceParams(this));
if (mFIV != null) {
mFIV.update(currentRect, 1f, progress,
mWindowAlphaThreshold, mMatrix.mapRadius(cornerRadius), false);
}
}
@Override
public void onBuildTargetParams(
Builder builder, RemoteAnimationTargetCompat app, TransformParams params) {
builder.withMatrix(mMatrix)
.withWindowCrop(mCropRect)
.withCornerRadius(params.getCornerRadius());
}
@Override
public void onCancel() {
if (mFIV != null) {
mFIV.fastFinish();
}
}
@Override
public void onAnimationStart(Animator animation) {
mHomeAnim.dispatchOnStart();
}
@Override
public void onAnimationSuccess(Animator animator) {
mHomeAnim.getAnimationPlayer().end();
}
}
public interface RunningWindowAnim {
void end();
void cancel();
static RunningWindowAnim wrap(Animator animator) {
return new RunningWindowAnim() {
@Override
public void end() {
animator.end();
}
@Override
public void cancel() {
animator.cancel();
}
};
}
static RunningWindowAnim wrap(RectFSpringAnim rectFSpringAnim) {
return new RunningWindowAnim() {
@Override
public void end() {
rectFSpringAnim.end();
}
@Override
public void cancel() {
rectFSpringAnim.cancel();
}
};
}
}
}

View File

@ -15,6 +15,7 @@
*/
package com.android.quickstep.fallback;
import android.graphics.PointF;
import android.view.MotionEvent;
import androidx.annotation.Nullable;
@ -30,7 +31,8 @@ import com.android.quickstep.util.TriggerSwipeUpTouchTracker;
/**
* In 0-button mode, intercepts swipe up from the nav bar on FallbackRecentsView to go home.
*/
public class FallbackNavBarTouchController implements TouchController {
public class FallbackNavBarTouchController implements TouchController,
TriggerSwipeUpTouchTracker.OnSwipeUpListener {
private final RecentsActivity mActivity;
@Nullable
@ -44,7 +46,7 @@ public class FallbackNavBarTouchController implements TouchController {
DefaultDisplay.INSTANCE.get(mActivity).getInfo());
mTriggerSwipeUpTracker = new TriggerSwipeUpTouchTracker(mActivity,
true /* disableHorizontalSwipe */, navBarPosition,
null /* onInterceptTouch */, this::onSwipeUp);
null /* onInterceptTouch */, this);
} else {
mTriggerSwipeUpTracker = null;
}
@ -72,7 +74,11 @@ public class FallbackNavBarTouchController implements TouchController {
return false;
}
private void onSwipeUp(boolean wasFling) {
@Override
public void onSwipeUp(boolean wasFling, PointF finalVelocity) {
mActivity.<FallbackRecentsView>getOverviewPanel().startHome();
}
@Override
public void onSwipeUpCancelled() {}
}

View File

@ -17,6 +17,7 @@ package com.android.quickstep.inputconsumers;
import android.content.Context;
import android.content.Intent;
import android.graphics.PointF;
import android.view.MotionEvent;
import com.android.launcher3.BaseActivity;
@ -33,7 +34,8 @@ import com.android.quickstep.util.ActiveGestureLog;
import com.android.quickstep.util.TriggerSwipeUpTouchTracker;
import com.android.systemui.shared.system.InputMonitorCompat;
public class OverviewWithoutFocusInputConsumer implements InputConsumer {
public class OverviewWithoutFocusInputConsumer implements InputConsumer,
TriggerSwipeUpTouchTracker.OnSwipeUpListener {
private final Context mContext;
private final InputMonitorCompat mInputMonitor;
@ -45,7 +47,7 @@ public class OverviewWithoutFocusInputConsumer implements InputConsumer {
mContext = context;
mInputMonitor = inputMonitor;
mTriggerSwipeUpTracker = new TriggerSwipeUpTouchTracker(context, disableHorizontalSwipe,
deviceState.getNavBarPosition(), this::onInterceptTouch, this::onSwipeUp);
deviceState.getNavBarPosition(), this::onInterceptTouch, this);
}
@Override
@ -70,7 +72,8 @@ public class OverviewWithoutFocusInputConsumer implements InputConsumer {
}
}
private void onSwipeUp(boolean wasFling) {
@Override
public void onSwipeUp(boolean wasFling, PointF finalVelocity) {
mContext.startActivity(new Intent(Intent.ACTION_MAIN)
.addCategory(Intent.CATEGORY_HOME)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
@ -83,4 +86,7 @@ public class OverviewWithoutFocusInputConsumer implements InputConsumer {
wasFling ? Touch.FLING : Touch.SWIPE, Direction.UP, containerType, pageIndex);
activity.getUserEventDispatcher().setPreviousHomeGesture(true);
}
@Override
public void onSwipeUpCancelled() {}
}

View File

@ -23,6 +23,7 @@ import static com.android.systemui.shared.system.WindowManagerWrapper.WINDOWING_
import android.animation.TimeInterpolator;
import android.content.Context;
import android.graphics.Matrix;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
@ -74,7 +75,7 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy {
private DeviceProfile mDp;
private final Matrix mMatrix = new Matrix();
private RemoteAnimationTargetCompat mRunningTarget;
private final Point mRunningTargetWindowPosition = new Point();
// Thumbnail view properties
private final Rect mThumbnailPosition = new Rect();
@ -139,13 +140,19 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy {
* Sets the targets which the simulator will control
*/
public void setPreview(RemoteAnimationTargetCompat runningTarget) {
mRunningTarget = runningTarget;
setPreviewBounds(runningTarget.screenSpaceBounds, runningTarget.contentInsets);
mRunningTargetWindowPosition.set(runningTarget.position.x, runningTarget.position.y);
}
mThumbnailData.insets.set(mRunningTarget.contentInsets);
/**
* Sets the targets which the simulator will control
*/
public void setPreviewBounds(Rect bounds, Rect insets) {
mThumbnailData.insets.set(insets);
// TODO: What is this?
mThumbnailData.windowingMode = WINDOWING_MODE_FULLSCREEN;
mThumbnailPosition.set(runningTarget.screenSpaceBounds);
mThumbnailPosition.set(bounds);
mLayoutValid = false;
}
@ -199,16 +206,14 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy {
postDisplayRotation(deltaRotation(
mOrientationState.getLauncherRotation(), mOrientationState.getDisplayRotation()),
mDp.widthPx, mDp.heightPx, matrix);
if (mRunningTarget != null) {
matrix.postTranslate(-mRunningTarget.position.x, -mRunningTarget.position.y);
}
matrix.postTranslate(-mRunningTargetWindowPosition.x, -mRunningTargetWindowPosition.y);
}
/**
* Applies the target to the previously set parameters
*/
public void apply(TransformParams params) {
if (mDp == null || mRunningTarget == null) {
if (mDp == null || mThumbnailPosition.isEmpty()) {
return;
}
if (!mLayoutValid) {

View File

@ -149,8 +149,12 @@ public class TriggerSwipeUpTouchTracker {
isSwipeUp = squaredHypot(displacementX, displacementY) >= mSquaredTouchSlop;
}
if (isSwipeUp && mOnSwipeUp != null) {
mOnSwipeUp.onSwipeUp(wasFling);
if (mOnSwipeUp != null) {
if (isSwipeUp) {
mOnSwipeUp.onSwipeUp(wasFling, new PointF(velocityX, velocityY));
} else {
mOnSwipeUp.onSwipeUpCancelled();
}
}
}
@ -161,7 +165,11 @@ public class TriggerSwipeUpTouchTracker {
/**
* Called on touch up if a swipe up was detected.
* @param wasFling Whether the swipe was a fling, or just passed touch slop at low velocity.
* @param finalVelocity The final velocity of the swipe.
*/
void onSwipeUp(boolean wasFling);
void onSwipeUp(boolean wasFling, PointF finalVelocity);
/** Called on touch up if a swipe up was not detected. */
void onSwipeUpCancelled();
}
}

View File

@ -24,6 +24,13 @@
android:layout_height="match_parent"
android:background="@drawable/gesture_tutorial_ripple"/>
<View
android:id="@+id/gesture_tutorial_fake_task_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/gesture_tutorial_fake_task_view_color"
android:visibility="invisible" />
<ImageView
android:id="@+id/gesture_tutorial_fragment_hand_coaching"
android:layout_width="match_parent"

View File

@ -18,8 +18,11 @@ package com.android.quickstep.interaction;
import static com.android.quickstep.interaction.TutorialController.TutorialType.BACK_NAVIGATION_COMPLETE;
import static com.android.quickstep.interaction.TutorialController.TutorialType.LEFT_EDGE_BACK_NAVIGATION;
import android.graphics.PointF;
import android.view.View;
import androidx.annotation.Nullable;
import com.android.launcher3.R;
import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureResult;
import com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult;
@ -154,11 +157,14 @@ final class BackGestureTutorialController extends TutorialController {
}
@Override
public void onNavBarGestureAttempted(NavBarGestureResult result) {
public void onNavBarGestureAttempted(NavBarGestureResult result, PointF finalVelocity) {
if (mTutorialType == BACK_NAVIGATION_COMPLETE) {
if (result == NavBarGestureResult.HOME_GESTURE_COMPLETED) {
mTutorialFragment.closeTutorial();
}
}
}
@Override
public void setNavBarGestureProgress(@Nullable Float displacement) {}
}

View File

@ -15,19 +15,134 @@
*/
package com.android.quickstep.interaction;
import static com.android.launcher3.anim.Interpolators.ACCEL;
import static com.android.launcher3.util.DefaultDisplay.getSingleFrameMs;
import static com.android.quickstep.BaseSwipeUpHandlerV2.MAX_SWIPE_DURATION;
import static com.android.quickstep.interaction.TutorialController.TutorialType.HOME_NAVIGATION_COMPLETE;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Insets;
import android.graphics.Outline;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Build;
import android.view.SurfaceControl;
import android.view.View;
import android.view.ViewOutlineProvider;
import android.view.WindowInsets.Type;
import android.view.WindowManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.InvariantDeviceProfile;
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.PendingAnimation;
import com.android.quickstep.AnimatedFloat;
import com.android.quickstep.GestureState;
import com.android.quickstep.OverviewComponentObserver;
import com.android.quickstep.RecentsAnimationDeviceState;
import com.android.quickstep.SwipeUpAnimationLogic;
import com.android.quickstep.SwipeUpAnimationLogic.RunningWindowAnim;
import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureResult;
import com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult;
import com.android.quickstep.util.RectFSpringAnim;
import com.android.quickstep.util.TransformParams;
import com.android.systemui.shared.system.QuickStepContract;
import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams;
/** A {@link TutorialController} for the Home tutorial. */
@TargetApi(Build.VERSION_CODES.R)
final class HomeGestureTutorialController extends TutorialController {
private float mFakeTaskViewRadius;
private Rect mFakeTaskViewRect = new Rect();
private final ViewSwipeUpAnimation mViewSwipeUpAnimation;
private RunningWindowAnim mRunningWindowAnim;
HomeGestureTutorialController(HomeGestureTutorialFragment fragment, TutorialType tutorialType) {
super(fragment, tutorialType);
RecentsAnimationDeviceState deviceState = new RecentsAnimationDeviceState(mContext);
OverviewComponentObserver observer = new OverviewComponentObserver(mContext, deviceState);
mViewSwipeUpAnimation = new ViewSwipeUpAnimation(mContext, deviceState,
new GestureState(observer, -1));
observer.onDestroy();
DeviceProfile dp = InvariantDeviceProfile.INSTANCE.get(mContext)
.getDeviceProfile(mContext)
.copy(mContext);
Insets insets = mContext.getSystemService(WindowManager.class)
.getCurrentWindowMetrics()
.getWindowInsets()
.getInsets(Type.systemBars());
dp.updateInsets(new Rect(insets.left, insets.top, insets.right, insets.bottom));
mViewSwipeUpAnimation.initDp(dp);
mFakeTaskViewRadius = QuickStepContract.getWindowCornerRadius(mContext.getResources());
mFakeTaskView.setClipToOutline(true);
mFakeTaskView.setOutlineProvider(new ViewOutlineProvider() {
@Override
public void getOutline(View view, Outline outline) {
outline.setRoundRect(mFakeTaskViewRect, mFakeTaskViewRadius);
}
});
}
private void cancelRunningAnimation() {
if (mRunningWindowAnim != null) {
mRunningWindowAnim.cancel();
}
mRunningWindowAnim = null;
}
/** Fades the task view, optionally after animating to a fake Overview. */
private void fadeOutFakeTaskView(boolean toOverviewFirst, @Nullable Runnable onEndRunnable) {
cancelRunningAnimation();
PendingAnimation anim = new PendingAnimation(300);
AnimatorListenerAdapter resetTaskView = new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation, boolean isReverse) {
mFakeTaskView.setVisibility(View.INVISIBLE);
mFakeTaskView.setAlpha(1);
mRunningWindowAnim = null;
}
};
if (toOverviewFirst) {
anim.setFloat(mViewSwipeUpAnimation.getCurrentShift(), AnimatedFloat.VALUE, 1, ACCEL);
anim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation, boolean isReverse) {
PendingAnimation fadeAnim = new PendingAnimation(300);
fadeAnim.setViewAlpha(mFakeTaskView, 0, ACCEL);
fadeAnim.addListener(resetTaskView);
AnimatorSet animset = fadeAnim.buildAnim();
animset.setStartDelay(100);
animset.start();
mRunningWindowAnim = RunningWindowAnim.wrap(animset);
}
});
} else {
anim.setViewAlpha(mFakeTaskView, 0, ACCEL);
anim.addListener(resetTaskView);
}
if (onEndRunnable != null) {
anim.addListener(AnimationSuccessListener.forRunnable(onEndRunnable));
}
AnimatorSet animset = anim.buildAnim();
animset.start();
mRunningWindowAnim = RunningWindowAnim.wrap(animset);
}
@Override
@ -85,22 +200,35 @@ final class HomeGestureTutorialController extends TutorialController {
}
@Override
public void onNavBarGestureAttempted(NavBarGestureResult result) {
public void onNavBarGestureAttempted(NavBarGestureResult result, PointF finalVelocity) {
switch (mTutorialType) {
case HOME_NAVIGATION:
switch (result) {
case HOME_GESTURE_COMPLETED:
case HOME_GESTURE_COMPLETED: {
hideFeedback();
cancelRunningAnimation();
hideHandCoachingAnimation();
mTutorialFragment.changeController(HOME_NAVIGATION_COMPLETE);
RectFSpringAnim rectAnim =
mViewSwipeUpAnimation.handleSwipeUpToHome(finalVelocity);
// After home animation finishes, fade out and then move to the next screen.
rectAnim.addAnimatorListener(AnimationSuccessListener.forRunnable(
() -> fadeOutFakeTaskView(false,
() -> mTutorialFragment.changeController(
HOME_NAVIGATION_COMPLETE))));
mRunningWindowAnim = RunningWindowAnim.wrap(rectAnim);
break;
}
case HOME_NOT_STARTED_TOO_FAR_FROM_EDGE:
case OVERVIEW_NOT_STARTED_TOO_FAR_FROM_EDGE:
showFeedback(R.string.home_gesture_feedback_swipe_too_far_from_edge);
break;
case OVERVIEW_GESTURE_COMPLETED:
showFeedback(R.string.home_gesture_feedback_overview_detected);
fadeOutFakeTaskView(true, () ->
showFeedback(R.string.home_gesture_feedback_overview_detected));
break;
case HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION:
case HOME_OR_OVERVIEW_CANCELLED:
fadeOutFakeTaskView(false, null);
showFeedback(R.string.home_gesture_feedback_wrong_swipe_direction);
break;
}
@ -112,4 +240,94 @@ final class HomeGestureTutorialController extends TutorialController {
break;
}
}
@Override
public void setNavBarGestureProgress(@Nullable Float displacement) {
if (displacement == null || mTutorialType == HOME_NAVIGATION_COMPLETE) {
mFakeTaskView.setVisibility(View.INVISIBLE);
} else {
mFakeTaskView.setVisibility(View.VISIBLE);
if (mRunningWindowAnim == null) {
mViewSwipeUpAnimation.updateDisplacement(displacement);
}
}
}
private class ViewSwipeUpAnimation extends SwipeUpAnimationLogic {
ViewSwipeUpAnimation(Context context, RecentsAnimationDeviceState deviceState,
GestureState gestureState) {
super(context, deviceState, gestureState, new FakeTransformParams());
}
void initDp(DeviceProfile dp) {
initTransitionEndpoints(dp);
mTaskViewSimulator.setPreviewBounds(
new Rect(0, 0, dp.widthPx, dp.heightPx), dp.getInsets());
}
@Override
public void updateFinalShift() {
float progress = mCurrentShift.value / mDragLengthFactor;
mWindowTransitionController.setPlayFraction(progress);
mTaskViewSimulator.apply(mTransformParams);
}
AnimatedFloat getCurrentShift() {
return mCurrentShift;
}
RectFSpringAnim handleSwipeUpToHome(PointF velocity) {
PointF velocityPxPerMs = new PointF(velocity.x, velocity.y);
float currentShift = mCurrentShift.value;
final float startShift = Utilities.boundToRange(currentShift - velocityPxPerMs.y
* getSingleFrameMs(mContext) / mTransitionDragLength, 0, mDragLengthFactor);
float distanceToTravel = (1 - currentShift) * 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. 2.
long baseDuration = Math.round(Math.abs(distanceToTravel / velocityPxPerMs.y));
long duration = Math.min(MAX_SWIPE_DURATION, 2 * baseDuration);
HomeAnimationFactory homeAnimFactory = new HomeAnimationFactory(null) {
@Override
public AnimatorPlaybackController createActivityAnimationToHome() {
return AnimatorPlaybackController.wrap(new AnimatorSet(), duration);
}
@NonNull
@Override
public RectF getWindowTargetRect() {
int fakeHomeIconSizePx = mDp.allAppsIconSizePx;
int fakeHomeIconLeft = (mDp.widthPx - fakeHomeIconSizePx) / 2;
int fakeHomeIconTop = mDp.heightPx - (mDp.allAppsCellHeightPx * 3);
return new RectF(fakeHomeIconLeft, fakeHomeIconTop,
fakeHomeIconLeft + fakeHomeIconSizePx,
fakeHomeIconTop + fakeHomeIconSizePx);
}
};
RectFSpringAnim windowAnim = createWindowAnimationToHome(startShift, homeAnimFactory);
windowAnim.start(mContext, velocityPxPerMs);
return windowAnim;
}
}
private class FakeTransformParams extends TransformParams {
@Override
public SurfaceParams[] createSurfaceParams(BuilderProxy proxy) {
SurfaceParams.Builder builder = new SurfaceParams.Builder((SurfaceControl) null);
proxy.onBuildTargetParams(builder, null, this);
return new SurfaceParams[] {builder.build()};
}
@Override
public void applySurfaceParams(SurfaceParams[] params) {
SurfaceParams p = params[0];
mFakeTaskView.setAnimationMatrix(p.matrix);
mFakeTaskViewRect.set(p.windowCrop);
mFakeTaskViewRadius = p.cornerRadius;
mFakeTaskView.invalidateOutline();
}
}
}

View File

@ -17,6 +17,7 @@ package com.android.quickstep.interaction;
import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.HOME_GESTURE_COMPLETED;
import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.HOME_NOT_STARTED_TOO_FAR_FROM_EDGE;
import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.HOME_OR_OVERVIEW_CANCELLED;
import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION;
import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.OVERVIEW_GESTURE_COMPLETED;
import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult.OVERVIEW_NOT_STARTED_TOO_FAR_FROM_EDGE;
@ -24,19 +25,23 @@ import static com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestu
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.PointF;
import android.view.Display;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.View;
import android.view.View.OnTouchListener;
import androidx.annotation.Nullable;
import com.android.launcher3.ResourceUtils;
import com.android.quickstep.SysUINavigationMode.Mode;
import com.android.quickstep.util.NavBarPosition;
import com.android.quickstep.util.TriggerSwipeUpTouchTracker;
/** Utility class to handle home gestures. */
public class NavBarGestureHandler implements OnTouchListener {
public class NavBarGestureHandler implements OnTouchListener,
TriggerSwipeUpTouchTracker.OnSwipeUpListener {
private static final String LOG_TAG = "NavBarGestureHandler";
@ -44,6 +49,7 @@ public class NavBarGestureHandler implements OnTouchListener {
private final TriggerSwipeUpTouchTracker mSwipeUpTouchTracker;
private int mBottomGestureHeight;
private boolean mTouchCameFromNavBar;
private float mDownY;
private NavBarGestureAttemptCallback mGestureCallback;
NavBarGestureHandler(Context context) {
@ -55,10 +61,11 @@ public class NavBarGestureHandler implements OnTouchListener {
displayRotation = display.getRotation();
display.getRealSize(mDisplaySize);
}
mDownY = mDisplaySize.y;
mSwipeUpTouchTracker =
new TriggerSwipeUpTouchTracker(context, true /*disableHorizontalSwipe*/,
new NavBarPosition(Mode.NO_BUTTON, displayRotation),
null /*onInterceptTouch*/, this::onSwipeUp);
null /*onInterceptTouch*/, this);
final Resources resources = context.getResources();
mBottomGestureHeight =
@ -73,16 +80,26 @@ public class NavBarGestureHandler implements OnTouchListener {
mGestureCallback = null;
}
private void onSwipeUp(boolean wasFling) {
@Override
public void onSwipeUp(boolean wasFling, PointF finalVelocity) {
if (mGestureCallback == null) {
return;
}
finalVelocity.set(finalVelocity.x / 1000, finalVelocity.y / 1000);
if (mTouchCameFromNavBar) {
mGestureCallback.onNavBarGestureAttempted(wasFling
? HOME_GESTURE_COMPLETED : OVERVIEW_GESTURE_COMPLETED);
? HOME_GESTURE_COMPLETED : OVERVIEW_GESTURE_COMPLETED, finalVelocity);
} else {
mGestureCallback.onNavBarGestureAttempted(wasFling
? HOME_NOT_STARTED_TOO_FAR_FROM_EDGE : OVERVIEW_NOT_STARTED_TOO_FAR_FROM_EDGE);
? HOME_NOT_STARTED_TOO_FAR_FROM_EDGE : OVERVIEW_NOT_STARTED_TOO_FAR_FROM_EDGE,
finalVelocity);
}
}
@Override
public void onSwipeUpCancelled() {
if (mGestureCallback != null) {
mGestureCallback.onNavBarGestureAttempted(HOME_OR_OVERVIEW_CANCELLED, new PointF());
}
}
@ -91,15 +108,22 @@ public class NavBarGestureHandler implements OnTouchListener {
int action = motionEvent.getAction();
boolean intercepted = mSwipeUpTouchTracker.interceptedTouch();
if (action == MotionEvent.ACTION_DOWN) {
mTouchCameFromNavBar = motionEvent.getRawY() >= mDisplaySize.y - mBottomGestureHeight;
mDownY = motionEvent.getY();
mTouchCameFromNavBar = mDownY >= mDisplaySize.y - mBottomGestureHeight;
if (!mTouchCameFromNavBar) {
mGestureCallback.setNavBarGestureProgress(null);
}
mSwipeUpTouchTracker.init();
} else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
if (mGestureCallback != null && !intercepted && mTouchCameFromNavBar) {
mGestureCallback.onNavBarGestureAttempted(
HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION);
HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION, new PointF());
intercepted = true;
}
}
if (mTouchCameFromNavBar && mGestureCallback != null) {
mGestureCallback.setNavBarGestureProgress(motionEvent.getY() - mDownY);
}
mSwipeUpTouchTracker.onMotionEvent(motionEvent);
return intercepted;
}
@ -110,12 +134,16 @@ public class NavBarGestureHandler implements OnTouchListener {
OVERVIEW_GESTURE_COMPLETED,
HOME_NOT_STARTED_TOO_FAR_FROM_EDGE,
OVERVIEW_NOT_STARTED_TOO_FAR_FROM_EDGE,
HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION // Side swipe on nav bar.
HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION, // Side swipe on nav bar.
HOME_OR_OVERVIEW_CANCELLED
}
/** Callback to let the UI react to attempted nav bar gestures. */
interface NavBarGestureAttemptCallback {
/** Called whenever any touch is completed. */
void onNavBarGestureAttempted(NavBarGestureResult result);
void onNavBarGestureAttempted(NavBarGestureResult result, PointF finalVelocity);
/** Indicates how far a touch originating in the nav bar has moved from the nav bar. */
void setNavBarGestureProgress(@Nullable Float displacement);
}
}

View File

@ -15,6 +15,7 @@
*/
package com.android.quickstep.interaction;
import android.content.Context;
import android.graphics.drawable.RippleDrawable;
import android.view.View;
import android.view.View.OnClickListener;
@ -39,11 +40,13 @@ abstract class TutorialController implements BackGestureAttemptCallback,
final TutorialFragment mTutorialFragment;
TutorialType mTutorialType;
final Context mContext;
final ImageButton mCloseButton;
final TextView mTitleTextView;
final TextView mSubtitleTextView;
final TextView mFeedbackView;
final View mFakeTaskView;
final View mRippleView;
final RippleDrawable mRippleDrawable;
final TutorialHandAnimation mHandCoachingAnimation;
@ -55,6 +58,7 @@ abstract class TutorialController implements BackGestureAttemptCallback,
TutorialController(TutorialFragment tutorialFragment, TutorialType tutorialType) {
mTutorialFragment = tutorialFragment;
mTutorialType = tutorialType;
mContext = mTutorialFragment.getContext();
View rootView = tutorialFragment.getRootView();
mCloseButton = rootView.findViewById(R.id.gesture_tutorial_fragment_close_button);
@ -62,6 +66,7 @@ abstract class TutorialController implements BackGestureAttemptCallback,
mTitleTextView = rootView.findViewById(R.id.gesture_tutorial_fragment_title_view);
mSubtitleTextView = rootView.findViewById(R.id.gesture_tutorial_fragment_subtitle_view);
mFeedbackView = rootView.findViewById(R.id.gesture_tutorial_fragment_feedback_view);
mFakeTaskView = rootView.findViewById(R.id.gesture_tutorial_fake_task_view);
mRippleView = rootView.findViewById(R.id.gesture_tutorial_ripple_view);
mRippleDrawable = (RippleDrawable) mRippleView.getBackground();
mHandCoachingAnimation = tutorialFragment.getHandAnimation();

View File

@ -43,6 +43,7 @@
<color name="gesture_tutorial_subtitle_color">#99000000</color> <!-- 60% black -->
<color name="gesture_tutorial_feedback_color">#FF000000</color>
<color name="gesture_tutorial_ripple_color">#A0C2F9</color> <!-- Light Blue -->
<color name="gesture_tutorial_fake_task_view_color">#6DA1FF</color> <!-- Light Blue -->
<color name="gesture_tutorial_action_button_label_color">#FFFFFFFF</color>
<color name="gesture_tutorial_primary_color">#1A73E8</color> <!-- Blue -->