| /* |
| * 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.SurfaceControl; |
| 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.AnimatorListeners; |
| 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.RemoteTargetGluer; |
| import com.android.quickstep.SwipeUpAnimationLogic; |
| import com.android.quickstep.SwipeUpAnimationLogic.RunningWindowAnim; |
| import com.android.quickstep.util.RectFSpringAnim; |
| import com.android.quickstep.util.TransformParams; |
| import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams; |
| |
| @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 resetFakeTaskView(boolean animateFromHome) { |
| 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(); |
| 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, 255, |
| 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 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); |
| mFakePreviousTaskView.setAnimationMatrix(p.matrix); |
| mFakeTaskViewRect.set(p.windowCrop); |
| mFakeTaskViewRadius = p.cornerRadius; |
| mFakeTaskView.invalidateOutline(); |
| mFakePreviousTaskView.invalidateOutline(); |
| } |
| } |
| } |