Files
Lawnchair/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java
T
Jon Miranda f23e2d1e16 Keep foreground drawable centered during app open/close animation.
For apps that use AdaptiveIcon, we used to animate the foreground
drawable independently from the background. This is sometimes
perceived as jank so we remove the animation since it is very subtle
in the best case scenario.

Bug: 268026344
Test: open/close apps/folders that use AdaptiveIcons
Change-Id: I500bf56e04823757d511d909a93d3b30703249a1
2023-03-20 16:13:02 +00:00

449 lines
19 KiB
Java

/*
* 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.interaction;
import static com.android.launcher3.anim.Interpolators.ACCEL;
import static com.android.launcher3.util.window.RefreshRateTracker.getSingleFrameMs;
import static com.android.launcher3.views.FloatingIconView.SHAPE_PROGRESS_DURATION;
import static com.android.quickstep.AbsSwipeUpHandler.MAX_SWIPE_DURATION;
import static com.android.quickstep.interaction.TutorialController.TutorialType.HOME_NAVIGATION_COMPLETE;
import static com.android.quickstep.interaction.TutorialController.TutorialType.OVERVIEW_NAVIGATION_COMPLETE;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ValueAnimator;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Outline;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Build;
import android.view.View;
import android.view.ViewOutlineProvider;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimatedFloat;
import com.android.launcher3.anim.AnimatorListeners;
import com.android.launcher3.anim.AnimatorPlaybackController;
import com.android.launcher3.anim.PendingAnimation;
import com.android.quickstep.GestureState;
import com.android.quickstep.OverviewComponentObserver;
import com.android.quickstep.RecentsAnimationDeviceState;
import com.android.quickstep.RemoteTargetGluer;
import com.android.quickstep.SwipeUpAnimationLogic;
import com.android.quickstep.SwipeUpAnimationLogic.RunningWindowAnim;
import com.android.quickstep.util.RecordingSurfaceTransaction;
import com.android.quickstep.util.RectFSpringAnim;
import com.android.quickstep.util.SurfaceTransaction;
import com.android.quickstep.util.SurfaceTransaction.MockProperties;
import com.android.quickstep.util.TransformParams;
@TargetApi(Build.VERSION_CODES.R)
abstract class SwipeUpGestureTutorialController extends TutorialController {
private static final int FAKE_PREVIOUS_TASK_MARGIN = Utilities.dpToPx(12);
protected static final long TASK_VIEW_END_ANIMATION_DURATION_MILLIS = 300;
private static final long HOME_SWIPE_ANIMATION_DURATION_MILLIS = 625;
private static final long OVERVIEW_SWIPE_ANIMATION_DURATION_MILLIS = 1000;
final ViewSwipeUpAnimation mTaskViewSwipeUpAnimation;
private float mFakeTaskViewRadius;
private final Rect mFakeTaskViewRect = new Rect();
RunningWindowAnim mRunningWindowAnim;
private boolean mShowTasks = false;
private boolean mShowPreviousTasks = false;
private final AnimatorListenerAdapter mResetTaskView = new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mFakeHotseatView.setVisibility(View.INVISIBLE);
mFakeIconView.setVisibility(View.INVISIBLE);
if (mTutorialFragment.getActivity() != null) {
int height = mTutorialFragment.getRootView().getFullscreenHeight();
int width = mTutorialFragment.getRootView().getWidth();
mFakeTaskViewRect.set(0, 0, width, height);
}
mFakeTaskViewRadius = 0;
mFakeTaskView.invalidateOutline();
mFakeTaskView.setVisibility(View.VISIBLE);
mFakeTaskView.setAlpha(1);
mFakePreviousTaskView.setVisibility(View.INVISIBLE);
mFakePreviousTaskView.setAlpha(1);
mFakePreviousTaskView.setToSingleRowLayout(false);
mShowTasks = false;
mShowPreviousTasks = false;
mRunningWindowAnim = null;
}
};
SwipeUpGestureTutorialController(TutorialFragment tutorialFragment, TutorialType tutorialType) {
super(tutorialFragment, tutorialType);
RecentsAnimationDeviceState deviceState = new RecentsAnimationDeviceState(mContext);
OverviewComponentObserver observer = new OverviewComponentObserver(mContext, deviceState);
mTaskViewSwipeUpAnimation = new ViewSwipeUpAnimation(mContext, deviceState,
new GestureState(observer, -1));
observer.onDestroy();
deviceState.destroy();
DeviceProfile dp = InvariantDeviceProfile.INSTANCE.get(mContext)
.getDeviceProfile(mContext)
.copy(mContext);
mTaskViewSwipeUpAnimation.initDp(dp);
int height = mTutorialFragment.getRootView().getFullscreenHeight();
int width = mTutorialFragment.getRootView().getWidth();
mFakeTaskViewRect.set(0, 0, width, height);
mFakeTaskViewRadius = 0;
ViewOutlineProvider outlineProvider = new ViewOutlineProvider() {
@Override
public void getOutline(View view, Outline outline) {
outline.setRoundRect(mFakeTaskViewRect, mFakeTaskViewRadius);
}
};
mFakeTaskView.setClipToOutline(true);
mFakeTaskView.setOutlineProvider(outlineProvider);
mFakePreviousTaskView.setClipToOutline(true);
mFakePreviousTaskView.setOutlineProvider(outlineProvider);
}
private void cancelRunningAnimation() {
if (mRunningWindowAnim != null) {
mRunningWindowAnim.cancel();
}
mRunningWindowAnim = null;
}
/** Fades the task view, optionally after animating to a fake Overview. */
void fadeOutFakeTaskView(boolean toOverviewFirst, boolean reset,
@Nullable Runnable onEndRunnable) {
cancelRunningAnimation();
PendingAnimation anim = new PendingAnimation(300);
if (toOverviewFirst) {
anim.setFloat(mTaskViewSwipeUpAnimation
.getCurrentShift(), AnimatedFloat.VALUE, 1, ACCEL);
anim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation, boolean isReverse) {
PendingAnimation fadeAnim =
new PendingAnimation(TASK_VIEW_END_ANIMATION_DURATION_MILLIS);
if (reset) {
fadeAnim.setFloat(mTaskViewSwipeUpAnimation
.getCurrentShift(), AnimatedFloat.VALUE, 0, ACCEL);
fadeAnim.addListener(mResetTaskView);
} else {
fadeAnim.setViewAlpha(mFakeTaskView, 0, ACCEL);
fadeAnim.setViewAlpha(mFakePreviousTaskView, 0, ACCEL);
}
if (onEndRunnable != null) {
fadeAnim.addListener(AnimatorListeners.forSuccessCallback(onEndRunnable));
}
AnimatorSet animset = fadeAnim.buildAnim();
if (reset && mTutorialFragment.isLargeScreen()) {
animset.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
super.onAnimationStart(animation);
Animator multiRowAnimation =
mFakePreviousTaskView.createAnimationToMultiRowLayout();
if (multiRowAnimation != null) {
multiRowAnimation.setDuration(
TASK_VIEW_END_ANIMATION_DURATION_MILLIS).start();
}
}
});
}
animset.setStartDelay(100);
animset.start();
mRunningWindowAnim = RunningWindowAnim.wrap(animset);
}
});
} else {
if (reset) {
anim.setFloat(mTaskViewSwipeUpAnimation
.getCurrentShift(), AnimatedFloat.VALUE, 0, ACCEL);
anim.addListener(mResetTaskView);
} else {
anim.setViewAlpha(mFakeTaskView, 0, ACCEL);
anim.setViewAlpha(mFakePreviousTaskView, 0, ACCEL);
}
if (onEndRunnable != null) {
anim.addListener(AnimatorListeners.forSuccessCallback(onEndRunnable));
}
}
AnimatorSet animset = anim.buildAnim();
hideFakeTaskbar(/* animateToHotseat= */ false);
animset.start();
mRunningWindowAnim = RunningWindowAnim.wrap(animset);
}
void resetFakeTaskViewFromOverview() {
resetFakeTaskView(false, false);
}
void resetFakeTaskView(boolean animateFromHome) {
resetFakeTaskView(animateFromHome, true);
}
void resetFakeTaskView(boolean animateFromHome, boolean animateTaskbar) {
mFakeTaskView.setVisibility(View.VISIBLE);
PendingAnimation anim = new PendingAnimation(300);
anim.setFloat(mTaskViewSwipeUpAnimation
.getCurrentShift(), AnimatedFloat.VALUE, 0, ACCEL);
anim.setViewAlpha(mFakeTaskView, 1, ACCEL);
anim.addListener(mResetTaskView);
AnimatorSet animset = anim.buildAnim();
if (animateTaskbar) {
showFakeTaskbar(animateFromHome);
}
animset.start();
mRunningWindowAnim = RunningWindowAnim.wrap(animset);
}
void animateFakeTaskViewHome(PointF finalVelocity, @Nullable Runnable onEndRunnable) {
cancelRunningAnimation();
hideFakeTaskbar(/* animateToHotseat= */ true);
mFakePreviousTaskView.setVisibility(View.INVISIBLE);
mFakeHotseatView.setVisibility(View.VISIBLE);
mShowPreviousTasks = false;
RectFSpringAnim rectAnim =
mTaskViewSwipeUpAnimation.handleSwipeUpToHome(finalVelocity);
// After home animation finishes, fade out and run onEndRunnable.
PendingAnimation fadeAnim = new PendingAnimation(300);
fadeAnim.setViewAlpha(mFakeIconView, 0, ACCEL);
if (onEndRunnable != null) {
fadeAnim.addListener(AnimatorListeners.forSuccessCallback(onEndRunnable));
}
AnimatorSet animset = fadeAnim.buildAnim();
rectAnim.addAnimatorListener(AnimatorListeners.forSuccessCallback(animset::start));
mRunningWindowAnim = RunningWindowAnim.wrap(rectAnim);
}
@Override
public void setNavBarGestureProgress(@Nullable Float displacement) {
if (isGestureCompleted()) {
return;
}
if (mTutorialType == HOME_NAVIGATION_COMPLETE
|| mTutorialType == OVERVIEW_NAVIGATION_COMPLETE) {
mFakeTaskView.setVisibility(View.INVISIBLE);
mFakePreviousTaskView.setVisibility(View.INVISIBLE);
} else {
mShowTasks = true;
mFakeTaskView.setVisibility(View.VISIBLE);
if (mShowPreviousTasks) {
mFakePreviousTaskView.setVisibility(View.VISIBLE);
}
if (mRunningWindowAnim == null && displacement != null) {
mTaskViewSwipeUpAnimation.updateDisplacement(displacement);
}
}
}
@Override
public void onMotionPaused(boolean unused) {
if (isGestureCompleted()) {
return;
}
if (mShowTasks) {
if (!mShowPreviousTasks) {
mFakePreviousTaskView.setTranslationX(
-(2 * mFakePreviousTaskView.getWidth() + FAKE_PREVIOUS_TASK_MARGIN));
mFakePreviousTaskView.animate()
.setDuration(300)
.translationX(-(mFakePreviousTaskView.getWidth() + FAKE_PREVIOUS_TASK_MARGIN))
.start();
}
mShowPreviousTasks = true;
}
}
class ViewSwipeUpAnimation extends SwipeUpAnimationLogic {
ViewSwipeUpAnimation(Context context, RecentsAnimationDeviceState deviceState,
GestureState gestureState) {
super(context, deviceState, gestureState);
mRemoteTargetHandles[0] = new RemoteTargetGluer.RemoteTargetHandle(
mRemoteTargetHandles[0].getTaskViewSimulator(), new FakeTransformParams());
for (RemoteTargetGluer.RemoteTargetHandle handle
: mTargetGluer.getRemoteTargetHandles()) {
// Override home screen rotation preference so that home and overview animations
// work properly
handle.getTaskViewSimulator()
.getOrientationState()
.ignoreAllowHomeRotationPreference();
}
}
void initDp(DeviceProfile dp) {
initTransitionEndpoints(dp);
mRemoteTargetHandles[0].getTaskViewSimulator().setPreviewBounds(
new Rect(0, 0, dp.widthPx, dp.heightPx), dp.getInsets());
}
@Override
public void updateFinalShift() {
mRemoteTargetHandles[0].getPlaybackController()
.setProgress(mCurrentShift.value, mDragLengthFactor);
mRemoteTargetHandles[0].getTaskViewSimulator().apply(
mRemoteTargetHandles[0].getTransformParams());
}
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() {
@Override
public AnimatorPlaybackController createActivityAnimationToHome() {
return AnimatorPlaybackController.wrap(new AnimatorSet(), duration);
}
@NonNull
@Override
public RectF getWindowTargetRect() {
int fakeHomeIconSizePx = Utilities.dpToPx(60);
int fakeHomeIconLeft = getHotseatIconLeft();
int fakeHomeIconTop = getHotseatIconTop();
return new RectF(fakeHomeIconLeft, fakeHomeIconTop,
fakeHomeIconLeft + fakeHomeIconSizePx,
fakeHomeIconTop + fakeHomeIconSizePx);
}
@Override
public void update(RectF rect, float progress, float radius) {
mFakeIconView.setVisibility(View.VISIBLE);
mFakeIconView.update(rect, progress,
1f - SHAPE_PROGRESS_DURATION /* shapeProgressStart */,
radius,
false, /* isOpening */
mFakeIconView, mDp);
mFakeIconView.setAlpha(1);
mFakeTaskView.setAlpha(getWindowAlpha(progress));
mFakePreviousTaskView.setAlpha(getWindowAlpha(progress));
}
@Override
public void onCancel() {
mFakeIconView.setVisibility(View.INVISIBLE);
}
};
RectFSpringAnim windowAnim = createWindowAnimationToHome(startShift,
homeAnimFactory)[0];
windowAnim.start(mContext, mDp, velocityPxPerMs);
return windowAnim;
}
}
protected Animator createFingerDotHomeSwipeAnimator(float fingerDotStartTranslationY) {
Animator homeSwipeAnimator = createFingerDotSwipeUpAnimator(fingerDotStartTranslationY)
.setDuration(HOME_SWIPE_ANIMATION_DURATION_MILLIS);
homeSwipeAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
animateFakeTaskViewHome(
new PointF(
0f,
fingerDotStartTranslationY / HOME_SWIPE_ANIMATION_DURATION_MILLIS),
null);
}
});
return homeSwipeAnimator;
}
protected Animator createFingerDotOverviewSwipeAnimator(float fingerDotStartTranslationY) {
Animator overviewSwipeAnimator = createFingerDotSwipeUpAnimator(fingerDotStartTranslationY)
.setDuration(OVERVIEW_SWIPE_ANIMATION_DURATION_MILLIS);
overviewSwipeAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
mFakePreviousTaskView.setVisibility(View.VISIBLE);
onMotionPaused(true /*arbitrary value*/);
}
});
return overviewSwipeAnimator;
}
private Animator createFingerDotSwipeUpAnimator(float fingerDotStartTranslationY) {
ValueAnimator swipeAnimator = ValueAnimator.ofFloat(0f, 1f);
swipeAnimator.addUpdateListener(valueAnimator -> {
float gestureProgress =
-fingerDotStartTranslationY * valueAnimator.getAnimatedFraction();
setNavBarGestureProgress(gestureProgress);
mFingerDotView.setTranslationY(fingerDotStartTranslationY + gestureProgress);
});
return swipeAnimator;
}
private class FakeTransformParams extends TransformParams {
@Override
public SurfaceTransaction createSurfaceParams(BuilderProxy proxy) {
RecordingSurfaceTransaction transaction = new RecordingSurfaceTransaction();
proxy.onBuildTargetParams(transaction.mockProperties, null, this);
return transaction;
}
@Override
public void applySurfaceParams(SurfaceTransaction params) {
if (params instanceof RecordingSurfaceTransaction) {
MockProperties p = ((RecordingSurfaceTransaction) params).mockProperties;
mFakeTaskView.setAnimationMatrix(p.matrix);
mFakePreviousTaskView.setAnimationMatrix(p.matrix);
mFakeTaskViewRect.set(p.windowCrop);
mFakeTaskViewRadius = p.cornerRadius;
mFakeTaskView.invalidateOutline();
mFakePreviousTaskView.invalidateOutline();
}
}
}
}