blob: deb70e0f8befa6b320e1a1c9b41908ee8e7adcfd [file] [log] [blame]
/*
* 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.util;
import static com.android.launcher3.anim.Interpolators.DEACCEL;
import static com.android.launcher3.anim.Interpolators.LINEAR;
import static com.android.quickstep.SysUINavigationMode.Mode.TWO_BUTTONS;
import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION;
import android.animation.TimeInterpolator;
import android.content.Context;
import android.graphics.Matrix;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.util.FloatProperty;
import androidx.annotation.Nullable;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Launcher;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimatorPlaybackController;
import com.android.launcher3.anim.PendingAnimation;
import com.android.launcher3.touch.PagedOrientationHandler;
import com.android.quickstep.LauncherActivityInterface;
import com.android.quickstep.SysUINavigationMode;
import com.android.quickstep.views.RecentsView;
/**
* Controls an animation that can go beyond progress = 1, at which point resistance should be
* applied. Internally, this is a wrapper around 2 {@link AnimatorPlaybackController}s, one that
* runs from progress 0 to 1 like normal, then one that seamlessly continues that animation but
* starts applying resistance as well.
*/
public class AnimatorControllerWithResistance {
/**
* How much farther we can drag past overview in 2-button mode, as a factor of the distance
* it takes to drag from an app to overview.
*/
public static final float TWO_BUTTON_EXTRA_DRAG_FACTOR = 0.25f;
private enum RecentsResistanceParams {
FROM_APP(0.75f, 0.5f, 1f),
FROM_OVERVIEW(1f, 0.75f, 0.5f);
RecentsResistanceParams(float scaleStartResist, float scaleMaxResist,
float translationFactor) {
this.scaleStartResist = scaleStartResist;
this.scaleMaxResist = scaleMaxResist;
this.translationFactor = translationFactor;
}
/**
* Start slowing down the rate of scaling down when recents view is smaller than this scale.
*/
public final float scaleStartResist;
/**
* Recents view will reach this scale at the very end of the drag.
*/
public final float scaleMaxResist;
/**
* How much translation to apply to RecentsView when the drag reaches the top of the screen,
* where 0 will keep it centered and 1 will have it barely touch the top of the screen.
*/
public final float translationFactor;
}
private static final TimeInterpolator RECENTS_SCALE_RESIST_INTERPOLATOR = DEACCEL;
private static final TimeInterpolator RECENTS_TRANSLATE_RESIST_INTERPOLATOR = LINEAR;
private final AnimatorPlaybackController mNormalController;
private final AnimatorPlaybackController mResistanceController;
// Initialize to -1 so the first 0 gets applied.
private float mLastNormalProgress = -1;
private float mLastResistProgress;
public AnimatorControllerWithResistance(AnimatorPlaybackController normalController,
AnimatorPlaybackController resistanceController) {
mNormalController = normalController;
mResistanceController = resistanceController;
}
public AnimatorPlaybackController getNormalController() {
return mNormalController;
}
/**
* Applies the current progress of the animation.
* @param progress From 0 to maxProgress, where 1 is the target we are animating towards.
* @param maxProgress > 1, this is where the resistance will be applied.
*/
public void setProgress(float progress, float maxProgress) {
float normalProgress = Utilities.boundToRange(progress, 0, 1);
if (normalProgress != mLastNormalProgress) {
mLastNormalProgress = normalProgress;
mNormalController.setPlayFraction(normalProgress);
}
if (maxProgress <= 1) {
return;
}
float resistProgress = progress <= 1 ? 0 : Utilities.getProgress(progress, 1, maxProgress);
if (resistProgress != mLastResistProgress) {
mLastResistProgress = resistProgress;
mResistanceController.setPlayFraction(resistProgress);
}
}
/**
* Applies resistance to recents when swiping up past its target position.
* @param normalController The controller to run from 0 to 1 before this resistance applies.
* @param context Used to compute start and end values.
* @param recentsOrientedState Used to compute start and end values.
* @param dp Used to compute start and end values.
* @param scaleTarget The target for the scaleProperty.
* @param scaleProperty Animate the value to change the scale of the window/recents view.
* @param translationTarget The target for the translationProperty.
* @param translationProperty Animate the value to change the translation of the recents view.
*/
public static <SCALE, TRANSLATION> AnimatorControllerWithResistance createForRecents(
AnimatorPlaybackController normalController, Context context,
RecentsOrientedState recentsOrientedState, DeviceProfile dp, SCALE scaleTarget,
FloatProperty<SCALE> scaleProperty, TRANSLATION translationTarget,
FloatProperty<TRANSLATION> translationProperty) {
RecentsParams params = new RecentsParams(context, recentsOrientedState, dp, scaleTarget,
scaleProperty, translationTarget, translationProperty);
PendingAnimation resistAnim = createRecentsResistanceAnim(params);
AnimatorPlaybackController resistanceController = resistAnim.createPlaybackController();
return new AnimatorControllerWithResistance(normalController, resistanceController);
}
/**
* Creates the resistance animation for {@link #createForRecents}, or can be used separately
* when starting from recents, i.e. {@link #createRecentsResistanceFromOverviewAnim}.
*/
public static <SCALE, TRANSLATION> PendingAnimation createRecentsResistanceAnim(
RecentsParams<SCALE, TRANSLATION> params) {
Rect startRect = new Rect();
PagedOrientationHandler orientationHandler = params.recentsOrientedState
.getOrientationHandler();
LauncherActivityInterface.INSTANCE.calculateTaskSize(params.context, params.dp, startRect,
orientationHandler);
long distanceToCover = startRect.bottom;
boolean isTwoButtonMode = SysUINavigationMode.getMode(params.context) == TWO_BUTTONS;
if (isTwoButtonMode) {
// We can only drag a small distance past overview, not to the top of the screen.
distanceToCover = (long)
((params.dp.heightPx - startRect.bottom) * TWO_BUTTON_EXTRA_DRAG_FACTOR);
}
PendingAnimation resistAnim = params.resistAnim != null
? params.resistAnim
: new PendingAnimation(distanceToCover * 2);
PointF pivot = new PointF();
float fullscreenScale = params.recentsOrientedState.getFullScreenScaleAndPivot(
startRect, params.dp, pivot);
float prevScaleRate = (fullscreenScale - params.startScale)
/ (params.dp.heightPx - startRect.bottom);
// This is what the scale would be at the end of the drag if we didn't apply resistance.
float endScale = params.startScale - prevScaleRate * distanceToCover;
final TimeInterpolator scaleInterpolator;
if (isTwoButtonMode) {
// We are bounded by the distance of the drag, so we don't need to apply resistance.
scaleInterpolator = LINEAR;
} else {
// Create an interpolator that resists the scale so the scale doesn't get smaller than
// RECENTS_SCALE_MAX_RESIST.
float startResist = Utilities.getProgress(params.resistanceParams.scaleStartResist,
params.startScale, endScale);
float maxResist = Utilities.getProgress(params.resistanceParams.scaleMaxResist,
params.startScale, endScale);
scaleInterpolator = t -> {
if (t < startResist) {
return t;
}
float resistProgress = Utilities.getProgress(t, startResist, 1);
resistProgress = RECENTS_SCALE_RESIST_INTERPOLATOR.getInterpolation(resistProgress);
return startResist + resistProgress * (maxResist - startResist);
};
}
resistAnim.addFloat(params.scaleTarget, params.scaleProperty, params.startScale, endScale,
scaleInterpolator);
if (!isTwoButtonMode) {
// Compute where the task view would be based on the end scale, if we didn't translate.
RectF endRectF = new RectF(startRect);
Matrix temp = new Matrix();
temp.setScale(params.resistanceParams.scaleMaxResist,
params.resistanceParams.scaleMaxResist, pivot.x, pivot.y);
temp.mapRect(endRectF);
// Translate such that the task view touches the top of the screen when drag does.
float endTranslation = endRectF.top
* orientationHandler.getSecondaryTranslationDirectionFactor()
* params.resistanceParams.translationFactor;
resistAnim.addFloat(params.translationTarget, params.translationProperty,
params.startTranslation, endTranslation, RECENTS_TRANSLATE_RESIST_INTERPOLATOR);
}
return resistAnim;
}
/**
* Helper method to update or create a PendingAnimation suitable for animating
* a RecentsView interaction that started from the overview state.
*/
public static PendingAnimation createRecentsResistanceFromOverviewAnim(
Launcher launcher, @Nullable PendingAnimation resistanceAnim) {
RecentsView recentsView = launcher.getOverviewPanel();
RecentsParams params = new RecentsParams(launcher, recentsView.getPagedViewOrientedState(),
launcher.getDeviceProfile(), recentsView, RECENTS_SCALE_PROPERTY, recentsView,
TASK_SECONDARY_TRANSLATION)
.setResistAnim(resistanceAnim)
.setResistanceParams(RecentsResistanceParams.FROM_OVERVIEW)
.setStartScale(recentsView.getScaleX());
return createRecentsResistanceAnim(params);
}
/**
* Params to compute resistance when scaling/translating recents.
*/
private static class RecentsParams<SCALE, TRANSLATION> {
// These are all required and can't have default values, hence are final.
public final Context context;
public final RecentsOrientedState recentsOrientedState;
public final DeviceProfile dp;
public final SCALE scaleTarget;
public final FloatProperty<SCALE> scaleProperty;
public final TRANSLATION translationTarget;
public final FloatProperty<TRANSLATION> translationProperty;
// These are not required, or can have a default value that is generally correct.
@Nullable public PendingAnimation resistAnim = null;
public RecentsResistanceParams resistanceParams = RecentsResistanceParams.FROM_APP;
public float startScale = 1f;
public float startTranslation = 0f;
private RecentsParams(Context context, RecentsOrientedState recentsOrientedState,
DeviceProfile dp, SCALE scaleTarget, FloatProperty<SCALE> scaleProperty,
TRANSLATION translationTarget, FloatProperty<TRANSLATION> translationProperty) {
this.context = context;
this.recentsOrientedState = recentsOrientedState;
this.dp = dp;
this.scaleTarget = scaleTarget;
this.scaleProperty = scaleProperty;
this.translationTarget = translationTarget;
this.translationProperty = translationProperty;
}
private RecentsParams setResistAnim(PendingAnimation resistAnim) {
this.resistAnim = resistAnim;
return this;
}
private RecentsParams setResistanceParams(RecentsResistanceParams resistanceParams) {
this.resistanceParams = resistanceParams;
return this;
}
private RecentsParams setStartScale(float startScale) {
this.startScale = startScale;
return this;
}
private RecentsParams setStartTranslation(float startTranslation) {
this.startTranslation = startTranslation;
return this;
}
}
}