diff options
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(); |