blob: 68739ba458c1b980ffd7a52ebbaccb7194879026 [file] [log] [blame]
/*
* Copyright (C) 2019 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 java.lang.annotation.RetentionPolicy.SOURCE;
import android.animation.Animator;
import android.content.Context;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.dynamicanimation.animation.DynamicAnimation.OnAnimationEndListener;
import androidx.dynamicanimation.animation.FloatPropertyCompat;
import androidx.dynamicanimation.animation.SpringAnimation;
import androidx.dynamicanimation.animation.SpringForce;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.FlingSpringAnim;
import com.android.launcher3.touch.OverScroll;
import com.android.launcher3.util.DynamicResource;
import com.android.quickstep.RemoteAnimationTargets.ReleaseCheck;
import com.android.systemui.plugins.ResourceProvider;
import java.lang.annotation.Retention;
import java.util.ArrayList;
import java.util.List;
/**
* Applies spring forces to animate from a starting rect to a target rect,
* while providing update callbacks to the caller.
*/
public class RectFSpringAnim extends ReleaseCheck {
private static final FloatPropertyCompat<RectFSpringAnim> RECT_CENTER_X =
new FloatPropertyCompat<RectFSpringAnim>("rectCenterXSpring") {
@Override
public float getValue(RectFSpringAnim anim) {
return anim.mCurrentCenterX;
}
@Override
public void setValue(RectFSpringAnim anim, float currentCenterX) {
anim.mCurrentCenterX = currentCenterX;
anim.onUpdate();
}
};
private static final FloatPropertyCompat<RectFSpringAnim> RECT_Y =
new FloatPropertyCompat<RectFSpringAnim>("rectYSpring") {
@Override
public float getValue(RectFSpringAnim anim) {
return anim.mCurrentY;
}
@Override
public void setValue(RectFSpringAnim anim, float y) {
anim.mCurrentY = y;
anim.onUpdate();
}
};
private static final FloatPropertyCompat<RectFSpringAnim> RECT_SCALE_PROGRESS =
new FloatPropertyCompat<RectFSpringAnim>("rectScaleProgress") {
@Override
public float getValue(RectFSpringAnim object) {
return object.mCurrentScaleProgress;
}
@Override
public void setValue(RectFSpringAnim object, float value) {
object.mCurrentScaleProgress = value;
object.onUpdate();
}
};
private final RectF mStartRect;
private final RectF mTargetRect;
private final RectF mCurrentRect = new RectF();
private final List<OnUpdateListener> mOnUpdateListeners = new ArrayList<>();
private final List<Animator.AnimatorListener> mAnimatorListeners = new ArrayList<>();
private float mCurrentCenterX;
private float mCurrentY;
// If true, tracking the bottom of the rects, else tracking the top.
private float mCurrentScaleProgress;
private FlingSpringAnim mRectXAnim;
private FlingSpringAnim mRectYAnim;
private SpringAnimation mRectScaleAnim;
private boolean mAnimsStarted;
private boolean mRectXAnimEnded;
private boolean mRectYAnimEnded;
private boolean mRectScaleAnimEnded;
private float mMinVisChange;
private int mMaxVelocityPxPerS;
/**
* Indicates which part of the start & target rects we are interpolating between.
*/
public static final int TRACKING_TOP = 0;
public static final int TRACKING_CENTER = 1;
public static final int TRACKING_BOTTOM = 2;
@Retention(SOURCE)
@IntDef(value = {TRACKING_TOP,
TRACKING_CENTER,
TRACKING_BOTTOM})
public @interface Tracking{}
@Tracking
public final int mTracking;
public RectFSpringAnim(RectF startRect, RectF targetRect, Context context,
@Nullable DeviceProfile deviceProfile) {
mStartRect = startRect;
mTargetRect = targetRect;
mCurrentCenterX = mStartRect.centerX();
ResourceProvider rp = DynamicResource.provider(context);
mMinVisChange = rp.getDimension(R.dimen.swipe_up_fling_min_visible_change);
mMaxVelocityPxPerS = (int) rp.getDimension(R.dimen.swipe_up_max_velocity);
setCanRelease(true);
if (deviceProfile == null) {
mTracking = startRect.bottom < targetRect.bottom
? TRACKING_BOTTOM
: TRACKING_TOP;
} else {
int heightPx = deviceProfile.heightPx;
Rect padding = deviceProfile.workspacePadding;
final float topThreshold = heightPx / 3f;
final float bottomThreshold = deviceProfile.heightPx - padding.bottom;
if (targetRect.bottom > bottomThreshold) {
mTracking = TRACKING_BOTTOM;
} else if (targetRect.top < topThreshold) {
mTracking = TRACKING_TOP;
} else {
mTracking = TRACKING_CENTER;
}
}
mCurrentY = getTrackedYFromRect(mStartRect);
}
private float getTrackedYFromRect(RectF rect) {
switch (mTracking) {
case TRACKING_TOP:
return rect.top;
case TRACKING_BOTTOM:
return rect.bottom;
case TRACKING_CENTER:
default:
return rect.centerY();
}
}
public void onTargetPositionChanged() {
if (mRectXAnim != null && mRectXAnim.getTargetPosition() != mTargetRect.centerX()) {
mRectXAnim.updatePosition(mCurrentCenterX, mTargetRect.centerX());
}
if (mRectYAnim != null) {
switch (mTracking) {
case TRACKING_TOP:
if (mRectYAnim.getTargetPosition() != mTargetRect.top) {
mRectYAnim.updatePosition(mCurrentY, mTargetRect.top);
}
break;
case TRACKING_BOTTOM:
if (mRectYAnim.getTargetPosition() != mTargetRect.bottom) {
mRectYAnim.updatePosition(mCurrentY, mTargetRect.bottom);
}
break;
case TRACKING_CENTER:
if (mRectYAnim.getTargetPosition() != mTargetRect.centerY()) {
mRectYAnim.updatePosition(mCurrentY, mTargetRect.centerY());
}
break;
}
}
}
public void addOnUpdateListener(OnUpdateListener onUpdateListener) {
mOnUpdateListeners.add(onUpdateListener);
}
public void addAnimatorListener(Animator.AnimatorListener animatorListener) {
mAnimatorListeners.add(animatorListener);
}
/**
* Starts the fling/spring animation.
* @param context The activity context.
* @param velocityPxPerMs Velocity of swipe in px/ms.
*/
public void start(Context context, @Nullable DeviceProfile profile, PointF velocityPxPerMs) {
// Only tell caller that we ended if both x and y animations have ended.
OnAnimationEndListener onXEndListener = ((animation, canceled, centerX, velocityX) -> {
mRectXAnimEnded = true;
maybeOnEnd();
});
OnAnimationEndListener onYEndListener = ((animation, canceled, centerY, velocityY) -> {
mRectYAnimEnded = true;
maybeOnEnd();
});
// We dampen the user velocity here to keep the natural feeling and to prevent the
// rect from straying too from a linear path.
final float xVelocityPxPerS = velocityPxPerMs.x * 1000;
final float yVelocityPxPerS = velocityPxPerMs.y * 1000;
final float dampedXVelocityPxPerS = OverScroll.dampedScroll(
Math.abs(xVelocityPxPerS), mMaxVelocityPxPerS) * Math.signum(xVelocityPxPerS);
final float dampedYVelocityPxPerS = OverScroll.dampedScroll(
Math.abs(yVelocityPxPerS), mMaxVelocityPxPerS) * Math.signum(yVelocityPxPerS);
float startX = mCurrentCenterX;
float endX = mTargetRect.centerX();
float minXValue = Math.min(startX, endX);
float maxXValue = Math.max(startX, endX);
mRectXAnim = new FlingSpringAnim(this, context, RECT_CENTER_X, startX, endX,
dampedXVelocityPxPerS, mMinVisChange, minXValue, maxXValue, onXEndListener);
float startY = mCurrentY;
float endY = getTrackedYFromRect(mTargetRect);
float minYValue = Math.min(startY, endY);
float maxYValue = Math.max(startY, endY);
mRectYAnim = new FlingSpringAnim(this, context, RECT_Y, startY, endY, dampedYVelocityPxPerS,
mMinVisChange, minYValue, maxYValue, onYEndListener);
float minVisibleChange = Math.abs(1f / mStartRect.height());
ResourceProvider rp = DynamicResource.provider(context);
float damping = rp.getFloat(R.dimen.swipe_up_rect_scale_damping_ratio);
// Increase the stiffness for devices where we want the window size to transform quicker.
boolean shouldUseHigherStiffness = profile != null
&& (profile.isLandscape || profile.isTablet);
float stiffness = shouldUseHigherStiffness
? rp.getFloat(R.dimen.swipe_up_rect_scale_higher_stiffness)
: rp.getFloat(R.dimen.swipe_up_rect_scale_stiffness);
mRectScaleAnim = new SpringAnimation(this, RECT_SCALE_PROGRESS)
.setSpring(new SpringForce(1f)
.setDampingRatio(damping)
.setStiffness(stiffness))
.setStartVelocity(velocityPxPerMs.y * minVisibleChange)
.setMaxValue(1f)
.setMinimumVisibleChange(minVisibleChange)
.addEndListener((animation, canceled, value, velocity) -> {
mRectScaleAnimEnded = true;
maybeOnEnd();
});
setCanRelease(false);
mAnimsStarted = true;
mRectXAnim.start();
mRectYAnim.start();
mRectScaleAnim.start();
for (Animator.AnimatorListener animatorListener : mAnimatorListeners) {
animatorListener.onAnimationStart(null);
}
}
public void end() {
if (mAnimsStarted) {
mRectXAnim.end();
mRectYAnim.end();
if (mRectScaleAnim.canSkipToEnd()) {
mRectScaleAnim.skipToEnd();
}
}
mRectXAnimEnded = true;
mRectYAnimEnded = true;
mRectScaleAnimEnded = true;
maybeOnEnd();
}
private boolean isEnded() {
return mRectXAnimEnded && mRectYAnimEnded && mRectScaleAnimEnded;
}
private void onUpdate() {
if (isEnded()) {
// Prevent further updates from being called. This can happen between callbacks for
// ending the x/y/scale animations.
return;
}
if (!mOnUpdateListeners.isEmpty()) {
float currentWidth = Utilities.mapRange(mCurrentScaleProgress, mStartRect.width(),
mTargetRect.width());
float currentHeight = Utilities.mapRange(mCurrentScaleProgress, mStartRect.height(),
mTargetRect.height());
switch (mTracking) {
case TRACKING_TOP:
mCurrentRect.set(mCurrentCenterX - currentWidth / 2,
mCurrentY,
mCurrentCenterX + currentWidth / 2,
mCurrentY + currentHeight);
break;
case TRACKING_BOTTOM:
mCurrentRect.set(mCurrentCenterX - currentWidth / 2,
mCurrentY - currentHeight,
mCurrentCenterX + currentWidth / 2,
mCurrentY);
break;
case TRACKING_CENTER:
mCurrentRect.set(mCurrentCenterX - currentWidth / 2,
mCurrentY - currentHeight / 2,
mCurrentCenterX + currentWidth / 2,
mCurrentY + currentHeight / 2);
break;
}
for (OnUpdateListener onUpdateListener : mOnUpdateListeners) {
onUpdateListener.onUpdate(mCurrentRect, mCurrentScaleProgress);
}
}
}
private void maybeOnEnd() {
if (mAnimsStarted && isEnded()) {
mAnimsStarted = false;
setCanRelease(true);
for (Animator.AnimatorListener animatorListener : mAnimatorListeners) {
animatorListener.onAnimationEnd(null);
}
}
}
public void cancel() {
if (mAnimsStarted) {
for (OnUpdateListener onUpdateListener : mOnUpdateListeners) {
onUpdateListener.onCancel();
}
}
end();
}
public interface OnUpdateListener {
/**
* Called when an update is made to the animation.
* @param currentRect The rect of the window.
* @param progress [0, 1] The progress of the rect scale animation.
*/
void onUpdate(RectF currentRect, float progress);
default void onCancel() { }
}
}