Merge "ActivityView animations!" into rvc-dev

This commit is contained in:
Josh Tsuji
2020-06-10 13:24:37 +00:00
committed by Android (Google) Code Review
7 changed files with 625 additions and 121 deletions

View File

@@ -1222,10 +1222,6 @@
<dimen name="bubble_dismiss_slop">16dp</dimen>
<!-- Height of button allowing users to adjust settings for bubbles. -->
<dimen name="bubble_manage_button_height">48dp</dimen>
<!-- How far, horizontally, to animate the expanded view over when animating in/out. -->
<dimen name="bubble_expanded_animate_x_distance">100dp</dimen>
<!-- How far, vertically, to animate the expanded view over when animating in/out. -->
<dimen name="bubble_expanded_animate_y_distance">500dp</dimen>
<!-- Max width of the message bubble-->
<dimen name="bubble_message_max_width">144dp</dimen>
<!-- Min width of the message bubble -->

View File

@@ -22,6 +22,7 @@ import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
import static android.graphics.PixelFormat.TRANSPARENT;
import static android.view.Display.INVALID_DISPLAY;
import static android.view.InsetsState.ITYPE_IME;
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
import static android.view.ViewRootImpl.NEW_INSETS_MODE_FULL;
import static android.view.ViewRootImpl.sNewInsetsMode;
import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS;
@@ -33,7 +34,6 @@ import static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_EXPAND
import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES;
import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.app.ActivityOptions;
import android.app.ActivityTaskManager;
@@ -46,6 +46,7 @@ import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.Insets;
import android.graphics.Outline;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.ShapeDrawable;
@@ -55,12 +56,19 @@ import android.os.RemoteException;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.SurfaceControl;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import androidx.annotation.Nullable;
import com.android.internal.policy.ScreenDecorationsUtils;
import com.android.systemui.Dependency;
import com.android.systemui.R;
@@ -88,6 +96,7 @@ public class BubbleExpandedView extends LinearLayout {
// The triangle pointing to the expanded view
private View mPointerView;
private int mPointerMargin;
@Nullable private int[] mExpandedViewContainerLocation;
private AlphaOptimizedButton mSettingsIcon;
@@ -121,6 +130,16 @@ public class BubbleExpandedView extends LinearLayout {
private View mVirtualImeView;
private WindowManager mVirtualDisplayWindowManager;
private boolean mImeShowing = false;
private float mCornerRadius = 0f;
/**
* Container for the ActivityView that has a solid, round-rect background that shows if the
* ActivityView hasn't loaded.
*/
private FrameLayout mActivityViewContainer = new FrameLayout(getContext());
/** The SurfaceView that the ActivityView draws to. */
@Nullable private SurfaceView mActivitySurface;
private ActivityView.StateCallback mStateCallback = new ActivityView.StateCallback() {
@Override
@@ -269,7 +288,28 @@ public class BubbleExpandedView extends LinearLayout {
// Set ActivityView's alpha value as zero, since there is no view content to be shown.
setContentVisibility(false);
addView(mActivityView);
mActivityViewContainer.setBackgroundColor(Color.WHITE);
mActivityViewContainer.setOutlineProvider(new ViewOutlineProvider() {
@Override
public void getOutline(View view, Outline outline) {
outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mCornerRadius);
}
});
mActivityViewContainer.setClipToOutline(true);
mActivityViewContainer.addView(mActivityView);
mActivityViewContainer.setLayoutParams(
new ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
addView(mActivityViewContainer);
if (mActivityView != null
&& mActivityView.getChildCount() > 0
&& mActivityView.getChildAt(0) instanceof SurfaceView) {
// Retrieve the surface from the ActivityView so we can screenshot it and change its
// z-ordering. This should always be possible, since ActivityView's constructor adds the
// SurfaceView as its first child.
mActivitySurface = (SurfaceView) mActivityView.getChildAt(0);
}
// Expanded stack layout, top to bottom:
// Expanded view container
@@ -327,6 +367,39 @@ public class BubbleExpandedView extends LinearLayout {
return mBubble != null ? mBubble.getKey() : "null";
}
/**
* Asks the ActivityView's surface to draw on top of all other views in the window. This is
* useful for ordering surfaces during animations, but should otherwise be set to false so that
* bubbles and menus can draw over the ActivityView.
*/
void setSurfaceZOrderedOnTop(boolean onTop) {
if (mActivitySurface == null) {
return;
}
mActivitySurface.setZOrderedOnTop(onTop, true);
}
/** Return a GraphicBuffer with the contents of the ActivityView's underlying surface. */
@Nullable SurfaceControl.ScreenshotGraphicBuffer snapshotActivitySurface() {
if (mActivitySurface == null) {
return null;
}
return SurfaceControl.captureLayers(
mActivitySurface.getSurfaceControl(),
new Rect(0, 0, mActivityView.getWidth(), mActivityView.getHeight()),
1 /* scale */);
}
int[] getActivityViewLocationOnScreen() {
if (mActivityView != null) {
return mActivityView.getLocationOnScreen();
} else {
return new int[]{0, 0};
}
}
void setManageClickListener(OnClickListener manageClickListener) {
findViewById(R.id.settings_button).setOnClickListener(manageClickListener);
}
@@ -345,12 +418,12 @@ public class BubbleExpandedView extends LinearLayout {
void applyThemeAttrs() {
final TypedArray ta = mContext.obtainStyledAttributes(
new int[] {android.R.attr.dialogCornerRadius});
float cornerRadius = ta.getDimensionPixelSize(0, 0);
mCornerRadius = ta.getDimensionPixelSize(0, 0);
ta.recycle();
if (mActivityView != null && ScreenDecorationsUtils.supportsRoundedCornersOnWindows(
mContext.getResources())) {
mActivityView.setCornerRadius(cornerRadius);
mActivityView.setCornerRadius(mCornerRadius);
}
}
@@ -398,6 +471,7 @@ public class BubbleExpandedView extends LinearLayout {
mPointerView.setAlpha(alpha);
if (mActivityView != null) {
mActivityView.setAlpha(alpha);
mActivityView.bringToFront();
}
}
@@ -557,6 +631,11 @@ public class BubbleExpandedView extends LinearLayout {
if (DEBUG_BUBBLE_EXPANDED_VIEW) {
Log.d(TAG, "updateHeight: bubble=" + getBubbleKey());
}
if (mExpandedViewContainerLocation == null) {
return;
}
if (usingActivityView()) {
float desiredHeight = mOverflowHeight;
if (!mIsOverflow) {
@@ -564,7 +643,7 @@ public class BubbleExpandedView extends LinearLayout {
}
float height = Math.min(desiredHeight, getMaxExpandedHeight());
height = Math.max(height, mIsOverflow? mOverflowHeight : mMinHeight);
LayoutParams lp = (LayoutParams) mActivityView.getLayoutParams();
ViewGroup.LayoutParams lp = mActivityView.getLayoutParams();
mNeedsNewHeight = lp.height != height;
if (!mKeyboardVisible) {
// If the keyboard is visible... don't adjust the height because that will cause
@@ -574,7 +653,8 @@ public class BubbleExpandedView extends LinearLayout {
mNeedsNewHeight = false;
}
if (DEBUG_BUBBLE_EXPANDED_VIEW) {
Log.d(TAG, "updateHeight: bubble=" + getBubbleKey() + " height=" + height
Log.d(TAG, "updateHeight: bubble=" + getBubbleKey()
+ " height=" + height
+ " mNeedsNewHeight=" + mNeedsNewHeight);
}
}
@@ -582,28 +662,40 @@ public class BubbleExpandedView extends LinearLayout {
private int getMaxExpandedHeight() {
mWindowManager.getDefaultDisplay().getRealSize(mDisplaySize);
int[] windowLocation = mActivityView.getLocationOnScreen();
int bottomInset = getRootWindowInsets() != null
? getRootWindowInsets().getStableInsetBottom()
: 0;
return mDisplaySize.y - windowLocation[1] - mSettingsIconHeight - mPointerHeight
return mDisplaySize.y
- mExpandedViewContainerLocation[1]
- getPaddingTop()
- getPaddingBottom()
- mSettingsIconHeight
- mPointerHeight
- mPointerMargin - bottomInset;
}
/**
* Update appearance of the expanded view being displayed.
*
* @param containerLocationOnScreen The location on-screen of the container the expanded view is
* added to. This allows us to calculate max height without
* waiting for layout.
*/
public void updateView() {
public void updateView(int[] containerLocationOnScreen) {
if (DEBUG_BUBBLE_EXPANDED_VIEW) {
Log.d(TAG, "updateView: bubble="
+ getBubbleKey());
}
mExpandedViewContainerLocation = containerLocationOnScreen;
if (usingActivityView()
&& mActivityView.getVisibility() == VISIBLE
&& mActivityView.isAttachedToWindow()) {
mActivityView.onLocationChanged();
updateHeight();
}
updateHeight();
}
/**

View File

@@ -56,6 +56,8 @@ import android.view.DisplayCutout;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.SurfaceControl;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
@@ -82,6 +84,7 @@ import com.android.internal.util.ContrastColorUtil;
import com.android.systemui.Interpolators;
import com.android.systemui.Prefs;
import com.android.systemui.R;
import com.android.systemui.bubbles.animation.AnimatableScaleMatrix;
import com.android.systemui.bubbles.animation.ExpandedAnimationController;
import com.android.systemui.bubbles.animation.PhysicsAnimationLayout;
import com.android.systemui.bubbles.animation.StackAnimationController;
@@ -147,6 +150,16 @@ public class BubbleStackView extends FrameLayout
StackAnimationController.IME_ANIMATION_STIFFNESS,
StackAnimationController.DEFAULT_BOUNCINESS);
private final PhysicsAnimator.SpringConfig mScaleInSpringConfig =
new PhysicsAnimator.SpringConfig(300f, 0.9f);
private final PhysicsAnimator.SpringConfig mScaleOutSpringConfig =
new PhysicsAnimator.SpringConfig(900f, 1f);
private final PhysicsAnimator.SpringConfig mTranslateSpringConfig =
new PhysicsAnimator.SpringConfig(
SpringForce.STIFFNESS_LOW, SpringForce.DAMPING_RATIO_NO_BOUNCY);
/**
* Interface to synchronize {@link View} state and the screen.
*
@@ -186,8 +199,6 @@ public class BubbleStackView extends FrameLayout
private Point mDisplaySize;
private final SpringAnimation mExpandedViewXAnim;
private final SpringAnimation mExpandedViewYAnim;
private final BubbleData mBubbleData;
private final ValueAnimator mDesaturateAndDarkenAnimator;
@@ -199,6 +210,24 @@ public class BubbleStackView extends FrameLayout
private FrameLayout mExpandedViewContainer;
/** Matrix used to scale the expanded view container with a given pivot point. */
private final AnimatableScaleMatrix mExpandedViewContainerMatrix = new AnimatableScaleMatrix();
/**
* SurfaceView that we draw screenshots of animating-out bubbles into. This allows us to animate
* between bubble activities without needing both to be alive at the same time.
*/
private SurfaceView mAnimatingOutSurfaceView;
/** Container for the animating-out SurfaceView. */
private FrameLayout mAnimatingOutSurfaceContainer;
/**
* Buffer containing a screenshot of the animating-out bubble. This is drawn into the
* SurfaceView during animations.
*/
private SurfaceControl.ScreenshotGraphicBuffer mAnimatingOutBubbleBuffer;
private BubbleFlyoutView mFlyout;
/** Runnable that fades out the flyout and then sets it to GONE. */
private Runnable mHideFlyout = () -> animateFlyoutCollapsed(true, 0 /* velX */);
@@ -230,8 +259,7 @@ public class BubbleStackView extends FrameLayout
private int mBubblePaddingTop;
private int mBubbleTouchPadding;
private int mExpandedViewPadding;
private int mExpandedAnimateXDistance;
private int mExpandedAnimateYDistance;
private int mCornerRadius;
private int mPointerHeight;
private int mStatusBarHeight;
private int mImeOffset;
@@ -680,10 +708,6 @@ public class BubbleStackView extends FrameLayout
mBubbleElevation = res.getDimensionPixelSize(R.dimen.bubble_elevation);
mBubblePaddingTop = res.getDimensionPixelSize(R.dimen.bubble_padding_top);
mBubbleTouchPadding = res.getDimensionPixelSize(R.dimen.bubble_touch_padding);
mExpandedAnimateXDistance =
res.getDimensionPixelSize(R.dimen.bubble_expanded_animate_x_distance);
mExpandedAnimateYDistance =
res.getDimensionPixelSize(R.dimen.bubble_expanded_animate_y_distance);
mPointerHeight = res.getDimensionPixelSize(R.dimen.bubble_pointer_height);
mStatusBarHeight =
@@ -698,6 +722,11 @@ public class BubbleStackView extends FrameLayout
mExpandedViewPadding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding);
int elevation = res.getDimensionPixelSize(R.dimen.bubble_elevation);
final TypedArray ta = mContext.obtainStyledAttributes(
new int[] {android.R.attr.dialogCornerRadius});
mCornerRadius = ta.getDimensionPixelSize(0, 0);
ta.recycle();
final Runnable onBubbleAnimatedOut = () -> {
if (getBubbleCount() == 0) {
allBubblesAnimatedOutAction.run();
@@ -731,6 +760,24 @@ public class BubbleStackView extends FrameLayout
mExpandedViewContainer.setClipChildren(false);
addView(mExpandedViewContainer);
mAnimatingOutSurfaceContainer = new FrameLayout(getContext());
mAnimatingOutSurfaceContainer.setLayoutParams(
new ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT));
addView(mAnimatingOutSurfaceContainer);
mAnimatingOutSurfaceView = new SurfaceView(getContext());
mAnimatingOutSurfaceView.setUseAlpha();
mAnimatingOutSurfaceView.setZOrderOnTop(true);
mAnimatingOutSurfaceView.setCornerRadius(mCornerRadius);
mAnimatingOutSurfaceView.setLayoutParams(new ViewGroup.LayoutParams(0, 0));
mAnimatingOutSurfaceContainer.addView(mAnimatingOutSurfaceView);
mAnimatingOutSurfaceContainer.setPadding(
mExpandedViewPadding,
mExpandedViewPadding,
mExpandedViewPadding,
mExpandedViewPadding);
setUpManageMenu();
setUpFlyout();
@@ -776,26 +823,6 @@ public class BubbleStackView extends FrameLayout
// MagnetizedObjects.
mMagneticTarget = new MagnetizedObject.MagneticTarget(mDismissTargetCircle, dismissRadius);
mExpandedViewXAnim =
new SpringAnimation(mExpandedViewContainer, DynamicAnimation.TRANSLATION_X);
mExpandedViewXAnim.setSpring(
new SpringForce()
.setStiffness(SpringForce.STIFFNESS_LOW)
.setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY));
mExpandedViewYAnim =
new SpringAnimation(mExpandedViewContainer, DynamicAnimation.TRANSLATION_Y);
mExpandedViewYAnim.setSpring(
new SpringForce()
.setStiffness(SpringForce.STIFFNESS_LOW)
.setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY));
mExpandedViewYAnim.addEndListener((anim, cancelled, value, velocity) -> {
if (mIsExpanded && mExpandedBubble != null
&& mExpandedBubble.getExpandedView() != null) {
mExpandedBubble.getExpandedView().updateView();
}
});
setClipChildren(false);
setFocusable(true);
mBubbleContainer.bringToFront();
@@ -830,7 +857,7 @@ public class BubbleStackView extends FrameLayout
if (mIsExpanded) {
mExpandedViewContainer.setTranslationY(getExpandedViewY());
if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
mExpandedBubble.getExpandedView().updateView();
mExpandedBubble.getExpandedView().updateView(getLocationOnScreen());
}
}
@@ -954,15 +981,10 @@ public class BubbleStackView extends FrameLayout
PhysicsAnimator.getInstance(mManageMenu).setDefaultSpringConfig(mManageSpringConfig);
final TypedArray ta = mContext.obtainStyledAttributes(
new int[] {android.R.attr.dialogCornerRadius});
final int menuCornerRadius = ta.getDimensionPixelSize(0, 0);
ta.recycle();
mManageMenu.setOutlineProvider(new ViewOutlineProvider() {
@Override
public void getOutline(View view, Outline outline) {
outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), menuCornerRadius);
outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mCornerRadius);
}
});
mManageMenu.setClipToOutline(true);
@@ -1447,6 +1469,31 @@ public class BubbleStackView extends FrameLayout
mBubbleData.setShowingOverflow(true);
}
// If we're expanded, screenshot the currently expanded bubble (before expanding the newly
// selected bubble) so we can animate it out.
if (mIsExpanded && mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
// Before screenshotting, have the real ActivityView show on top of other surfaces
// so that the screenshot doesn't flicker on top of it.
mExpandedBubble.getExpandedView().setSurfaceZOrderedOnTop(true);
}
try {
screenshotAnimatingOutBubbleIntoSurface((success) -> {
mAnimatingOutSurfaceContainer.setVisibility(
success ? View.VISIBLE : View.INVISIBLE);
showNewlySelectedBubble(bubbleToSelect);
});
} catch (Exception e) {
showNewlySelectedBubble(bubbleToSelect);
e.printStackTrace();
}
} else {
showNewlySelectedBubble(bubbleToSelect);
}
}
private void showNewlySelectedBubble(BubbleViewProvider bubbleToSelect) {
final BubbleViewProvider previouslySelected = mExpandedBubble;
mExpandedBubble = bubbleToSelect;
updatePointerPosition();
@@ -1637,50 +1684,18 @@ public class BubbleStackView extends FrameLayout
}
private void beforeExpandedViewAnimation() {
mIsExpansionAnimating = true;
hideFlyoutImmediate();
updateExpandedBubble();
updateExpandedView();
mIsExpansionAnimating = true;
}
private void afterExpandedViewAnimation() {
updateExpandedView();
mIsExpansionAnimating = false;
updateExpandedView();
requestUpdate();
}
private void animateCollapse() {
// Hide the menu if it's visible.
showManageMenu(false);
mIsExpanded = false;
final BubbleViewProvider previouslySelected = mExpandedBubble;
beforeExpandedViewAnimation();
maybeShowManageEducation(false);
if (DEBUG_BUBBLE_STACK_VIEW) {
Log.d(TAG, "animateCollapse");
Log.d(TAG, BubbleDebugConfig.formatBubblesString(getBubblesOnScreen(),
mExpandedBubble));
}
updateOverflowVisibility();
mBubbleContainer.cancelAllAnimations();
mExpandedAnimationController.collapseBackToStack(
mStackAnimationController.getStackPositionAlongNearestHorizontalEdge()
/* collapseTo */,
() -> {
mBubbleContainer.setActiveController(mStackAnimationController);
afterExpandedViewAnimation();
previouslySelected.setContentVisibility(false);
});
mExpandedViewXAnim.animateToFinalPosition(getCollapsedX());
mExpandedViewYAnim.animateToFinalPosition(getCollapsedY());
mExpandedViewContainer.animate()
.setDuration(100)
.alpha(0f);
}
private void animateExpansion() {
mIsExpanded = true;
hideStackUserEducation(true /* fromExpansion */);
@@ -1688,32 +1703,196 @@ public class BubbleStackView extends FrameLayout
mBubbleContainer.setActiveController(mExpandedAnimationController);
updateOverflowVisibility();
updatePointerPosition();
mExpandedAnimationController.expandFromStack(() -> {
updatePointerPosition();
afterExpandedViewAnimation();
maybeShowManageEducation(true);
} /* after */);
mExpandedViewContainer.setTranslationX(getCollapsedX());
mExpandedViewContainer.setTranslationY(getCollapsedY());
mExpandedViewContainer.setAlpha(0f);
mExpandedViewContainer.setTranslationX(0);
mExpandedViewContainer.setTranslationY(getExpandedViewY());
mExpandedViewContainer.setAlpha(1f);
// X-value of the bubble we're expanding, once it's settled in its row.
final float bubbleWillBeAtX =
mExpandedAnimationController.getBubbleLeft(
mBubbleData.getBubbles().indexOf(mExpandedBubble));
// How far horizontally the bubble will be animating. We'll wait a bit longer for bubbles
// that are animating farther, so that the expanded view doesn't move as much.
final float horizontalDistanceAnimated =
Math.abs(bubbleWillBeAtX
- mStackAnimationController.getStackPosition().x);
// Wait for the path animation target to reach its end, and add a small amount of extra time
// if the bubble is moving a lot horizontally.
long startDelay = 0L;
// Should not happen since we lay out before expanding, but just in case...
if (getWidth() > 0) {
startDelay = (long)
(ExpandedAnimationController.EXPAND_COLLAPSE_TARGET_ANIM_DURATION
+ (horizontalDistanceAnimated / getWidth()) * 30);
}
// Set the pivot point for the scale, so the expanded view animates out from the bubble.
mExpandedViewContainerMatrix.setScale(
0f, 0f,
bubbleWillBeAtX + mBubbleSize / 2f, getExpandedViewY());
mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix);
if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
mExpandedBubble.getExpandedView().setSurfaceZOrderedOnTop(false);
}
postDelayed(() -> PhysicsAnimator.getInstance(mExpandedViewContainerMatrix)
.spring(AnimatableScaleMatrix.SCALE_X,
AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f),
mScaleInSpringConfig)
.spring(AnimatableScaleMatrix.SCALE_Y,
AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f),
mScaleInSpringConfig)
.addUpdateListener((target, values) -> {
mExpandedViewContainerMatrix.postTranslate(
mExpandedBubble.getIconView().getTranslationX()
- bubbleWillBeAtX,
0);
mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix);
})
.withEndActions(() -> {
if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
mExpandedBubble.getExpandedView().setSurfaceZOrderedOnTop(false);
}
})
.start(), startDelay);
mExpandedViewXAnim.animateToFinalPosition(0f);
mExpandedViewYAnim.animateToFinalPosition(getExpandedViewY());
mExpandedViewContainer.animate()
.setDuration(100)
.alpha(1f);
}
private float getCollapsedX() {
return mStackAnimationController.getStackPosition().x < getWidth() / 2
? -mExpandedAnimateXDistance
: mExpandedAnimateXDistance;
private void animateCollapse() {
// Hide the menu if it's visible.
showManageMenu(false);
mIsExpanded = false;
mBubbleContainer.cancelAllAnimations();
// If we were in the middle of swapping, the animating-out surface would have been scaling
// to zero - finish it off.
PhysicsAnimator.getInstance(mAnimatingOutSurfaceContainer).cancel();
mAnimatingOutSurfaceContainer.setScaleX(0f);
mAnimatingOutSurfaceContainer.setScaleY(0f);
if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
mExpandedBubble.getExpandedView().hideImeIfVisible();
}
final long startDelay =
(long) (ExpandedAnimationController.EXPAND_COLLAPSE_TARGET_ANIM_DURATION * 0.6f);
postDelayed(() -> mExpandedAnimationController.collapseBackToStack(
mStackAnimationController.getStackPositionAlongNearestHorizontalEdge()
/* collapseTo */,
() -> {
mBubbleContainer.setActiveController(mStackAnimationController);
}), startDelay);
// We want to visually collapse into this bubble during the animation.
final View expandingFromBubble = mExpandedBubble.getIconView();
// X-value the bubble is animating from (back into the stack).
final float expandingFromBubbleAtX =
mExpandedAnimationController.getBubbleLeft(
mBubbleData.getBubbles().indexOf(mExpandedBubble));
// Set the pivot point.
mExpandedViewContainerMatrix.setScale(
1f, 1f,
expandingFromBubbleAtX + mBubbleSize / 2f,
getExpandedViewY());
PhysicsAnimator.getInstance(mExpandedViewContainerMatrix).cancel();
PhysicsAnimator.getInstance(mExpandedViewContainerMatrix)
.spring(AnimatableScaleMatrix.SCALE_X, 0f, mScaleOutSpringConfig)
.spring(AnimatableScaleMatrix.SCALE_Y, 0f, mScaleOutSpringConfig)
.addUpdateListener((target, values) -> {
if (expandingFromBubble != null) {
// Follow the bubble as it translates!
mExpandedViewContainerMatrix.postTranslate(
expandingFromBubble.getTranslationX()
- expandingFromBubbleAtX, 0f);
}
mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix);
// Hide early so we don't have a tiny little expanded view still visible at the
// end of the scale animation.
if (mExpandedViewContainerMatrix.getScaleX() < 0.05f) {
mExpandedViewContainer.setVisibility(View.INVISIBLE);
}
})
.withEndActions(() -> {
final BubbleViewProvider previouslySelected = mExpandedBubble;
beforeExpandedViewAnimation();
maybeShowManageEducation(false);
if (DEBUG_BUBBLE_STACK_VIEW) {
Log.d(TAG, "animateCollapse");
Log.d(TAG, BubbleDebugConfig.formatBubblesString(getBubblesOnScreen(),
mExpandedBubble));
}
updateOverflowVisibility();
afterExpandedViewAnimation();
previouslySelected.setContentVisibility(false);
})
.start();
}
private float getCollapsedY() {
return Math.min(mStackAnimationController.getStackPosition().y,
mExpandedAnimateYDistance);
private void animateSwitchBubbles() {
// The surface contains a screenshot of the animating out bubble, so we just need to animate
// it out (and then release the GraphicBuffer).
PhysicsAnimator.getInstance(mAnimatingOutSurfaceContainer).cancel();
PhysicsAnimator.getInstance(mAnimatingOutSurfaceContainer)
.spring(DynamicAnimation.SCALE_X, 0f, mScaleOutSpringConfig)
.spring(DynamicAnimation.SCALE_Y, 0f, mScaleOutSpringConfig)
.spring(DynamicAnimation.TRANSLATION_Y,
mAnimatingOutSurfaceContainer.getTranslationY() - mBubbleSize * 2,
mTranslateSpringConfig)
.withEndActions(this::releaseAnimatingOutBubbleBuffer)
.start();
float expandingFromBubbleDestinationX =
mExpandedAnimationController.getBubbleLeft(
mBubbleData.getBubbles().indexOf(mExpandedBubble));
mExpandedViewContainer.setAlpha(1f);
mExpandedViewContainer.setVisibility(View.VISIBLE);
mExpandedViewContainerMatrix.setScale(
0f, 0f, expandingFromBubbleDestinationX + mBubbleSize / 2f, getExpandedViewY());
mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix);
mExpandedViewContainer.postDelayed(() -> {
if (!mIsExpanded) {
return;
}
PhysicsAnimator.getInstance(mExpandedViewContainerMatrix)
.spring(AnimatableScaleMatrix.SCALE_X,
AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f),
mScaleInSpringConfig)
.spring(AnimatableScaleMatrix.SCALE_Y,
AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f),
mScaleInSpringConfig)
.addUpdateListener((target, values) -> {
mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix);
})
.withEndActions(() -> {
if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
mExpandedBubble.getExpandedView().setSurfaceZOrderedOnTop(false);
}
})
.start();
}, 25);
}
private void notifyExpansionChanged(BubbleViewProvider bubble, boolean expanded) {
@@ -2228,11 +2407,114 @@ public class BubbleStackView extends FrameLayout
if (mIsExpanded && mExpandedBubble != null
&& mExpandedBubble.getExpandedView() != null) {
BubbleExpandedView bev = mExpandedBubble.getExpandedView();
bev.setContentVisibility(false);
mExpandedViewContainerMatrix.setScaleX(0f);
mExpandedViewContainerMatrix.setScaleY(0f);
mExpandedViewContainer.setVisibility(View.INVISIBLE);
mExpandedViewContainer.setAlpha(0f);
mExpandedViewContainer.addView(bev);
bev.setManageClickListener((view) -> showManageMenu(!mShowingManage));
bev.populateExpandedView();
mExpandedViewContainer.setVisibility(VISIBLE);
mExpandedViewContainer.setAlpha(1.0f);
if (!mIsExpansionAnimating) {
mSurfaceSynchronizer.syncSurfaceAndRun(() -> {
post(this::animateSwitchBubbles);
});
}
}
}
/**
* Requests a snapshot from the currently expanded bubble's ActivityView and displays it in a
* SurfaceView. This allows us to load a newly expanded bubble's Activity into the ActivityView,
* while animating the (screenshot of the) previously selected bubble's content away.
*
* @param onComplete Callback to run once we're done here - called with 'false' if something
* went wrong, or 'true' if the SurfaceView is now showing a screenshot of the
* expanded bubble.
*/
private void screenshotAnimatingOutBubbleIntoSurface(Consumer<Boolean> onComplete) {
if (mExpandedBubble == null || mExpandedBubble.getExpandedView() == null) {
// You can't animate null.
onComplete.accept(false);
return;
}
final BubbleExpandedView animatingOutExpandedView = mExpandedBubble.getExpandedView();
// Release the previous screenshot if it hasn't been released already.
if (mAnimatingOutBubbleBuffer != null) {
releaseAnimatingOutBubbleBuffer();
}
try {
mAnimatingOutBubbleBuffer = animatingOutExpandedView.snapshotActivitySurface();
} catch (Exception e) {
// If we fail for any reason, print the stack trace and then notify the callback of our
// failure. This is not expected to occur, but it's not worth crashing over.
Log.wtf(TAG, e);
onComplete.accept(false);
}
if (mAnimatingOutBubbleBuffer == null
|| mAnimatingOutBubbleBuffer.getGraphicBuffer() == null) {
// While no exception was thrown, we were unable to get a snapshot.
onComplete.accept(false);
return;
}
// Make sure the surface container's properties have been reset.
PhysicsAnimator.getInstance(mAnimatingOutSurfaceContainer).cancel();
mAnimatingOutSurfaceContainer.setScaleX(1f);
mAnimatingOutSurfaceContainer.setScaleY(1f);
mAnimatingOutSurfaceContainer.setTranslationX(0);
mAnimatingOutSurfaceContainer.setTranslationY(0);
final int[] activityViewLocation =
mExpandedBubble.getExpandedView().getActivityViewLocationOnScreen();
final int[] surfaceViewLocation = mAnimatingOutSurfaceView.getLocationOnScreen();
// Translate the surface to overlap the real ActivityView.
mAnimatingOutSurfaceContainer.setTranslationY(
activityViewLocation[1] - surfaceViewLocation[1]);
// Set the width/height of the SurfaceView to match the snapshot.
mAnimatingOutSurfaceView.getLayoutParams().width =
mAnimatingOutBubbleBuffer.getGraphicBuffer().getWidth();
mAnimatingOutSurfaceView.getLayoutParams().height =
mAnimatingOutBubbleBuffer.getGraphicBuffer().getHeight();
mAnimatingOutSurfaceView.requestLayout();
// Post to wait for layout.
post(() -> {
// The buffer might have been destroyed if the user is mashing on bubbles, that's okay.
if (mAnimatingOutBubbleBuffer.getGraphicBuffer().isDestroyed()) {
onComplete.accept(false);
return;
}
if (!mIsExpanded) {
onComplete.accept(false);
return;
}
// Attach the buffer! We're now displaying the snapshot.
mAnimatingOutSurfaceView.getHolder().getSurface().attachAndQueueBufferWithColorSpace(
mAnimatingOutBubbleBuffer.getGraphicBuffer(),
mAnimatingOutBubbleBuffer.getColorSpace());
mSurfaceSynchronizer.syncSurfaceAndRun(() -> post(() -> onComplete.accept(true)));
});
}
/**
* Releases the buffer containing the screenshot of the animating-out bubble, if it exists and
* isn't yet destroyed.
*/
private void releaseAnimatingOutBubbleBuffer() {
if (mAnimatingOutBubbleBuffer != null
&& !mAnimatingOutBubbleBuffer.getGraphicBuffer().isDestroyed()) {
mAnimatingOutBubbleBuffer.getGraphicBuffer().destroy();
}
}
@@ -2242,19 +2524,10 @@ public class BubbleStackView extends FrameLayout
}
mExpandedViewContainer.setVisibility(mIsExpanded ? VISIBLE : GONE);
if (mIsExpanded) {
final float y = getExpandedViewY();
if (!mExpandedViewYAnim.isRunning()) {
// We're not animating so set the value
mExpandedViewContainer.setTranslationY(y);
if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
mExpandedBubble.getExpandedView().updateView();
}
} else {
// We are animating so update the value; there is an end listener on the animator
// that will ensure expandedeView.updateView gets called.
mExpandedViewYAnim.animateToFinalPosition(y);
}
if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) {
mExpandedViewContainer.setTranslationY(getExpandedViewY());
mExpandedBubble.getExpandedView().updateView(
mExpandedViewContainer.getLocationOnScreen());
}
mStackOnLeftOrWillBe = mStackAnimationController.isStackOnLeftSide();

View File

@@ -0,0 +1,137 @@
/*
* 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.systemui.bubbles.animation;
import android.graphics.Matrix;
import androidx.dynamicanimation.animation.DynamicAnimation;
import androidx.dynamicanimation.animation.FloatPropertyCompat;
/**
* Matrix whose scale properties can be animated using physics animations, via the {@link #SCALE_X}
* and {@link #SCALE_Y} FloatProperties.
*
* This is useful when you need to perform a scale animation with a pivot point, since pivot points
* are not supported by standard View scale operations but are supported by matrices.
*
* NOTE: DynamicAnimation assumes that all custom properties are denominated in pixels, and thus
* considers 1 to be the smallest user-visible change for custom properties. This means that if you
* animate {@link #SCALE_X} and {@link #SCALE_Y} to 3f, for example, the animation would have only
* three frames.
*
* To work around this, whenever animating to a desired scale value, animate to the value returned
* by {@link #getAnimatableValueForScaleFactor} instead. The SCALE_X and SCALE_Y properties will
* convert that (larger) value into the appropriate scale factor when scaling the matrix.
*/
public class AnimatableScaleMatrix extends Matrix {
/**
* The X value of the scale.
*
* NOTE: This must be set or animated to the value returned by
* {@link #getAnimatableValueForScaleFactor}, not the desired scale factor itself.
*/
public static final FloatPropertyCompat<AnimatableScaleMatrix> SCALE_X =
new FloatPropertyCompat<AnimatableScaleMatrix>("matrixScaleX") {
@Override
public float getValue(AnimatableScaleMatrix object) {
return getAnimatableValueForScaleFactor(object.mScaleX);
}
@Override
public void setValue(AnimatableScaleMatrix object, float value) {
object.setScaleX(value * DynamicAnimation.MIN_VISIBLE_CHANGE_SCALE);
}
};
/**
* The Y value of the scale.
*
* NOTE: This must be set or animated to the value returned by
* {@link #getAnimatableValueForScaleFactor}, not the desired scale factor itself.
*/
public static final FloatPropertyCompat<AnimatableScaleMatrix> SCALE_Y =
new FloatPropertyCompat<AnimatableScaleMatrix>("matrixScaleY") {
@Override
public float getValue(AnimatableScaleMatrix object) {
return getAnimatableValueForScaleFactor(object.mScaleY);
}
@Override
public void setValue(AnimatableScaleMatrix object, float value) {
object.setScaleY(value * DynamicAnimation.MIN_VISIBLE_CHANGE_SCALE);
}
};
private float mScaleX = 1f;
private float mScaleY = 1f;
private float mPivotX = 0f;
private float mPivotY = 0f;
/**
* Return the value to animate SCALE_X or SCALE_Y to in order to achieve the desired scale
* factor.
*/
public static float getAnimatableValueForScaleFactor(float scale) {
return scale * (1f / DynamicAnimation.MIN_VISIBLE_CHANGE_SCALE);
}
@Override
public void setScale(float sx, float sy, float px, float py) {
mScaleX = sx;
mScaleY = sy;
mPivotX = px;
mPivotY = py;
super.setScale(mScaleX, mScaleY, mPivotX, mPivotY);
}
public void setScaleX(float scaleX) {
mScaleX = scaleX;
super.setScale(mScaleX, mScaleY, mPivotX, mPivotY);
}
public void setScaleY(float scaleY) {
mScaleY = scaleY;
super.setScale(mScaleX, mScaleY, mPivotX, mPivotY);
}
public void setPivotX(float pivotX) {
mPivotX = pivotX;
super.setScale(mScaleX, mScaleY, mPivotX, mPivotY);
}
public void setPivotY(float pivotY) {
mPivotY = pivotY;
super.setScale(mScaleX, mScaleY, mPivotX, mPivotY);
}
public float getScaleX() {
return mScaleX;
}
public float getScaleY() {
return mScaleY;
}
public float getPivotX() {
return mPivotX;
}
public float getPivotY() {
return mPivotY;
}
}

View File

@@ -56,7 +56,7 @@ public class ExpandedAnimationController
private static final int ANIMATE_TRANSLATION_FACTOR = 4;
/** Duration of the expand/collapse target path animation. */
private static final int EXPAND_COLLAPSE_TARGET_ANIM_DURATION = 175;
public static final int EXPAND_COLLAPSE_TARGET_ANIM_DURATION = 175;
/** Stiffness for the expand/collapse path-following animation. */
private static final int EXPAND_COLLAPSE_ANIM_STIFFNESS = 1000;

View File

@@ -425,7 +425,8 @@ public class BubbleControllerTest extends SysuiTestCase {
BubbleStackView stackView = mBubbleController.getStackView();
mBubbleData.setExpanded(true);
assertTrue(mBubbleController.isStackExpanded());
verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow2.getEntry().getKey());
verify(mBubbleExpandListener, atLeastOnce()).onBubbleExpandChanged(
true, mRow2.getEntry().getKey());
assertTrue(mSysUiStateBubblesExpanded);
@@ -443,9 +444,11 @@ public class BubbleControllerTest extends SysuiTestCase {
mRow.getEntry()));
// collapse for previous bubble
verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow2.getEntry().getKey());
verify(mBubbleExpandListener, atLeastOnce()).onBubbleExpandChanged(
false, mRow2.getEntry().getKey());
// expand for selected bubble
verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getEntry().getKey());
verify(mBubbleExpandListener, atLeastOnce()).onBubbleExpandChanged(
true, mRow.getEntry().getKey());
// Collapse
mBubbleController.collapseStack();

View File

@@ -382,7 +382,8 @@ public class NewNotifPipelineBubbleControllerTest extends SysuiTestCase {
BubbleStackView stackView = mBubbleController.getStackView();
mBubbleData.setExpanded(true);
assertTrue(mBubbleController.isStackExpanded());
verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow2.getEntry().getKey());
verify(mBubbleExpandListener, atLeastOnce()).onBubbleExpandChanged(
true, mRow.getEntry().getKey());
// Last added is the one that is expanded
assertEquals(mRow2.getEntry(), mBubbleData.getSelectedBubble().getEntry());
@@ -397,9 +398,11 @@ public class NewNotifPipelineBubbleControllerTest extends SysuiTestCase {
mRow.getEntry()));
// collapse for previous bubble
verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow2.getEntry().getKey());
verify(mBubbleExpandListener, atLeastOnce()).onBubbleExpandChanged(
false, mRow2.getEntry().getKey());
// expand for selected bubble
verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getEntry().getKey());
verify(mBubbleExpandListener, atLeastOnce()).onBubbleExpandChanged(
true, mRow.getEntry().getKey());
// Collapse
mBubbleController.collapseStack();