diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index 3dbb745f0c6c7..8040bd72e2a3b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -682,7 +682,7 @@ public class BubbleController implements ConfigurationChangeListener, return mBubblePositioner; } - Bubbles.SysuiProxy getSysuiProxy() { + public Bubbles.SysuiProxy getSysuiProxy() { return mSysuiProxy; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java new file mode 100644 index 0000000000000..23f65f943aa46 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java @@ -0,0 +1,233 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.bubbles.bar; + +import static android.view.View.VISIBLE; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.content.Context; +import android.graphics.PointF; +import android.util.Log; +import android.widget.FrameLayout; + +import com.android.wm.shell.animation.Interpolators; +import com.android.wm.shell.animation.PhysicsAnimator; +import com.android.wm.shell.bubbles.BubblePositioner; +import com.android.wm.shell.bubbles.BubbleViewProvider; +import com.android.wm.shell.bubbles.animation.AnimatableScaleMatrix; + +/** + * Helper class to animate a {@link BubbleBarExpandedView} on a bubble. + */ +public class BubbleBarAnimationHelper { + + private static final String TAG = BubbleBarAnimationHelper.class.getSimpleName(); + + private static final float EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT = 0.1f; + private static final float EXPANDED_VIEW_ANIMATE_OUT_SCALE_AMOUNT = .75f; + private static final int EXPANDED_VIEW_ALPHA_ANIMATION_DURATION = 150; + + /** Spring config for the expanded view scale-in animation. */ + private final PhysicsAnimator.SpringConfig mScaleInSpringConfig = + new PhysicsAnimator.SpringConfig(300f, 0.9f); + + /** Spring config for the expanded view scale-out animation. */ + private final PhysicsAnimator.SpringConfig mScaleOutSpringConfig = + new PhysicsAnimator.SpringConfig(900f, 1f); + + /** Matrix used to scale the expanded view container with a given pivot point. */ + private final AnimatableScaleMatrix mExpandedViewContainerMatrix = new AnimatableScaleMatrix(); + + /** Animator for animating the expanded view's alpha (including the TaskView inside it). */ + private final ValueAnimator mExpandedViewAlphaAnimator = ValueAnimator.ofFloat(0f, 1f); + + private final Context mContext; + private final BubbleBarLayerView mLayerView; + private final BubblePositioner mPositioner; + + private BubbleViewProvider mExpandedBubble; + private boolean mIsExpanded = false; + + public BubbleBarAnimationHelper(Context context, + BubbleBarLayerView bubbleBarLayerView, + BubblePositioner positioner) { + mContext = context; + mLayerView = bubbleBarLayerView; + mPositioner = positioner; + + mExpandedViewAlphaAnimator.setDuration(EXPANDED_VIEW_ALPHA_ANIMATION_DURATION); + mExpandedViewAlphaAnimator.setInterpolator(Interpolators.PANEL_CLOSE_ACCELERATED); + mExpandedViewAlphaAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + if (mExpandedBubble != null && mExpandedBubble.getBubbleBarExpandedView() != null) { + // We need to be Z ordered on top in order for alpha animations to work. + mExpandedBubble.getBubbleBarExpandedView().setSurfaceZOrderedOnTop(true); + mExpandedBubble.getBubbleBarExpandedView().setAnimating(true); + } + } + + @Override + public void onAnimationEnd(Animator animation) { + if (mExpandedBubble != null && mExpandedBubble.getBubbleBarExpandedView() != null) { + // The surface needs to be Z ordered on top for alpha values to work on the + // TaskView, and if we're temporarily hidden, we are still on the screen + // with alpha = 0f until we animate back. Stay Z ordered on top so the alpha + // = 0f remains in effect. + if (mIsExpanded) { + mExpandedBubble.getBubbleBarExpandedView().setSurfaceZOrderedOnTop(false); + } + + mExpandedBubble.getBubbleBarExpandedView().setContentVisibility(mIsExpanded); + mExpandedBubble.getBubbleBarExpandedView().setAnimating(false); + } + } + }); + mExpandedViewAlphaAnimator.addUpdateListener(valueAnimator -> { + if (mExpandedBubble != null && mExpandedBubble.getBubbleBarExpandedView() != null) { + float alpha = (float) valueAnimator.getAnimatedValue(); + mExpandedBubble.getBubbleBarExpandedView().setTaskViewAlpha(alpha); + mExpandedBubble.getBubbleBarExpandedView().setAlpha(alpha); + } + }); + } + + /** + * Animates the provided bubble's expanded view to the expanded state. + */ + public void animateExpansion(BubbleViewProvider expandedBubble) { + mExpandedBubble = expandedBubble; + if (mExpandedBubble == null) { + return; + } + BubbleBarExpandedView bev = mExpandedBubble.getBubbleBarExpandedView(); + if (bev == null) { + return; + } + mIsExpanded = true; + + mExpandedViewContainerMatrix.setScaleX(0f); + mExpandedViewContainerMatrix.setScaleY(0f); + + updateExpandedView(); + bev.setAnimating(true); + bev.setContentVisibility(false); + bev.setAlpha(0f); + bev.setTaskViewAlpha(0f); + bev.setVisibility(VISIBLE); + + // Set the pivot point for the scale, so the view animates out from the bubble bar. + PointF bubbleBarPosition = mPositioner.getBubbleBarPosition(); + mExpandedViewContainerMatrix.setScale( + 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, + 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, + bubbleBarPosition.x, + bubbleBarPosition.y); + + bev.setAnimationMatrix(mExpandedViewContainerMatrix); + + mExpandedViewAlphaAnimator.start(); + + PhysicsAnimator.getInstance(mExpandedViewContainerMatrix).cancel(); + PhysicsAnimator.getInstance(mExpandedViewContainerMatrix) + .spring(AnimatableScaleMatrix.SCALE_X, + AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f), + mScaleInSpringConfig) + .spring(AnimatableScaleMatrix.SCALE_Y, + AnimatableScaleMatrix.getAnimatableValueForScaleFactor(1f), + mScaleInSpringConfig) + .addUpdateListener((target, values) -> { + mExpandedBubble.getBubbleBarExpandedView().setAnimationMatrix( + mExpandedViewContainerMatrix); + }) + .withEndActions(() -> { + bev.setAnimationMatrix(null); + updateExpandedView(); + bev.setSurfaceZOrderedOnTop(false); + }) + .start(); + } + + /** + * Collapses the currently expanded bubble. + * + * @param endRunnable a runnable to run at the end of the animation. + */ + public void animateCollapse(Runnable endRunnable) { + mIsExpanded = false; + if (mExpandedBubble == null || mExpandedBubble.getBubbleBarExpandedView() == null) { + Log.w(TAG, "Trying to animate collapse without a bubble"); + return; + } + + mExpandedViewContainerMatrix.setScaleX(1f); + mExpandedViewContainerMatrix.setScaleY(1f); + + PhysicsAnimator.getInstance(mExpandedViewContainerMatrix).cancel(); + PhysicsAnimator.getInstance(mExpandedViewContainerMatrix) + .spring(AnimatableScaleMatrix.SCALE_X, + AnimatableScaleMatrix.getAnimatableValueForScaleFactor( + EXPANDED_VIEW_ANIMATE_OUT_SCALE_AMOUNT), + mScaleOutSpringConfig) + .spring(AnimatableScaleMatrix.SCALE_Y, + AnimatableScaleMatrix.getAnimatableValueForScaleFactor( + EXPANDED_VIEW_ANIMATE_OUT_SCALE_AMOUNT), + mScaleOutSpringConfig) + .addUpdateListener((target, values) -> { + if (mExpandedBubble != null + && mExpandedBubble.getBubbleBarExpandedView() != null) { + mExpandedBubble.getBubbleBarExpandedView().setAnimationMatrix( + mExpandedViewContainerMatrix); + } + }) + .withEndActions(() -> { + if (mExpandedBubble != null + && mExpandedBubble.getBubbleBarExpandedView() != null) { + mExpandedBubble.getBubbleBarExpandedView().setAnimationMatrix(null); + } + if (endRunnable != null) { + endRunnable.run(); + } + }) + .start(); + mExpandedViewAlphaAnimator.reverse(); + } + + private void updateExpandedView() { + if (mExpandedBubble == null || mExpandedBubble.getBubbleBarExpandedView() == null) { + Log.w(TAG, "Trying to update the expanded view without a bubble"); + return; + } + BubbleBarExpandedView bbev = mExpandedBubble.getBubbleBarExpandedView(); + + final int padding = mPositioner.getBubbleBarExpandedViewPadding(); + final int width = mPositioner.getExpandedViewWidthForBubbleBar(); + final int height = mPositioner.getExpandedViewHeightForBubbleBar(); + FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) bbev.getLayoutParams(); + lp.width = width; + lp.height = height; + bbev.setLayoutParams(lp); + if (mLayerView.isOnLeft()) { + bbev.setX(mPositioner.getInsets().left + padding); + } else { + bbev.setX(mPositioner.getAvailableRect().width() - width - padding); + } + bbev.setY(mPositioner.getInsets().top + padding); + bbev.updateLocation(); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java new file mode 100644 index 0000000000000..b1a725b6e5c4b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java @@ -0,0 +1,212 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.bubbles.bar; + +import static com.android.wm.shell.animation.Interpolators.ALPHA_IN; +import static com.android.wm.shell.animation.Interpolators.ALPHA_OUT; + +import android.annotation.Nullable; +import android.content.Context; +import android.graphics.Rect; +import android.graphics.Region; +import android.graphics.drawable.ColorDrawable; +import android.view.View; +import android.view.ViewTreeObserver; +import android.widget.FrameLayout; + +import com.android.wm.shell.bubbles.BubbleController; +import com.android.wm.shell.bubbles.BubblePositioner; +import com.android.wm.shell.bubbles.BubbleViewProvider; + +/** + * Similar to {@link com.android.wm.shell.bubbles.BubbleStackView}, this view is added to window + * manager to display bubbles. However, it is only used when bubbles are being displayed in + * launcher in the bubble bar. This view does not show a stack of bubbles that can be moved around + * on screen and instead shows & animates the expanded bubble for the bubble bar. + */ +public class BubbleBarLayerView extends FrameLayout + implements ViewTreeObserver.OnComputeInternalInsetsListener { + + private static final String TAG = BubbleBarLayerView.class.getSimpleName(); + + private static final float SCRIM_ALPHA = 0.2f; + + private final BubbleController mBubbleController; + private final BubblePositioner mPositioner; + private final BubbleBarAnimationHelper mAnimationHelper; + private final View mScrimView; + + @Nullable + private BubbleViewProvider mExpandedBubble; + private BubbleBarExpandedView mExpandedView; + + // TODO(b/273310265) - currently the view is always on the right, need to update for RTL. + /** Whether the expanded view is displaying on the left of the screen or not. */ + private boolean mOnLeft = false; + + /** Whether a bubble is expanded. */ + private boolean mIsExpanded = false; + + private final Region mTouchableRegion = new Region(); + private final Rect mTempRect = new Rect(); + + public BubbleBarLayerView(Context context, BubbleController controller) { + super(context); + mBubbleController = controller; + mPositioner = mBubbleController.getPositioner(); + + mAnimationHelper = new BubbleBarAnimationHelper(context, + this, mPositioner); + + mScrimView = new View(getContext()); + mScrimView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); + mScrimView.setBackgroundDrawable(new ColorDrawable( + getResources().getColor(android.R.color.system_neutral1_1000))); + addView(mScrimView); + mScrimView.setAlpha(0f); + mScrimView.setBackgroundDrawable(new ColorDrawable( + getResources().getColor(android.R.color.system_neutral1_1000))); + + setOnClickListener(view -> { + mBubbleController.collapseStack(); + }); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + mPositioner.update(); + getViewTreeObserver().addOnComputeInternalInsetsListener(this); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + getViewTreeObserver().removeOnComputeInternalInsetsListener(this); + + if (mExpandedView != null) { + removeView(mExpandedView); + mExpandedView = null; + } + } + + @Override + public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) { + inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); + mTouchableRegion.setEmpty(); + getTouchableRegion(mTouchableRegion); + inoutInfo.touchableRegion.set(mTouchableRegion); + } + + /** Updates the sizes of any displaying expanded view. */ + public void onDisplaySizeChanged() { + if (mIsExpanded && mExpandedView != null) { + updateExpandedView(); + } + } + + /** Whether the stack of bubbles is expanded or not. */ + public boolean isExpanded() { + return mIsExpanded; + } + + // (TODO: b/273310265): BubblePositioner should be source of truth when this work is done. + /** Whether the expanded view is positioned on the left or right side of the screen. */ + public boolean isOnLeft() { + return mOnLeft; + } + + /** Shows the expanded view of the provided bubble. */ + public void showExpandedView(BubbleViewProvider b) { + BubbleBarExpandedView expandedView = b.getBubbleBarExpandedView(); + if (expandedView == null) { + return; + } + if (mExpandedBubble != null && !b.getKey().equals(mExpandedBubble.getKey())) { + removeView(mExpandedView); + mExpandedView = null; + } + if (mExpandedView == null) { + mExpandedBubble = b; + mExpandedView = expandedView; + final int width = mPositioner.getExpandedViewWidthForBubbleBar(); + final int height = mPositioner.getExpandedViewHeightForBubbleBar(); + mExpandedView.setVisibility(GONE); + addView(mExpandedView, new FrameLayout.LayoutParams(width, height)); + } + + mIsExpanded = true; + mBubbleController.getSysuiProxy().onStackExpandChanged(true); + mAnimationHelper.animateExpansion(mExpandedBubble); + showScrim(true); + } + + /** Collapses any showing expanded view */ + public void collapse() { + mIsExpanded = false; + final BubbleBarExpandedView viewToRemove = mExpandedView; + mAnimationHelper.animateCollapse(() -> removeView(viewToRemove)); + mBubbleController.getSysuiProxy().onStackExpandChanged(false); + mExpandedView = null; + showScrim(false); + } + + /** Updates the expanded view size and position. */ + private void updateExpandedView() { + if (mExpandedView == null) return; + final int padding = mPositioner.getBubbleBarExpandedViewPadding(); + final int width = mPositioner.getExpandedViewWidthForBubbleBar(); + final int height = mPositioner.getExpandedViewHeightForBubbleBar(); + FrameLayout.LayoutParams lp = (LayoutParams) mExpandedView.getLayoutParams(); + lp.width = width; + lp.height = height; + mExpandedView.setLayoutParams(lp); + if (mOnLeft) { + mExpandedView.setX(mPositioner.getInsets().left + padding); + } else { + mExpandedView.setX(mPositioner.getAvailableRect().width() - width - padding); + } + mExpandedView.setY(mPositioner.getInsets().top + padding); + mExpandedView.updateLocation(); + } + + private void showScrim(boolean show) { + if (show) { + mScrimView.animate() + .setInterpolator(ALPHA_IN) + .alpha(SCRIM_ALPHA) + .start(); + } else { + mScrimView.animate() + .alpha(0f) + .setInterpolator(ALPHA_OUT) + .start(); + } + } + + /** + * Fills in the touchable region for expanded view. This is used by window manager to + * decide which touch events go to the expanded view. + */ + private void getTouchableRegion(Region outRegion) { + mTempRect.setEmpty(); + if (mIsExpanded) { + getBoundsOnScreen(mTempRect); + outRegion.op(mTempRect, Region.Op.UNION); + } + } +}