f23e2d1e16
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
449 lines
19 KiB
Java
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();
|
|
}
|
|
}
|
|
}
|
|
}
|