blob: 845dca34b41f661ed8fc4d7dff83d3d2b41be3bc [file] [log] [blame]
/*
* Copyright (C) 2022 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.wm.shell.bubbles.animation;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_COLLAPSE_ANIMATOR;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_EXPANDED_VIEW_DRAGGING;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES;
import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
import static com.android.wm.shell.bubbles.BubbleExpandedView.BACKGROUND_ALPHA;
import static com.android.wm.shell.bubbles.BubbleExpandedView.BOTTOM_CLIP_PROPERTY;
import static com.android.wm.shell.bubbles.BubbleExpandedView.CONTENT_ALPHA;
import static com.android.wm.shell.bubbles.BubbleExpandedView.MANAGE_BUTTON_ALPHA;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.util.Log;
import android.view.HapticFeedbackConstants;
import android.view.ViewConfiguration;
import androidx.annotation.Nullable;
import androidx.dynamicanimation.animation.DynamicAnimation;
import androidx.dynamicanimation.animation.FloatPropertyCompat;
import androidx.dynamicanimation.animation.SpringAnimation;
import androidx.dynamicanimation.animation.SpringForce;
import com.android.wm.shell.animation.FlingAnimationUtils;
import com.android.wm.shell.animation.Interpolators;
import com.android.wm.shell.bubbles.BubbleExpandedView;
import com.android.wm.shell.bubbles.BubblePositioner;
import java.util.ArrayList;
import java.util.List;
/**
* Implementation of {@link ExpandedViewAnimationController} that uses a collapse animation to
* hide the {@link BubbleExpandedView}
*/
public class ExpandedViewAnimationControllerImpl implements ExpandedViewAnimationController {
private static final String TAG = TAG_WITH_CLASS_NAME ? "ExpandedViewAnimCtrl" : TAG_BUBBLES;
private static final float COLLAPSE_THRESHOLD = 0.02f;
private static final int COLLAPSE_DURATION_MS = 250;
private static final int MANAGE_BUTTON_ANIM_DURATION_MS = 78;
private static final int CONTENT_OPACITY_ANIM_DELAY_MS = 93;
private static final int CONTENT_OPACITY_ANIM_DURATION_MS = 78;
private static final int BACKGROUND_OPACITY_ANIM_DELAY_MS = 172;
private static final int BACKGROUND_OPACITY_ANIM_DURATION_MS = 78;
/** Animation fraction threshold for content alpha animation when stack collapse should begin */
private static final float STACK_COLLAPSE_THRESHOLD = 0.5f;
private static final FloatPropertyCompat<ExpandedViewAnimationControllerImpl>
COLLAPSE_HEIGHT_PROPERTY =
new FloatPropertyCompat<ExpandedViewAnimationControllerImpl>("CollapseSpring") {
@Override
public float getValue(ExpandedViewAnimationControllerImpl controller) {
return controller.getCollapsedAmount();
}
@Override
public void setValue(ExpandedViewAnimationControllerImpl controller,
float value) {
controller.setCollapsedAmount(value);
}
};
private final int mMinFlingVelocity;
private float mSwipeUpVelocity;
private float mSwipeDownVelocity;
private final BubblePositioner mPositioner;
private final FlingAnimationUtils mFlingAnimationUtils;
private int mDraggedAmount;
private float mCollapsedAmount;
@Nullable
private BubbleExpandedView mExpandedView;
@Nullable
private AnimatorSet mCollapseAnimation;
private boolean mNotifiedAboutThreshold;
private SpringAnimation mBackToExpandedAnimation;
@Nullable
private ObjectAnimator mBottomClipAnim;
public ExpandedViewAnimationControllerImpl(Context context, BubblePositioner positioner) {
mFlingAnimationUtils = new FlingAnimationUtils(context.getResources().getDisplayMetrics(),
COLLAPSE_DURATION_MS / 1000f);
mMinFlingVelocity = ViewConfiguration.get(context).getScaledMinimumFlingVelocity();
mPositioner = positioner;
}
private static void adjustAnimatorSetDuration(AnimatorSet animatorSet,
float durationAdjustment) {
for (Animator animator : animatorSet.getChildAnimations()) {
animator.setStartDelay((long) (animator.getStartDelay() * durationAdjustment));
animator.setDuration((long) (animator.getDuration() * durationAdjustment));
}
}
@Override
public void setExpandedView(BubbleExpandedView expandedView) {
if (mExpandedView != null) {
if (DEBUG_COLLAPSE_ANIMATOR) {
Log.d(TAG, "updating expandedView, resetting previous");
}
if (mCollapseAnimation != null) {
mCollapseAnimation.cancel();
}
if (mBackToExpandedAnimation != null) {
mBackToExpandedAnimation.cancel();
}
reset();
}
mExpandedView = expandedView;
}
@Override
public void updateDrag(float distance) {
if (mExpandedView != null) {
mDraggedAmount = OverScroll.dampedScroll(distance, mExpandedView.getContentHeight());
if (DEBUG_COLLAPSE_ANIMATOR && DEBUG_EXPANDED_VIEW_DRAGGING) {
Log.d(TAG, "updateDrag: distance=" + distance + " dragged=" + mDraggedAmount);
}
setCollapsedAmount(mDraggedAmount);
if (!mNotifiedAboutThreshold && isPastCollapseThreshold()) {
mNotifiedAboutThreshold = true;
if (DEBUG_COLLAPSE_ANIMATOR) {
Log.d(TAG, "notifying over collapse threshold");
}
vibrateIfEnabled();
}
}
}
@Override
public void setSwipeVelocity(float velocity) {
if (velocity < 0) {
mSwipeUpVelocity = Math.abs(velocity);
mSwipeDownVelocity = 0;
} else {
mSwipeUpVelocity = 0;
mSwipeDownVelocity = velocity;
}
}
@Override
public boolean shouldCollapse() {
if (mSwipeDownVelocity > mMinFlingVelocity) {
// Swipe velocity is positive and over fling velocity.
// This is a swipe down, always reset to expanded state, regardless of dragged amount.
if (DEBUG_COLLAPSE_ANIMATOR) {
Log.d(TAG,
"not collapsing expanded view, swipe down velocity: " + mSwipeDownVelocity
+ " minV: " + mMinFlingVelocity);
}
return false;
}
if (mSwipeUpVelocity > mMinFlingVelocity) {
// Swiping up and over fling velocity, collapse the view.
if (DEBUG_COLLAPSE_ANIMATOR) {
Log.d(TAG,
"collapse expanded view, swipe up velocity: " + mSwipeUpVelocity + " minV: "
+ mMinFlingVelocity);
}
return true;
}
if (isPastCollapseThreshold()) {
if (DEBUG_COLLAPSE_ANIMATOR) {
Log.d(TAG, "collapse expanded view, past threshold, dragged: " + mDraggedAmount);
}
return true;
}
if (DEBUG_COLLAPSE_ANIMATOR) {
Log.d(TAG, "not collapsing expanded view");
}
return false;
}
@Override
public void animateCollapse(Runnable startStackCollapse, Runnable after) {
if (DEBUG_COLLAPSE_ANIMATOR) {
Log.d(TAG,
"expandedView animate collapse swipeVel=" + mSwipeUpVelocity + " minFlingVel="
+ mMinFlingVelocity);
}
if (mExpandedView != null) {
// Mark it as animating immediately to avoid updates to the view before animation starts
mExpandedView.setAnimating(true);
if (mCollapseAnimation != null) {
mCollapseAnimation.cancel();
}
mCollapseAnimation = createCollapseAnimation(mExpandedView, startStackCollapse, after);
if (mSwipeUpVelocity >= mMinFlingVelocity) {
int contentHeight = mExpandedView.getContentHeight();
// Use a temp animator to get adjusted duration value for swipe.
// This new value will be used to adjust animation times proportionally in the
// animator set. If we adjust animator set duration directly, all child animations
// will get the same animation time.
ValueAnimator tempAnimator = new ValueAnimator();
mFlingAnimationUtils.applyDismissing(tempAnimator, mCollapsedAmount, contentHeight,
mSwipeUpVelocity, (contentHeight - mCollapsedAmount));
float durationAdjustment =
(float) tempAnimator.getDuration() / COLLAPSE_DURATION_MS;
adjustAnimatorSetDuration(mCollapseAnimation, durationAdjustment);
mCollapseAnimation.setInterpolator(tempAnimator.getInterpolator());
}
mCollapseAnimation.start();
}
}
@Override
public void animateBackToExpanded() {
if (DEBUG_COLLAPSE_ANIMATOR) {
Log.d(TAG, "expandedView animate back to expanded");
}
BubbleExpandedView expandedView = mExpandedView;
if (expandedView == null) {
return;
}
expandedView.setAnimating(true);
mBackToExpandedAnimation = new SpringAnimation(this, COLLAPSE_HEIGHT_PROPERTY);
mBackToExpandedAnimation.setSpring(new SpringForce()
.setStiffness(SpringForce.STIFFNESS_LOW)
.setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
);
mBackToExpandedAnimation.addEndListener(new OneTimeEndListener() {
@Override
public void onAnimationEnd(DynamicAnimation animation, boolean canceled, float value,
float velocity) {
super.onAnimationEnd(animation, canceled, value, velocity);
mNotifiedAboutThreshold = false;
mBackToExpandedAnimation = null;
expandedView.setAnimating(false);
}
});
mBackToExpandedAnimation.setStartValue(mCollapsedAmount);
mBackToExpandedAnimation.animateToFinalPosition(0);
}
@Override
public void animateForImeVisibilityChange(boolean visible) {
if (mExpandedView != null) {
if (mBottomClipAnim != null) {
mBottomClipAnim.cancel();
}
int clip = 0;
if (visible) {
// Clip the expanded view at the top of the IME view
clip = mExpandedView.getContentBottomOnScreen() - mPositioner.getImeTop();
// Don't allow negative clip value
clip = Math.max(clip, 0);
}
mBottomClipAnim = ObjectAnimator.ofInt(mExpandedView, BOTTOM_CLIP_PROPERTY, clip);
mBottomClipAnim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mBottomClipAnim = null;
}
});
mBottomClipAnim.start();
}
}
@Override
public void reset() {
if (DEBUG_COLLAPSE_ANIMATOR) {
Log.d(TAG, "reset expandedView collapsed state");
}
if (mExpandedView == null) {
return;
}
mExpandedView.setAnimating(false);
if (mCollapseAnimation != null) {
mCollapseAnimation.cancel();
}
if (mBackToExpandedAnimation != null) {
mBackToExpandedAnimation.cancel();
}
mExpandedView.setContentAlpha(1);
mExpandedView.setBackgroundAlpha(1);
mExpandedView.setManageButtonAlpha(1);
setCollapsedAmount(0);
mExpandedView.setBottomClip(0);
mExpandedView.movePointerBy(0, 0);
mCollapsedAmount = 0;
mDraggedAmount = 0;
mSwipeUpVelocity = 0;
mSwipeDownVelocity = 0;
mNotifiedAboutThreshold = false;
}
private float getCollapsedAmount() {
return mCollapsedAmount;
}
private void setCollapsedAmount(float collapsed) {
if (mCollapsedAmount != collapsed) {
float previous = mCollapsedAmount;
mCollapsedAmount = collapsed;
if (mExpandedView != null) {
if (previous == 0) {
// View was not collapsed before. Apply z order change
mExpandedView.setSurfaceZOrderedOnTop(true);
mExpandedView.setAnimating(true);
}
mExpandedView.setTopClip((int) mCollapsedAmount);
// Move up with translationY. Use negative collapsed value
mExpandedView.setContentTranslationY(-mCollapsedAmount);
mExpandedView.setManageButtonTranslationY(-mCollapsedAmount);
if (mCollapsedAmount == 0) {
// View is no longer collapsed. Revert z order change
mExpandedView.setSurfaceZOrderedOnTop(false);
mExpandedView.setAnimating(false);
}
}
}
}
private boolean isPastCollapseThreshold() {
if (mExpandedView != null) {
return mDraggedAmount > mExpandedView.getContentHeight() * COLLAPSE_THRESHOLD;
}
return false;
}
private AnimatorSet createCollapseAnimation(BubbleExpandedView expandedView,
Runnable startStackCollapse, Runnable after) {
List<Animator> animatorList = new ArrayList<>();
animatorList.add(createHeightAnimation(expandedView));
animatorList.add(createManageButtonAnimation());
ObjectAnimator contentAlphaAnimation = createContentAlphaAnimation();
final boolean[] notified = {false};
contentAlphaAnimation.addUpdateListener(animation -> {
if (!notified[0] && animation.getAnimatedFraction() > STACK_COLLAPSE_THRESHOLD) {
notified[0] = true;
// Notify bubbles that they can start moving back to the collapsed position
startStackCollapse.run();
}
});
animatorList.add(contentAlphaAnimation);
animatorList.add(createBackgroundAlphaAnimation());
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
after.run();
}
});
animatorSet.playTogether(animatorList);
return animatorSet;
}
private ValueAnimator createHeightAnimation(BubbleExpandedView expandedView) {
ValueAnimator animator = ValueAnimator.ofInt((int) mCollapsedAmount,
expandedView.getContentHeight());
animator.setInterpolator(Interpolators.EMPHASIZED_ACCELERATE);
animator.setDuration(COLLAPSE_DURATION_MS);
animator.addUpdateListener(anim -> setCollapsedAmount((int) anim.getAnimatedValue()));
return animator;
}
private ObjectAnimator createManageButtonAnimation() {
ObjectAnimator animator = ObjectAnimator.ofFloat(mExpandedView, MANAGE_BUTTON_ALPHA, 0f);
animator.setDuration(MANAGE_BUTTON_ANIM_DURATION_MS);
animator.setInterpolator(Interpolators.LINEAR);
return animator;
}
private ObjectAnimator createContentAlphaAnimation() {
ObjectAnimator animator = ObjectAnimator.ofFloat(mExpandedView, CONTENT_ALPHA, 0f);
animator.setDuration(CONTENT_OPACITY_ANIM_DURATION_MS);
animator.setInterpolator(Interpolators.LINEAR);
animator.setStartDelay(CONTENT_OPACITY_ANIM_DELAY_MS);
return animator;
}
private ObjectAnimator createBackgroundAlphaAnimation() {
ObjectAnimator animator = ObjectAnimator.ofFloat(mExpandedView, BACKGROUND_ALPHA, 0f);
animator.setDuration(BACKGROUND_OPACITY_ANIM_DURATION_MS);
animator.setInterpolator(Interpolators.LINEAR);
animator.setStartDelay(BACKGROUND_OPACITY_ANIM_DELAY_MS);
return animator;
}
@SuppressLint("MissingPermission")
private void vibrateIfEnabled() {
if (mExpandedView != null) {
mExpandedView.performHapticFeedback(HapticFeedbackConstants.DRAG_CROSSING);
}
}
}