diff options
| author | 2020-06-08 11:18:40 -0400 | |
|---|---|---|
| committer | 2020-06-10 03:59:30 +0000 | |
| commit | 06785ab656c629c993e448f91afe8e977e911b9d (patch) | |
| tree | 2cb6b8f7df757252fc9350cfc5ad3289dcba6137 | |
| parent | b1215125800bfba2b6566028842e070cb4378922 (diff) | |
ActivityView animations!
Expand/collapse animations work by applying a matrix to the expanded view container - this is a) fast b) allows for pivot scale animation c) works around some weirdness with "actually" scaling the view, since the matrix transform is applied after the AV draws.
Switch animations work by snapshotting the current bubble's surface into graphics memory, rendering that into a SurfaceView, and animating the SurfaceView out. Memory profiler indicates this does not use additional memory (since those pixels were already in graphic memory anyway, and released as soon as the animation ends).
Test: lots and lots of manual testing
Fixes: 123306815
Fixes: 135137761
Merged-In: I0b01dab4bb0c82873afc55d054bafc672bacc8bf
Change-Id: I0b01dab4bb0c82873afc55d054bafc672bacc8bf
7 files changed, 622 insertions, 118 deletions
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 73d8e9a0d8a7..399099702a6a 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -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 --> diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java index c3dcc0b3038c..89e97cd4b437 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java @@ -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(); } } @@ -551,6 +625,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) { @@ -558,7 +637,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 @@ -568,7 +647,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); } } @@ -576,28 +656,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(); } /** diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java index 95c8d08841df..8de7226f85d2 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java @@ -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; @@ -83,6 +85,7 @@ import com.android.internal.widget.ViewClippingUtil; 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; @@ -148,6 +151,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. * @@ -187,8 +200,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; @@ -200,6 +211,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 */); @@ -231,8 +260,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; @@ -699,10 +727,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 = @@ -717,6 +741,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(); @@ -750,6 +779,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(); @@ -795,26 +842,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(); @@ -849,7 +876,7 @@ public class BubbleStackView extends FrameLayout if (mIsExpanded) { mExpandedViewContainer.setTranslationY(getExpandedViewY()); if (mExpandedBubble != null && mExpandedBubble.getExpandedView() != null) { - mExpandedBubble.getExpandedView().updateView(); + mExpandedBubble.getExpandedView().updateView(getLocationOnScreen()); } } @@ -973,15 +1000,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); @@ -1467,6 +1489,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(); @@ -1657,83 +1704,215 @@ 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 animateExpansion() { + mIsExpanded = true; + hideStackUserEducation(true /* fromExpansion */); + beforeExpandedViewAnimation(); + + mBubbleContainer.setActiveController(mExpandedAnimationController); + updateOverflowVisibility(); + updatePointerPosition(); + mExpandedAnimationController.expandFromStack(() -> { + afterExpandedViewAnimation(); + maybeShowManageEducation(true); + } /* after */); + + 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); + + } + 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( + + // 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); - }); - - mExpandedViewXAnim.animateToFinalPosition(getCollapsedX()); - mExpandedViewYAnim.animateToFinalPosition(getCollapsedY()); - mExpandedViewContainer.animate() - .setDuration(100) - .alpha(0f); + }) + .start(); } - private void animateExpansion() { - mIsExpanded = true; - hideStackUserEducation(true /* fromExpansion */); - beforeExpandedViewAnimation(); + 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(); - mBubbleContainer.setActiveController(mExpandedAnimationController); - updateOverflowVisibility(); - mExpandedAnimationController.expandFromStack(() -> { - updatePointerPosition(); - afterExpandedViewAnimation(); - maybeShowManageEducation(true); - } /* after */); + float expandingFromBubbleDestinationX = + mExpandedAnimationController.getBubbleLeft( + mBubbleData.getBubbles().indexOf(mExpandedBubble)); - mExpandedViewContainer.setTranslationX(getCollapsedX()); - mExpandedViewContainer.setTranslationY(getCollapsedY()); - mExpandedViewContainer.setAlpha(0f); + mExpandedViewContainer.setAlpha(1f); + mExpandedViewContainer.setVisibility(View.VISIBLE); - mExpandedViewXAnim.animateToFinalPosition(0f); - mExpandedViewYAnim.animateToFinalPosition(getExpandedViewY()); - mExpandedViewContainer.animate() - .setDuration(100) - .alpha(1f); - } + mExpandedViewContainerMatrix.setScale( + 0f, 0f, expandingFromBubbleDestinationX + mBubbleSize / 2f, getExpandedViewY()); + mExpandedViewContainer.setAnimationMatrix(mExpandedViewContainerMatrix); - private float getCollapsedX() { - return mStackAnimationController.getStackPosition().x < getWidth() / 2 - ? -mExpandedAnimateXDistance - : mExpandedAnimateXDistance; - } + mExpandedViewContainer.postDelayed(() -> { + if (!mIsExpanded) { + return; + } - private float getCollapsedY() { - return Math.min(mStackAnimationController.getStackPosition().y, - mExpandedAnimateYDistance); + 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) { @@ -2248,11 +2427,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(); } } @@ -2262,19 +2544,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(); diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/animation/AnimatableScaleMatrix.java b/packages/SystemUI/src/com/android/systemui/bubbles/animation/AnimatableScaleMatrix.java new file mode 100644 index 000000000000..ae7833634794 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/AnimatableScaleMatrix.java @@ -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; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java b/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java index 76ff1afef3f7..86fe10dddc2c 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java @@ -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; diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java index c89f6c2597d0..29a7f9bab1a6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java @@ -446,7 +446,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); @@ -464,9 +465,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(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/NewNotifPipelineBubbleControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/NewNotifPipelineBubbleControllerTest.java index ead95ca1665e..e91867b61017 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/NewNotifPipelineBubbleControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/NewNotifPipelineBubbleControllerTest.java @@ -399,7 +399,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()); @@ -414,9 +415,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(); |