diff options
Diffstat (limited to 'quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java')
-rw-r--r-- | quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java | 592 |
1 files changed, 592 insertions, 0 deletions
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java new file mode 100644 index 0000000000..7f7585000c --- /dev/null +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java @@ -0,0 +1,592 @@ +/* + * Copyright (C) 2021 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; + +import static android.view.HapticFeedbackConstants.LONG_PRESS; + +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_LONGPRESS_HIDE; +import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_LONGPRESS_SHOW; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IME_SHOWING; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.annotation.Nullable; +import android.content.SharedPreferences; +import android.content.res.Resources; +import android.view.ViewConfiguration; + +import com.android.launcher3.R; +import com.android.launcher3.Utilities; +import com.android.launcher3.util.MultiValueAlpha.AlphaProperty; +import com.android.quickstep.AnimatedFloat; +import com.android.quickstep.SystemUiProxy; + +import java.util.function.IntPredicate; + +/** + * Coordinates between controllers such as TaskbarViewController and StashedHandleViewController to + * create a cohesive animation between stashed/unstashed states. + */ +public class TaskbarStashController { + + public static final int FLAG_IN_APP = 1 << 0; + public static final int FLAG_STASHED_IN_APP_MANUAL = 1 << 1; // long press, persisted + public static final int FLAG_STASHED_IN_APP_PINNED = 1 << 2; // app pinning + public static final int FLAG_STASHED_IN_APP_EMPTY = 1 << 3; // no hotseat icons + public static final int FLAG_STASHED_IN_APP_SETUP = 1 << 4; // setup wizard and AllSetActivity + public static final int FLAG_STASHED_IN_APP_IME = 1 << 5; // IME is visible + public static final int FLAG_IN_STASHED_LAUNCHER_STATE = 1 << 6; + + // If we're in an app and any of these flags are enabled, taskbar should be stashed. + private static final int FLAGS_STASHED_IN_APP = FLAG_STASHED_IN_APP_MANUAL + | FLAG_STASHED_IN_APP_PINNED | FLAG_STASHED_IN_APP_EMPTY | FLAG_STASHED_IN_APP_SETUP + | FLAG_STASHED_IN_APP_IME; + + // If any of these flags are enabled, inset apps by our stashed height instead of our unstashed + // height. This way the reported insets are consistent even during transitions out of the app. + // Currently any flag that causes us to stash in an app is included, except for IME since that + // covers the underlying app anyway and thus the app shouldn't change insets. + private static final int FLAGS_REPORT_STASHED_INSETS_TO_APP = FLAGS_STASHED_IN_APP + & ~FLAG_STASHED_IN_APP_IME; + + /** + * How long to stash/unstash when manually invoked via long press. + */ + public static final long TASKBAR_STASH_DURATION = 300; + + /** + * How long to stash/unstash when keyboard is appearing/disappearing. + */ + private static final long TASKBAR_STASH_DURATION_FOR_IME = 80; + + /** + * The scale TaskbarView animates to when being stashed. + */ + private static final float STASHED_TASKBAR_SCALE = 0.5f; + + /** + * How long the hint animation plays, starting on motion down. + */ + private static final long TASKBAR_HINT_STASH_DURATION = + ViewConfiguration.DEFAULT_LONG_PRESS_TIMEOUT; + + /** + * The scale that TaskbarView animates to when hinting towards the stashed state. + */ + private static final float STASHED_TASKBAR_HINT_SCALE = 0.9f; + + /** + * The scale that the stashed handle animates to when hinting towards the unstashed state. + */ + private static final float UNSTASHED_TASKBAR_HANDLE_HINT_SCALE = 1.1f; + + /** + * The SharedPreferences key for whether user has manually stashed the taskbar. + */ + private static final String SHARED_PREFS_STASHED_KEY = "taskbar_is_stashed"; + + /** + * Whether taskbar should be stashed out of the box. + */ + private static final boolean DEFAULT_STASHED_PREF = false; + + private final TaskbarActivityContext mActivity; + private final SharedPreferences mPrefs; + private final int mStashedHeight; + private final int mUnstashedHeight; + private final SystemUiProxy mSystemUiProxy; + + // Initialized in init. + private TaskbarControllers mControllers; + // Taskbar background properties. + private AnimatedFloat mTaskbarBackgroundOffset; + private AnimatedFloat mTaskbarImeBgAlpha; + // TaskbarView icon properties. + private AlphaProperty mIconAlphaForStash; + private AnimatedFloat mIconScaleForStash; + private AnimatedFloat mIconTranslationYForStash; + // Stashed handle properties. + private AlphaProperty mTaskbarStashedHandleAlpha; + private AnimatedFloat mTaskbarStashedHandleHintScale; + + /** Whether we are currently visually stashed (might change based on launcher state). */ + private boolean mIsStashed = false; + private int mState; + + private @Nullable AnimatorSet mAnimator; + private boolean mIsSystemGestureInProgress; + private boolean mIsImeShowing; + + // Evaluate whether the handle should be stashed + private final StatePropertyHolder mStatePropertyHolder = new StatePropertyHolder( + flags -> { + boolean inApp = hasAnyFlag(flags, FLAG_IN_APP); + boolean stashedInApp = hasAnyFlag(flags, FLAGS_STASHED_IN_APP); + boolean stashedLauncherState = hasAnyFlag(flags, FLAG_IN_STASHED_LAUNCHER_STATE); + return (inApp && stashedInApp) || (!inApp && stashedLauncherState); + }); + + public TaskbarStashController(TaskbarActivityContext activity) { + mActivity = activity; + mPrefs = Utilities.getPrefs(mActivity); + final Resources resources = mActivity.getResources(); + mStashedHeight = resources.getDimensionPixelSize(R.dimen.taskbar_stashed_size); + mSystemUiProxy = SystemUiProxy.INSTANCE.get(activity); + mUnstashedHeight = mActivity.getDeviceProfile().taskbarSize; + } + + public void init(TaskbarControllers controllers, TaskbarSharedState sharedState) { + mControllers = controllers; + + TaskbarDragLayerController dragLayerController = controllers.taskbarDragLayerController; + mTaskbarBackgroundOffset = dragLayerController.getTaskbarBackgroundOffset(); + mTaskbarImeBgAlpha = dragLayerController.getImeBgTaskbar(); + + TaskbarViewController taskbarViewController = controllers.taskbarViewController; + mIconAlphaForStash = taskbarViewController.getTaskbarIconAlpha().getProperty( + TaskbarViewController.ALPHA_INDEX_STASH); + mIconScaleForStash = taskbarViewController.getTaskbarIconScaleForStash(); + mIconTranslationYForStash = taskbarViewController.getTaskbarIconTranslationYForStash(); + + StashedHandleViewController stashedHandleController = + controllers.stashedHandleViewController; + mTaskbarStashedHandleAlpha = stashedHandleController.getStashedHandleAlpha().getProperty( + StashedHandleViewController.ALPHA_INDEX_STASHED); + mTaskbarStashedHandleHintScale = stashedHandleController.getStashedHandleHintScale(); + + boolean isManuallyStashedInApp = supportsManualStashing() + && mPrefs.getBoolean(SHARED_PREFS_STASHED_KEY, DEFAULT_STASHED_PREF); + boolean isInSetup = !mActivity.isUserSetupComplete() || sharedState.setupUIVisible; + updateStateForFlag(FLAG_STASHED_IN_APP_MANUAL, isManuallyStashedInApp); + // TODO(b/204384193): Temporarily disable SUW specific logic + // updateStateForFlag(FLAG_STASHED_IN_APP_SETUP, isInSetup); + if (isInSetup) { + // Update the in-app state to ensure isStashed() reflects right state during SUW + updateStateForFlag(FLAG_IN_APP, true); + } + applyState(); + + notifyStashChange(/* visible */ false, /* stashed */ isStashedInApp()); + } + + /** + * Returns whether the taskbar can visually stash into a handle based on the current device + * state. + */ + private boolean supportsVisualStashing() { + return !mActivity.isThreeButtonNav(); + } + + /** + * Returns whether the user can manually stash the taskbar based on the current device state. + */ + private boolean supportsManualStashing() { + return supportsVisualStashing() + && (!Utilities.IS_RUNNING_IN_TEST_HARNESS || supportsStashingForTests()); + } + + private boolean supportsStashingForTests() { + // TODO: enable this for tests that specifically check stash/unstash behavior. + return false; + } + + /** + * Sets the flag indicating setup UI is visible + */ + protected void setSetupUIVisible(boolean isVisible) { + updateStateForFlag(FLAG_STASHED_IN_APP_SETUP, + isVisible || !mActivity.isUserSetupComplete()); + applyState(); + } + + /** + * Returns whether the taskbar is currently visually stashed. + */ + public boolean isStashed() { + return mIsStashed; + } + + /** + * Returns whether the taskbar should be stashed in apps (e.g. user long pressed to stash). + */ + public boolean isStashedInApp() { + return hasAnyFlag(FLAGS_STASHED_IN_APP); + } + + /** + * Returns whether the taskbar should be stashed in the current LauncherState. + */ + public boolean isInStashedLauncherState() { + return hasAnyFlag(FLAG_IN_STASHED_LAUNCHER_STATE) && supportsVisualStashing(); + } + + private boolean hasAnyFlag(int flagMask) { + return hasAnyFlag(mState, flagMask); + } + + private boolean hasAnyFlag(int flags, int flagMask) { + return (flags & flagMask) != 0; + } + + + /** + * Returns whether the taskbar is currently visible and in an app. + */ + public boolean isInAppAndNotStashed() { + return !mIsStashed && (mState & FLAG_IN_APP) != 0; + } + + /** + * Returns the height that taskbar will inset when inside apps. + */ + public int getContentHeightToReportToApps() { + if (hasAnyFlag(FLAGS_REPORT_STASHED_INSETS_TO_APP)) { + boolean isAnimating = mAnimator != null && mAnimator.isStarted(); + return mControllers.stashedHandleViewController.isStashedHandleVisible() || isAnimating + ? mStashedHeight : 0; + } + return mUnstashedHeight; + } + + public int getStashedHeight() { + return mStashedHeight; + } + + /** + * Should be called when long pressing the nav region when taskbar is present. + * @return Whether taskbar was stashed and now is unstashed. + */ + public boolean onLongPressToUnstashTaskbar() { + if (!isStashed()) { + // We only listen for long press on the nav region to unstash the taskbar. To stash the + // taskbar, we use an OnLongClickListener on TaskbarView instead. + return false; + } + if (updateAndAnimateIsManuallyStashedInApp(false)) { + mControllers.taskbarActivityContext.getDragLayer().performHapticFeedback(LONG_PRESS); + return true; + } + return false; + } + + /** + * Updates whether we should stash the taskbar when in apps, and animates to the changed state. + * @return Whether we started an animation to either be newly stashed or unstashed. + */ + public boolean updateAndAnimateIsManuallyStashedInApp(boolean isManuallyStashedInApp) { + if (!supportsManualStashing()) { + return false; + } + if (hasAnyFlag(FLAG_STASHED_IN_APP_MANUAL) != isManuallyStashedInApp) { + mPrefs.edit().putBoolean(SHARED_PREFS_STASHED_KEY, isManuallyStashedInApp).apply(); + updateStateForFlag(FLAG_STASHED_IN_APP_MANUAL, isManuallyStashedInApp); + applyState(); + return true; + } + return false; + } + + /** + * Create a stash animation and save to {@link #mAnimator}. + * @param isStashed whether it's a stash animation or an unstash animation + * @param duration duration of the animation + * @param startDelay how many milliseconds to delay the animation after starting it. + */ + private void createAnimToIsStashed(boolean isStashed, long duration, long startDelay) { + if (mAnimator != null) { + mAnimator.cancel(); + } + mAnimator = new AnimatorSet(); + + if (!supportsVisualStashing()) { + // Just hide/show the icons and background instead of stashing into a handle. + mAnimator.play(mIconAlphaForStash.animateToValue(isStashed ? 0 : 1) + .setDuration(duration)); + mAnimator.play(mTaskbarImeBgAlpha.animateToValue( + hasAnyFlag(FLAG_STASHED_IN_APP_IME) ? 0 : 1).setDuration(duration)); + mAnimator.setStartDelay(startDelay); + mAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mAnimator = null; + } + }); + return; + } + + 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; + final float stashTranslation = (mUnstashedHeight - mStashedHeight) / 2f; + + fullLengthAnimatorSet.playTogether( + mTaskbarBackgroundOffset.animateToValue(1), + mIconTranslationYForStash.animateToValue(stashTranslation) + ); + firstHalfAnimatorSet.playTogether( + mIconAlphaForStash.animateToValue(0), + mIconScaleForStash.animateToValue(STASHED_TASKBAR_SCALE) + ); + secondHalfAnimatorSet.playTogether( + mTaskbarStashedHandleAlpha.animateToValue(1) + ); + } else { + firstHalfDurationScale = 0.5f; + secondHalfDurationScale = 0.75f; + + fullLengthAnimatorSet.playTogether( + mTaskbarBackgroundOffset.animateToValue(0), + mIconScaleForStash.animateToValue(1), + mIconTranslationYForStash.animateToValue(0) + ); + firstHalfAnimatorSet.playTogether( + mTaskbarStashedHandleAlpha.animateToValue(0) + ); + secondHalfAnimatorSet.playTogether( + mIconAlphaForStash.animateToValue(1) + ); + } + + fullLengthAnimatorSet.play(mControllers.stashedHandleViewController + .createRevealAnimToIsStashed(isStashed)); + // Return the stashed handle to its default scale in case it was changed as part of the + // feedforward hint. Note that the reveal animation above also visually scales it. + fullLengthAnimatorSet.play(mTaskbarStashedHandleHintScale.animateToValue(1f)); + + fullLengthAnimatorSet.setDuration(duration); + firstHalfAnimatorSet.setDuration((long) (duration * firstHalfDurationScale)); + secondHalfAnimatorSet.setDuration((long) (duration * secondHalfDurationScale)); + secondHalfAnimatorSet.setStartDelay((long) (duration * (1 - secondHalfDurationScale))); + + mAnimator.playTogether(fullLengthAnimatorSet, firstHalfAnimatorSet, + secondHalfAnimatorSet); + mAnimator.setStartDelay(startDelay); + mAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + mIsStashed = isStashed; + onIsStashed(mIsStashed); + } + + @Override + public void onAnimationEnd(Animator animation) { + mAnimator = null; + } + }); + } + + /** + * Creates and starts a partial stash animation, hinting at the new state that will trigger when + * long press is detected. + * @param animateForward Whether we are going towards the new stashed state or returning to the + * unstashed state. + */ + public void startStashHint(boolean animateForward) { + if (isStashed() || !supportsManualStashing()) { + // Already stashed, no need to hint in that direction. + return; + } + mIconScaleForStash.animateToValue( + animateForward ? STASHED_TASKBAR_HINT_SCALE : 1) + .setDuration(TASKBAR_HINT_STASH_DURATION).start(); + } + + /** + * Creates and starts a partial unstash animation, hinting at the new state that will trigger + * when long press is detected. + * @param animateForward Whether we are going towards the new unstashed state or returning to + * the stashed state. + */ + public void startUnstashHint(boolean animateForward) { + if (!isStashed()) { + // Already unstashed, no need to hint in that direction. + return; + } + mTaskbarStashedHandleHintScale.animateToValue( + animateForward ? UNSTASHED_TASKBAR_HANDLE_HINT_SCALE : 1) + .setDuration(TASKBAR_HINT_STASH_DURATION).start(); + } + + private void onIsStashed(boolean isStashed) { + mControllers.stashedHandleViewController.onIsStashed(isStashed); + } + + public void applyState() { + applyState(TASKBAR_STASH_DURATION); + } + + public void applyState(long duration) { + mStatePropertyHolder.setState(mState, duration, true); + } + + public void applyState(long duration, long startDelay) { + mStatePropertyHolder.setState(mState, duration, startDelay, true); + } + + public Animator applyStateWithoutStart() { + return applyStateWithoutStart(TASKBAR_STASH_DURATION); + } + + public Animator applyStateWithoutStart(long duration) { + return mStatePropertyHolder.setState(mState, duration, false); + } + + /** + * Should be called when a system gesture starts and settles, so we can defer updating + * FLAG_STASHED_IN_APP_IME until after the gesture transition completes. + */ + public void setSystemGestureInProgress(boolean inProgress) { + mIsSystemGestureInProgress = inProgress; + // Only update FLAG_STASHED_IN_APP_IME when system gesture is not in progress. + if (!mIsSystemGestureInProgress) { + updateStateForFlag(FLAG_STASHED_IN_APP_IME, mIsImeShowing); + applyState(TASKBAR_STASH_DURATION_FOR_IME, getTaskbarStashStartDelayForIme()); + } + } + + /** + * When hiding the IME, delay the unstash animation to align with the end of the transition. + */ + private long getTaskbarStashStartDelayForIme() { + if (mIsImeShowing) { + // Only delay when IME is exiting, not entering. + return 0; + } + // This duration is based on input_method_extract_exit.xml. + long imeExitDuration = mControllers.taskbarActivityContext.getResources() + .getInteger(android.R.integer.config_shortAnimTime); + return imeExitDuration - TASKBAR_STASH_DURATION_FOR_IME; + } + + /** Called when some system ui state has changed. (See SYSUI_STATE_... in QuickstepContract) */ + public void updateStateForSysuiFlags(int systemUiStateFlags, boolean skipAnim) { + long animDuration = TASKBAR_STASH_DURATION; + long startDelay = 0; + + updateStateForFlag(FLAG_STASHED_IN_APP_PINNED, + hasAnyFlag(systemUiStateFlags, SYSUI_STATE_SCREEN_PINNING)); + + // Only update FLAG_STASHED_IN_APP_IME when system gesture is not in progress. + mIsImeShowing = hasAnyFlag(systemUiStateFlags, SYSUI_STATE_IME_SHOWING); + if (!mIsSystemGestureInProgress) { + updateStateForFlag(FLAG_STASHED_IN_APP_IME, mIsImeShowing); + animDuration = TASKBAR_STASH_DURATION_FOR_IME; + startDelay = getTaskbarStashStartDelayForIme(); + } + + applyState(skipAnim ? 0 : animDuration, skipAnim ? 0 : startDelay); + } + + /** + * Updates the proper flag to indicate whether the task bar should be stashed. + * + * Note that this only updates the flag. {@link #applyState()} needs to be called separately. + * + * @param flag The flag to update. + * @param enabled Whether to enable the flag: True will cause the task bar to be stashed / + * unstashed. + */ + public void updateStateForFlag(int flag, boolean enabled) { + if (enabled) { + mState |= flag; + } else { + mState &= ~flag; + } + } + + /** + * Called after updateStateForFlag() and applyState() have been called. + * @param changedFlags The flags that have changed. + */ + private void onStateChangeApplied(int changedFlags) { + if (hasAnyFlag(changedFlags, FLAGS_STASHED_IN_APP)) { + mControllers.uiController.onStashedInAppChanged(); + } + if (hasAnyFlag(changedFlags, FLAGS_STASHED_IN_APP | FLAG_IN_APP)) { + notifyStashChange(/* visible */ hasAnyFlag(FLAG_IN_APP), + /* stashed */ isStashedInApp()); + } + if (hasAnyFlag(changedFlags, FLAG_STASHED_IN_APP_MANUAL)) { + if (hasAnyFlag(FLAG_STASHED_IN_APP_MANUAL)) { + mActivity.getStatsLogManager().logger().log(LAUNCHER_TASKBAR_LONGPRESS_HIDE); + } else { + mActivity.getStatsLogManager().logger().log(LAUNCHER_TASKBAR_LONGPRESS_SHOW); + } + } + } + + private void notifyStashChange(boolean visible, boolean stashed) { + mSystemUiProxy.notifyTaskbarStatus(visible, stashed); + mControllers.rotationButtonController.onTaskbarStateChange(visible, stashed); + } + + private class StatePropertyHolder { + private final IntPredicate mStashCondition; + + private boolean mIsStashed; + private int mPrevFlags; + + StatePropertyHolder(IntPredicate stashCondition) { + mStashCondition = stashCondition; + } + + /** + * @see #setState(int, long, long, boolean) with a default startDelay = 0. + */ + public Animator setState(int flags, long duration, boolean start) { + return setState(flags, duration, 0 /* startDelay */, start); + } + + /** + * Applies the latest state, potentially calling onStateChangeApplied() and creating a new + * animation (stored in mAnimator) which is started if {@param start} is true. + * @param flags The latest flags to apply (see the top of this file). + * @param duration The length of the animation. + * @param startDelay How long to delay the animation after calling start(). + * @param start Whether to start mAnimator immediately. + * @return mAnimator if mIsStashed changed, else null. + */ + public Animator setState(int flags, long duration, long startDelay, boolean start) { + int changedFlags = mPrevFlags ^ flags; + if (mPrevFlags != flags) { + onStateChangeApplied(changedFlags); + mPrevFlags = flags; + } + boolean isStashed = mStashCondition.test(flags); + if (mIsStashed != isStashed) { + mIsStashed = isStashed; + + // This sets mAnimator. + createAnimToIsStashed(mIsStashed, duration, startDelay); + if (start) { + mAnimator.start(); + } + return mAnimator; + } + return null; + } + } +} |