Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 596688d1 authored by Mady Mellor's avatar Mady Mellor
Browse files

Introduce BubbleBarLayerView & animation for BubbleBarExpandedView

* BubbleBarLayerView will be used in place of BubbleStackView when the
  bubbles are shown in the bubble bar in launcher. It'll be added to
  the window manager instead and it'll host and animate the expanded
  view for bubbles from the bubble bar.

* BubbleBarExpandedViewAnimationHelper contains code to animate the
  expanded view shown in BubbleBarLayerView.

* This CL just adds the classes, usuage of them happens in a future
  CL.

Test: treehugger / manual with other CLs
Bug: 253318833
Change-Id: I79ab3cbe357439f1ec51d48c5f06504b899509f0
parent fa7f85b5
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -682,7 +682,7 @@ public class BubbleController implements ConfigurationChangeListener,
        return mBubblePositioner;
    }

    Bubbles.SysuiProxy getSysuiProxy() {
    public Bubbles.SysuiProxy getSysuiProxy() {
        return mSysuiProxy;
    }

+233 −0
Original line number Diff line number Diff line
/*
 * 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();
    }
}
+212 −0
Original line number Diff line number Diff line
/*
 * 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);
        }
    }
}