/* * 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.launcher3.taskbar.bubbles; import static java.lang.Math.abs; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.annotation.Nullable; import android.view.InsetsController; import android.view.MotionEvent; import android.view.View; import com.android.launcher3.R; import com.android.launcher3.anim.AnimatedFloat; import com.android.launcher3.taskbar.StashedHandleViewController; import com.android.launcher3.taskbar.TaskbarActivityContext; import com.android.launcher3.taskbar.TaskbarControllers; import com.android.launcher3.taskbar.TaskbarInsetsController; import com.android.launcher3.taskbar.TaskbarStashController; import com.android.launcher3.util.MultiPropertyFactory; import com.android.wm.shell.common.bubbles.BubbleBarLocation; import com.android.wm.shell.shared.animation.PhysicsAnimator; /** * Coordinates between controllers such as BubbleBarView and BubbleHandleViewController to * create a cohesive animation between stashed/unstashed states. */ public class BubbleStashController { private static final String TAG = "BubbleStashController"; /** * How long to stash/unstash. */ public static final long BAR_STASH_DURATION = InsetsController.ANIMATION_DURATION_RESIZE; /** * The scale bubble bar animates to when being stashed. */ private static final float STASHED_BAR_SCALE = 0.5f; protected final TaskbarActivityContext mActivity; // Initialized in init. private TaskbarControllers mControllers; private TaskbarInsetsController mTaskbarInsetsController; private BubbleBarViewController mBarViewController; private BubbleStashedHandleViewController mHandleViewController; private TaskbarStashController mTaskbarStashController; private MultiPropertyFactory.MultiProperty mIconAlphaForStash; private AnimatedFloat mIconScaleForStash; private AnimatedFloat mIconTranslationYForStash; private MultiPropertyFactory.MultiProperty mBubbleStashedHandleAlpha; private boolean mRequestedStashState; private boolean mRequestedExpandedState; private boolean mIsStashed; private int mStashedHeight; private int mUnstashedHeight; private boolean mBubblesShowingOnHome; private boolean mBubblesShowingOnOverview; private boolean mIsSysuiLocked; private final float mHandleCenterFromScreenBottom; @Nullable private AnimatorSet mAnimator; public BubbleStashController(TaskbarActivityContext activity) { mActivity = activity; // the handle is centered within the stashed taskbar area mHandleCenterFromScreenBottom = mActivity.getResources().getDimensionPixelSize(R.dimen.bubblebar_stashed_size) / 2f; } public void init(TaskbarControllers controllers, BubbleControllers bubbleControllers) { mControllers = controllers; mTaskbarInsetsController = controllers.taskbarInsetsController; mBarViewController = bubbleControllers.bubbleBarViewController; mHandleViewController = bubbleControllers.bubbleStashedHandleViewController; mTaskbarStashController = controllers.taskbarStashController; mIconAlphaForStash = mBarViewController.getBubbleBarAlpha().get(0); mIconScaleForStash = mBarViewController.getBubbleBarScale(); mIconTranslationYForStash = mBarViewController.getBubbleBarTranslationY(); mBubbleStashedHandleAlpha = mHandleViewController.getStashedHandleAlpha().get( StashedHandleViewController.ALPHA_INDEX_STASHED); mStashedHeight = mHandleViewController.getStashedHeight(); mUnstashedHeight = mHandleViewController.getUnstashedHeight(); } /** * Returns the touchable height of the bubble bar based on it's stashed state. */ public int getTouchableHeight() { return mIsStashed ? mStashedHeight : mUnstashedHeight; } /** * Returns whether the bubble bar is currently stashed. */ public boolean isStashed() { return mIsStashed; } /** * Animates the handle (or bubble bar depending on state) to be visible after the device is * unlocked. * *

Normally either the bubble bar or the handle is visible, * and {@link #showBubbleBar(boolean)} and {@link #stashBubbleBar()} are used to transition * between these two states. But the transition from the state where both the bar and handle * are invisible is slightly different. */ private void animateAfterUnlock() { AnimatorSet animatorSet = new AnimatorSet(); if (mBubblesShowingOnHome || mBubblesShowingOnOverview) { mIsStashed = false; animatorSet.playTogether(mIconScaleForStash.animateToValue(1), mIconTranslationYForStash.animateToValue(getBubbleBarTranslationY()), mIconAlphaForStash.animateToValue(1)); } else { mIsStashed = true; animatorSet.playTogether(mBubbleStashedHandleAlpha.animateToValue(1)); } animatorSet.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { onIsStashedChanged(); } }); animatorSet.setDuration(BAR_STASH_DURATION).start(); } /** * Called when launcher enters or exits the home page. Bubbles are unstashed on home. */ public void setBubblesShowingOnHome(boolean onHome) { if (mBubblesShowingOnHome != onHome) { mBubblesShowingOnHome = onHome; if (!mBarViewController.hasBubbles()) { // if there are no bubbles, there's nothing to show, so just return. return; } if (mBubblesShowingOnHome) { showBubbleBar(/* expanded= */ false); // When transitioning from app to home the stash animator may already have been // created, so we need to animate the bubble bar here to align with hotseat. if (!mIsStashed) { mIconTranslationYForStash.animateToValue(getBubbleBarTranslationYForHotseat()) .start(); } // If the bubble bar is already unstashed, the taskbar touchable region won't be // updated correctly, so force an update here. mControllers.runAfterInit(() -> mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged()); } else if (!mBarViewController.isExpanded()) { stashBubbleBar(); } } } /** Whether bubbles are showing on the launcher home page. */ public boolean isBubblesShowingOnHome() { return mBubblesShowingOnHome; } // TODO: when tapping on an app in overview, this is a bit delayed compared to taskbar stashing /** Called when launcher enters or exits overview. Bubbles are unstashed on overview. */ public void setBubblesShowingOnOverview(boolean onOverview) { if (mBubblesShowingOnOverview != onOverview) { mBubblesShowingOnOverview = onOverview; if (!mBubblesShowingOnOverview && !mBarViewController.isExpanded()) { stashBubbleBar(); } else { // When transitioning to overview the stash animator may already have been // created, so we need to animate the bubble bar here to align with taskbar. mIconTranslationYForStash.animateToValue(getBubbleBarTranslationYForTaskbar()) .start(); } } } /** Whether bubbles are showing on Overview. */ public boolean isBubblesShowingOnOverview() { return mBubblesShowingOnOverview; } /** Called when sysui locked state changes, when locked, bubble bar is stashed. */ public void onSysuiLockedStateChange(boolean isSysuiLocked) { if (isSysuiLocked != mIsSysuiLocked) { mIsSysuiLocked = isSysuiLocked; if (!mIsSysuiLocked && mBarViewController.hasBubbles()) { animateAfterUnlock(); } } } /** * Stashes the bubble bar if allowed based on other state (e.g. on home and overview the * bar does not stash). */ public void stashBubbleBar() { mRequestedStashState = true; mRequestedExpandedState = false; updateStashedAndExpandedState(); } /** * Shows the bubble bar, and expands bubbles depending on {@param expandBubbles}. */ public void showBubbleBar(boolean expandBubbles) { mRequestedStashState = false; mRequestedExpandedState = expandBubbles; updateStashedAndExpandedState(); } private void updateStashedAndExpandedState() { if (mBarViewController.isHiddenForNoBubbles()) { // If there are no bubbles the bar and handle are invisible, nothing to do here. return; } boolean isStashed = mRequestedStashState && !mBubblesShowingOnHome && !mBubblesShowingOnOverview; if (mIsStashed != isStashed) { // notify the view controller that the stash state is about to change so that it can // cancel an ongoing animation if there is one. // note that this has to be called before updating mIsStashed with the new value, // otherwise interrupting an ongoing animation may update it again with the wrong state mBarViewController.onStashStateChanging(); mIsStashed = isStashed; if (mAnimator != null) { mAnimator.cancel(); } mAnimator = createStashAnimator(mIsStashed, BAR_STASH_DURATION); mAnimator.start(); onIsStashedChanged(); } if (mBarViewController.isExpanded() != mRequestedExpandedState) { mBarViewController.setExpanded(mRequestedExpandedState); } } /** * Create a stash animation. * * @param isStashed whether it's a stash animation or an unstash animation * @param duration duration of the animation * @return the animation */ private AnimatorSet createStashAnimator(boolean isStashed, long duration) { AnimatorSet animatorSet = new AnimatorSet(); AnimatorSet fullLengthAnimatorSet = new AnimatorSet(); // Not exactly half and may overlap. See [first|second]HalfDurationScale below. AnimatorSet firstHalfAnimatorSet = new AnimatorSet(); AnimatorSet secondHalfAnimatorSet = new AnimatorSet(); final float firstHalfDurationScale; final float secondHalfDurationScale; if (isStashed) { firstHalfDurationScale = 0.75f; secondHalfDurationScale = 0.5f; fullLengthAnimatorSet.play( mIconTranslationYForStash.animateToValue(getStashTranslation())); firstHalfAnimatorSet.playTogether( mIconAlphaForStash.animateToValue(0), mIconScaleForStash.animateToValue(STASHED_BAR_SCALE)); secondHalfAnimatorSet.playTogether( mBubbleStashedHandleAlpha.animateToValue(1)); } else { firstHalfDurationScale = 0.5f; secondHalfDurationScale = 0.75f; final float translationY = getBubbleBarTranslationY(); fullLengthAnimatorSet.playTogether( mIconScaleForStash.animateToValue(1), mIconTranslationYForStash.animateToValue(translationY)); firstHalfAnimatorSet.playTogether( mBubbleStashedHandleAlpha.animateToValue(0) ); secondHalfAnimatorSet.playTogether( mIconAlphaForStash.animateToValue(1) ); } fullLengthAnimatorSet.play(mHandleViewController.createRevealAnimToIsStashed(isStashed)); fullLengthAnimatorSet.setDuration(duration); firstHalfAnimatorSet.setDuration((long) (duration * firstHalfDurationScale)); secondHalfAnimatorSet.setDuration((long) (duration * secondHalfDurationScale)); secondHalfAnimatorSet.setStartDelay((long) (duration * (1 - secondHalfDurationScale))); animatorSet.playTogether(fullLengthAnimatorSet, firstHalfAnimatorSet, secondHalfAnimatorSet); animatorSet.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mAnimator = null; mControllers.runAfterInit(() -> { if (isStashed) { mBarViewController.setExpanded(false); } mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged(); }); } }); return animatorSet; } private float getStashTranslation() { return (mUnstashedHeight - mStashedHeight) / 2f; } private void onIsStashedChanged() { mControllers.runAfterInit(() -> { mHandleViewController.onIsStashedChanged(); mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged(); }); } public float getBubbleBarTranslationYForTaskbar() { return -mActivity.getDeviceProfile().taskbarBottomMargin; } private float getBubbleBarTranslationYForHotseat() { final float hotseatBottomSpace = mActivity.getDeviceProfile().hotseatBarBottomSpacePx; final float hotseatCellHeight = mActivity.getDeviceProfile().hotseatCellHeightPx; return -hotseatBottomSpace - hotseatCellHeight + mUnstashedHeight - abs( hotseatCellHeight - mUnstashedHeight) / 2; } public float getBubbleBarTranslationY() { // If we're on home, adjust the translation so the bubble bar aligns with hotseat. // Otherwise we're either showing in an app or in overview. In either case adjust it so // the bubble bar aligns with the taskbar. return mBubblesShowingOnHome ? getBubbleBarTranslationYForHotseat() : getBubbleBarTranslationYForTaskbar(); } /** * The difference on the Y axis between the center of the handle and the center of the bubble * bar. */ public float getDiffBetweenHandleAndBarCenters() { // the difference between the centers of the handle and the bubble bar is the difference // between their distance from the bottom of the screen. float barCenter = mBarViewController.getBubbleBarCollapsedHeight() / 2f; return mHandleCenterFromScreenBottom - barCenter; } /** The distance the handle moves as part of the new bubble animation. */ public float getStashedHandleTranslationForNewBubbleAnimation() { // the should move up to the top of the stashed taskbar area. it is centered within it so // it should move the same distance as it is away from the bottom. return -mHandleCenterFromScreenBottom; } /** Checks whether the motion event is over the stash handle. */ public boolean isEventOverStashHandle(MotionEvent ev) { return mHandleViewController.isEventOverHandle(ev); } /** Set a bubble bar location */ public void setBubbleBarLocation(BubbleBarLocation bubbleBarLocation) { mHandleViewController.setBubbleBarLocation(bubbleBarLocation); } /** Returns the [PhysicsAnimator] for the stashed handle view. */ public PhysicsAnimator getStashedHandlePhysicsAnimator() { return mHandleViewController.getPhysicsAnimator(); } /** Notifies taskbar that it should update its touchable region. */ public void updateTaskbarTouchRegion() { mTaskbarInsetsController.onTaskbarOrBubblebarWindowHeightOrInsetsChanged(); } /** Shows the bubble bar immediately without animation. */ public void showBubbleBarImmediate() { mHandleViewController.setTranslationYForSwipe(0); mIconTranslationYForStash.updateValue(getBubbleBarTranslationY()); mIconAlphaForStash.setValue(1); mIconScaleForStash.updateValue(1); mIsStashed = false; onIsStashedChanged(); } /** Stashes the bubble bar immediately without animation. */ public void stashBubbleBarImmediate() { mHandleViewController.setTranslationYForSwipe(0); mBubbleStashedHandleAlpha.setValue(1); mIconAlphaForStash.setValue(0); mIconTranslationYForStash.updateValue(getStashTranslation()); mIconScaleForStash.updateValue(STASHED_BAR_SCALE); mIsStashed = true; onIsStashedChanged(); } /** * Updates the values of the internal animators after the new bubble animation was interrupted * * @param isStashed whether the current state should be stashed * @param bubbleBarTranslationY the current bubble bar translation. this is only used if the * bubble bar is showing to ensure that the stash animator runs * smoothly. */ public void onNewBubbleAnimationInterrupted(boolean isStashed, float bubbleBarTranslationY) { if (isStashed) { mBubbleStashedHandleAlpha.setValue(1); mIconAlphaForStash.setValue(0); mIconScaleForStash.updateValue(STASHED_BAR_SCALE); mIconTranslationYForStash.updateValue(getStashTranslation()); } else { mBubbleStashedHandleAlpha.setValue(0); mHandleViewController.setTranslationYForSwipe(0); mIconAlphaForStash.setValue(1); mIconScaleForStash.updateValue(1); mIconTranslationYForStash.updateValue(bubbleBarTranslationY); } mIsStashed = isStashed; onIsStashedChanged(); } /** Set the translation Y for the stashed handle. */ public void setHandleTranslationY(float ty) { mHandleViewController.setTranslationYForSwipe(ty); } }