/* * 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.animation.LayoutTransition.APPEARING; import static android.animation.LayoutTransition.CHANGE_APPEARING; import static android.animation.LayoutTransition.CHANGE_DISAPPEARING; import static android.animation.LayoutTransition.DISAPPEARING; import static com.android.app.animation.Interpolators.EMPHASIZED; import static com.android.app.animation.Interpolators.FINAL_FRAME; import static com.android.app.animation.Interpolators.LINEAR; import static com.android.launcher3.Flags.enableScalingRevealHomeAnimation; import static com.android.launcher3.Flags.taskbarOverflow; import static com.android.launcher3.Flags.taskbarRecentsLayoutTransition; import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY; import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA; import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X; import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y; import static com.android.launcher3.Utilities.mapRange; import static com.android.launcher3.anim.AnimatedFloat.VALUE; import static com.android.launcher3.anim.AnimatorListeners.forEndCallback; import static com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UNIFICATION; import static com.android.launcher3.config.FeatureFlags.enableTaskbarPinning; import static com.android.launcher3.taskbar.TaskbarPinningController.PINNING_PERSISTENT; import static com.android.launcher3.taskbar.TaskbarPinningController.PINNING_TRANSIENT; import static com.android.launcher3.taskbar.bubbles.BubbleBarView.FADE_IN_ANIM_ALPHA_DURATION_MS; import static com.android.launcher3.taskbar.bubbles.BubbleBarView.FADE_OUT_ANIM_POSITION_DURATION_MS; import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE; import static com.android.launcher3.util.MultiTranslateDelegate.INDEX_NAV_BAR_ANIM; import static com.android.launcher3.util.MultiTranslateDelegate.INDEX_TASKBAR_ALIGNMENT_ANIM; import static com.android.launcher3.util.MultiTranslateDelegate.INDEX_TASKBAR_PINNING_ANIM; import static com.android.launcher3.util.MultiTranslateDelegate.INDEX_TASKBAR_REVEAL_ANIM; import android.animation.Animator; import android.animation.AnimatorSet; import android.animation.LayoutTransition; import android.animation.LayoutTransition.TransitionListener; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.annotation.NonNull; import android.graphics.Rect; import android.util.FloatProperty; import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.animation.Interpolator; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.core.view.OneShotPreDrawListener; import com.android.app.animation.Interpolators; import com.android.launcher3.BubbleTextView; import com.android.launcher3.DeviceProfile; import com.android.launcher3.LauncherAppState; import com.android.launcher3.R; import com.android.launcher3.Reorderable; import com.android.launcher3.Utilities; import com.android.launcher3.anim.AlphaUpdateListener; import com.android.launcher3.anim.AnimatedFloat; import com.android.launcher3.anim.AnimatorPlaybackController; import com.android.launcher3.anim.PendingAnimation; import com.android.launcher3.anim.RevealOutlineAnimation; import com.android.launcher3.anim.RoundedRectRevealOutlineProvider; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.TaskItemInfo; import com.android.launcher3.taskbar.bubbles.BubbleBarController; import com.android.launcher3.util.DisplayController; import com.android.launcher3.util.ItemInfoMatcher; import com.android.launcher3.util.LauncherBindableItemsContainer; import com.android.launcher3.util.MultiPropertyFactory; import com.android.launcher3.util.MultiPropertyFactory.MultiProperty; import com.android.launcher3.util.MultiTranslateDelegate; import com.android.launcher3.util.MultiValueAlpha; import com.android.quickstep.util.GroupTask; import com.android.systemui.shared.recents.model.Task; import com.android.wm.shell.Flags; import com.android.wm.shell.shared.bubbles.BubbleBarLocation; import java.io.PrintWriter; import java.util.Collections; import java.util.HashSet; import java.util.Set; import java.util.function.Predicate; /** * Handles properties/data collection, then passes the results to TaskbarView to render. */ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbarController, BubbleBarController.BubbleBarLocationListener { private static final String TAG = "TaskbarViewController"; private static final Runnable NO_OP = () -> { }; public static final int ALPHA_INDEX_HOME = 0; public static final int ALPHA_INDEX_KEYGUARD = 1; public static final int ALPHA_INDEX_STASH = 2; public static final int ALPHA_INDEX_RECENTS_DISABLED = 3; public static final int ALPHA_INDEX_NOTIFICATION_EXPANDED = 4; public static final int ALPHA_INDEX_ASSISTANT_INVOKED = 5; public static final int ALPHA_INDEX_SMALL_SCREEN = 6; public static final int ALPHA_INDEX_BUBBLE_BAR = 7; private static final int NUM_ALPHA_CHANNELS = 8; /** Only used for animation purposes, to position the divider between two item indices. */ public static final float DIVIDER_VIEW_POSITION_OFFSET = 0.5f; /** Used if an unexpected edge case is hit in {@link #getPositionInHotseat}. */ private static final float ERROR_POSITION_IN_HOTSEAT_NOT_FOUND = -100; private static final int TRANSITION_DELAY = 50; private static final int TRANSITION_DEFAULT_DURATION = 500; private static final int TRANSITION_FADE_IN_DURATION = 167; private static final int TRANSITION_FADE_OUT_DURATION = 83; private static boolean sEnableModelLoadingForTests = true; private final TaskbarActivityContext mActivity; private final TaskbarView mTaskbarView; private final MultiValueAlpha mTaskbarIconAlpha; private final AnimatedFloat mTaskbarIconScaleForStash = new AnimatedFloat(this::updateScale); public final AnimatedFloat mTaskbarIconTranslationYForHome = new AnimatedFloat( this::updateTranslationY); private final AnimatedFloat mTaskbarIconTranslationYForStash = new AnimatedFloat( this::updateTranslationY); private final AnimatedFloat mTaskbarIconScaleForPinning = new AnimatedFloat( this::updateTaskbarIconsScale); private final AnimatedFloat mTaskbarIconTranslationXForPinning = new AnimatedFloat( this::updateTaskbarIconTranslationXForPinning); private final AnimatedFloat mIconsTranslationXForNavbar = new AnimatedFloat( this::updateTranslationXForNavBar); @Nullable private Animator mTaskbarShiftXAnim; @Nullable private BubbleBarLocation mCurrentBubbleBarLocation; private final AnimatedFloat mTaskbarIconTranslationYForPinning = new AnimatedFloat( this::updateTranslationY); private AnimatedFloat mTaskbarNavButtonTranslationY; private AnimatedFloat mTaskbarNavButtonTranslationYForInAppDisplay; private float mTaskbarIconTranslationYForSwipe; private float mTaskbarIconTranslationYForSpringOnStash; private int mTaskbarBottomMargin; private final int mStashedHandleHeight; private final TaskbarModelCallbacks mModelCallbacks; // Initialized in init. private TaskbarControllers mControllers; private final View.OnLayoutChangeListener mTaskbarViewLayoutChangeListener = (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { if (!taskbarRecentsLayoutTransition()) { updateTaskbarIconTranslationXForPinning(); } if (BubbleBarController.isBubbleBarEnabled()) { mControllers.navbarButtonsViewController.onLayoutsUpdated(); } }; // Animation to align icons with Launcher, created lazily. This allows the controller to be // active only during the animation and does not need to worry about layout changes. private AnimatorPlaybackController mIconAlignControllerLazy = null; private Runnable mOnControllerPreCreateCallback = NO_OP; // Stored here as signals to determine if the mIconAlignController needs to be recreated. private boolean mIsIconAlignedWithHotseat; private boolean mIsHotseatIconOnTopWhenAligned; private boolean mIsStashed; private final DeviceProfile.OnDeviceProfileChangeListener mDeviceProfileChangeListener = dp -> commitRunningAppsToUI(); private final boolean mIsRtl; private final DeviceProfile mTransientTaskbarDp; private final DeviceProfile mPersistentTaskbarDp; private final int mTransientIconSize; private final int mPersistentIconSize; private final float mTaskbarLeftRightMargin; public TaskbarViewController(TaskbarActivityContext activity, TaskbarView taskbarView) { mActivity = activity; mTransientTaskbarDp = mActivity.getTransientTaskbarDeviceProfile(); mPersistentTaskbarDp = mActivity.getPersistentTaskbarDeviceProfile(); mTransientIconSize = mTransientTaskbarDp.taskbarIconSize; mPersistentIconSize = mPersistentTaskbarDp.taskbarIconSize; mTaskbarView = taskbarView; mTaskbarIconAlpha = new MultiValueAlpha(mTaskbarView, NUM_ALPHA_CHANNELS); mTaskbarIconAlpha.setUpdateVisibility(true); mModelCallbacks = TaskbarModelCallbacksFactory.newInstance(mActivity) .create(mActivity, mTaskbarView); mTaskbarBottomMargin = activity.getDeviceProfile().taskbarBottomMargin; mStashedHandleHeight = activity.getResources() .getDimensionPixelSize(R.dimen.taskbar_stashed_handle_height); mIsRtl = Utilities.isRtl(mTaskbarView.getResources()); mTaskbarLeftRightMargin = mActivity.getResources().getDimensionPixelSize( R.dimen.transient_taskbar_padding); } public void init(TaskbarControllers controllers) { mControllers = controllers; mTaskbarView.init(TaskbarViewCallbacksFactory.newInstance(mActivity).create( mActivity, mControllers, mTaskbarView)); mTaskbarView.getLayoutParams().height = mActivity.isPhoneMode() ? mActivity.getResources().getDimensionPixelSize(R.dimen.taskbar_phone_size) : mActivity.getDeviceProfile().taskbarHeight; mTaskbarIconScaleForStash.updateValue(1f); float pinningValue = DisplayController.isTransientTaskbar(mActivity) ? PINNING_TRANSIENT : PINNING_PERSISTENT; mTaskbarIconScaleForPinning.updateValue(pinningValue); mTaskbarIconTranslationYForPinning.updateValue(pinningValue); mTaskbarIconTranslationXForPinning.updateValue(pinningValue); mModelCallbacks.init(controllers); if (mActivity.isUserSetupComplete() && sEnableModelLoadingForTests) { // Only load the callbacks if user setup is completed controllers.runAfterInit(() -> LauncherAppState.getInstance(mActivity).getModel() .addCallbacksAndLoad(mModelCallbacks)); } mTaskbarNavButtonTranslationY = controllers.navbarButtonsViewController.getTaskbarNavButtonTranslationY(); mTaskbarNavButtonTranslationYForInAppDisplay = controllers.navbarButtonsViewController .getTaskbarNavButtonTranslationYForInAppDisplay(); mActivity.addOnDeviceProfileChangeListener(mDeviceProfileChangeListener); if (ENABLE_TASKBAR_NAVBAR_UNIFICATION) { // This gets modified in NavbarButtonsViewController, but the initial value it reads // may be incorrect since it's state gets destroyed on taskbar recreate, so reset here mTaskbarIconAlpha.get(ALPHA_INDEX_SMALL_SCREEN).setValue( mActivity.isPhoneMode() ? 0 : 1); } if (enableTaskbarPinning()) { mTaskbarView.addOnLayoutChangeListener(mTaskbarViewLayoutChangeListener); } } /** Adjusts start aligned taskbar layout accordingly to the bubble bar position. */ @Override public void onBubbleBarLocationUpdated(BubbleBarLocation location) { updateCurrentBubbleBarLocation(location); if (!shouldMoveTaskbarOnBubbleBarLocationUpdate()) return; cancelTaskbarShiftAnimation(); // reset translation x, taskbar will position icons with the updated location mIconsTranslationXForNavbar.updateValue(0); mTaskbarView.onBubbleBarLocationUpdated(location); } /** Animates start aligned taskbar accordingly to the bubble bar position. */ @Override public void onBubbleBarLocationAnimated(BubbleBarLocation location) { if (!updateCurrentBubbleBarLocation(location) || !shouldMoveTaskbarOnBubbleBarLocationUpdate()) { return; } cancelTaskbarShiftAnimation(); float translationX = mTaskbarView.getTranslationXForBubbleBarPosition(location); mTaskbarShiftXAnim = createTaskbarIconsShiftAnimator(translationX); mTaskbarShiftXAnim.start(); } /** Updates the mCurrentBubbleBarLocation, returns {@code} true if location is updated. */ private boolean updateCurrentBubbleBarLocation(BubbleBarLocation location) { if (mCurrentBubbleBarLocation == location || location == null) { return false; } else { mCurrentBubbleBarLocation = location; return true; } } /** Returns whether taskbar should be moved on the bubble bar location update. */ private boolean shouldMoveTaskbarOnBubbleBarLocationUpdate() { return Flags.enableBubbleBarInPersistentTaskBar() && mControllers.bubbleControllers.isPresent() && mActivity.shouldStartAlignTaskbar() && mActivity.isThreeButtonNav(); } private void cancelTaskbarShiftAnimation() { if (mTaskbarShiftXAnim != null) { mTaskbarShiftXAnim.cancel(); } } /** * Announcement for Accessibility when Taskbar stashes/unstashes. */ public void announceForAccessibility() { mTaskbarView.announceAccessibilityChanges(); } public void onDestroy() { if (enableTaskbarPinning()) { mTaskbarView.removeOnLayoutChangeListener(mTaskbarViewLayoutChangeListener); } LauncherAppState.getInstance(mActivity).getModel().removeCallbacks(mModelCallbacks); mActivity.removeOnDeviceProfileChangeListener(mDeviceProfileChangeListener); } /** * Gets the taskbar {@link View.Visibility visibility}. */ public int getTaskbarVisibility() { return mTaskbarView.getVisibility(); } public boolean areIconsVisible() { return mTaskbarView.areIconsVisible(); } public MultiPropertyFactory getTaskbarIconAlpha() { return mTaskbarIconAlpha; } /** * Should be called when the recents button is disabled, so we can hide Taskbar icons as well. */ public void setRecentsButtonDisabled(boolean isDisabled) { // TODO: check TaskbarStashController#supportsStashing(), to stash instead of setting alpha. mTaskbarIconAlpha.get(ALPHA_INDEX_RECENTS_DISABLED).animateToValue(isDisabled ? 0 : 1) .start(); } /** * Sets OnClickListener and OnLongClickListener for the given view. */ public void setClickAndLongClickListenersForIcon(View icon) { mTaskbarView.setClickAndLongClickListenersForIcon(icon); } /** * Adds one time pre draw listener to the Taskbar view, it is called before * drawing a frame and invoked only once * @param listener callback that will be invoked before drawing the next frame */ public void addOneTimePreDrawListener(@NonNull Runnable listener) { OneShotPreDrawListener.add(mTaskbarView, listener); } @VisibleForTesting int getMaxNumIconViews() { return mTaskbarView.getMaxNumIconViews(); } public Rect getTransientTaskbarIconLayoutBounds() { return mTaskbarView.getTransientTaskbarIconLayoutBounds(); } public Rect getTransientTaskbarIconLayoutBoundsInParent() { return mTaskbarView.getTransientTaskbarIconLayoutBoundsInParent(); } public View[] getIconViews() { return mTaskbarView.getIconViews(); } public View getAllAppsButtonView() { return mTaskbarView.getAllAppsButtonContainer(); } public AnimatedFloat getTaskbarIconScaleForStash() { return mTaskbarIconScaleForStash; } public AnimatedFloat getTaskbarIconTranslationYForStash() { return mTaskbarIconTranslationYForStash; } public AnimatedFloat getTaskbarIconScaleForPinning() { return mTaskbarIconScaleForPinning; } public AnimatedFloat getTaskbarIconTranslationXForPinning() { return mTaskbarIconTranslationXForPinning; } public AnimatedFloat getTaskbarIconTranslationYForPinning() { return mTaskbarIconTranslationYForPinning; } /** * Applies scale properties for the entire TaskbarView (rather than individual icons). */ private void updateScale() { float scale = mTaskbarIconScaleForStash.value; mTaskbarView.setScaleX(scale); mTaskbarView.setScaleY(scale); } /** * Applies scale properties for the taskbar icons */ private void updateTaskbarIconsScale() { float scale = mTaskbarIconScaleForPinning.value; View[] iconViews = mTaskbarView.getIconViews(); float finalScale; if (mControllers.getSharedState().startTaskbarVariantIsTransient) { finalScale = mapRange(scale, 1f, ((float) mPersistentIconSize / mTransientIconSize)); } else { finalScale = mapRange(scale, ((float) mTransientIconSize / mPersistentIconSize), 1f); } for (int iconIndex = 0; iconIndex < iconViews.length; iconIndex++) { iconViews[iconIndex].setScaleX(finalScale); iconViews[iconIndex].setScaleY(finalScale); } } /** * Animate away taskbar icon notification dots during the taskbar pinning animation. */ public void animateAwayNotificationDotsDuringTaskbarPinningAnimation() { for (View iconView : mTaskbarView.getIconViews()) { if (iconView instanceof BubbleTextView && ((BubbleTextView) iconView).hasDot()) { ((BubbleTextView) iconView).animateDotScale(0); } } } void updateTaskbarIconTranslationXForPinning() { View[] iconViews = mTaskbarView.getIconViews(); float scale = mTaskbarIconTranslationXForPinning.value; float transientTaskbarAllAppsOffset = mActivity.getResources().getDimension( mTaskbarView.getAllAppsButtonContainer().getAllAppsButtonTranslationXOffset(true)); float persistentTaskbarAllAppsOffset = mActivity.getResources().getDimension( mTaskbarView.getAllAppsButtonContainer().getAllAppsButtonTranslationXOffset(false)); float allAppIconTranslateRange = mapRange(scale, transientTaskbarAllAppsOffset, persistentTaskbarAllAppsOffset); // Task icons are laid out so the taskbar content is centered. The taskbar width (used for // centering taskbar icons) depends on the all apps button X translation, and is different // for persistent and transient taskbar. If the offset used for current taskbar layout is // different than the offset used in final taskbar state, the icons may jump when the // animation completes, and the taskbar is replaced. Adjust item transform to account for // this mismatch. float sizeDiffTranslationRange = mapRange(scale, (mTaskbarView.getAllAppsButtonTranslationXOffsetUsedForLayout() - transientTaskbarAllAppsOffset) / 2, (mTaskbarView.getAllAppsButtonTranslationXOffsetUsedForLayout() - persistentTaskbarAllAppsOffset) / 2); // no x translation required when all apps button is the only icon in taskbar. if (iconViews.length <= 1) { allAppIconTranslateRange = 0f; } if (mIsRtl) { allAppIconTranslateRange *= -1; sizeDiffTranslationRange *= -1; } if (mActivity.isThreeButtonNav()) { mTaskbarView.getAllAppsButtonContainer() .setTranslationXForTaskbarAllAppsIcon(allAppIconTranslateRange); return; } float finalMarginScale = mapRange(scale, 0f, mTransientIconSize - mPersistentIconSize); // The index of the "middle" icon which will be used as a index from which the icon margins // will be scaled. If number of icons is even, using the middle point between indices of two // central icons. float middleIndex = (iconViews.length - 1) / 2.0f; for (int iconIndex = 0; iconIndex < iconViews.length; iconIndex++) { View iconView = iconViews[iconIndex]; MultiTranslateDelegate translateDelegate = ((Reorderable) iconView).getTranslateDelegate(); translateDelegate.getTranslationX(INDEX_TASKBAR_PINNING_ANIM).setValue( finalMarginScale * (middleIndex - iconIndex) + sizeDiffTranslationRange); if (iconView.equals(mTaskbarView.getAllAppsButtonContainer())) { mTaskbarView.getAllAppsButtonContainer().setTranslationXForTaskbarAllAppsIcon( allAppIconTranslateRange); } } } /** * Calculates visual taskbar view width. */ public float getCurrentVisualTaskbarWidth() { View[] iconViews = mTaskbarView.getIconViews(); if (iconViews.length == 0) { return 0; } float left = iconViews[0].getX(); int rightIndex = iconViews.length - 1; float right = iconViews[rightIndex].getRight() + iconViews[rightIndex].getTranslationX(); return right - left + (2 * mTaskbarLeftRightMargin); } /** * Sets the translation of the TaskbarView during the swipe up gesture. */ public void setTranslationYForSwipe(float transY) { mTaskbarIconTranslationYForSwipe = transY; updateTranslationY(); } /** * Sets the translation of the TaskbarView during the spring on stash animation. */ public void setTranslationYForStash(float transY) { mTaskbarIconTranslationYForSpringOnStash = transY; updateTranslationY(); } private void updateTranslationY() { mTaskbarView.setTranslationY(mTaskbarIconTranslationYForHome.value + mTaskbarIconTranslationYForStash.value + mTaskbarIconTranslationYForSwipe + getTaskbarIconTranslationYForPinningValue() + mTaskbarIconTranslationYForSpringOnStash); } private void updateTranslationXForNavBar() { View[] iconViews = mTaskbarView.getIconViews(); float translationX = mIconsTranslationXForNavbar.value; for (int iconIndex = 0; iconIndex < iconViews.length; iconIndex++) { View iconView = iconViews[iconIndex]; MultiTranslateDelegate translateDelegate = ((Reorderable) iconView).getTranslateDelegate(); translateDelegate.getTranslationX(INDEX_NAV_BAR_ANIM).setValue(translationX); } } /** * Computes translation y for taskbar pinning. */ private float getTaskbarIconTranslationYForPinningValue() { if (mControllers.getSharedState() == null) return 0f; float scale = mTaskbarIconTranslationYForPinning.value; float taskbarIconTranslationYForPinningValue; // transY is calculated here by adding/subtracting the taskbar bottom margin // aligning the icon bound to be at bottom of current taskbar view and then // finally placing the icon in the middle of new taskbar background height. if (mControllers.getSharedState().startTaskbarVariantIsTransient) { float transY = mTransientTaskbarDp.taskbarBottomMargin + (mTransientTaskbarDp.taskbarHeight - mTaskbarView.getTransientTaskbarIconLayoutBounds().bottom) - (mPersistentTaskbarDp.taskbarHeight - mTransientTaskbarDp.taskbarIconSize) / 2f; taskbarIconTranslationYForPinningValue = mapRange(scale, 0f, transY); } else { float transY = -mTransientTaskbarDp.taskbarBottomMargin + (mPersistentTaskbarDp.taskbarHeight - mTaskbarView.getTransientTaskbarIconLayoutBounds().bottom) - (mTransientTaskbarDp.taskbarHeight - mTransientTaskbarDp.taskbarIconSize) / 2f; taskbarIconTranslationYForPinningValue = mapRange(scale, transY, 0f); } return taskbarIconTranslationYForPinningValue; } private ValueAnimator createRevealAnimForView(View view, boolean isStashed, float newWidth, boolean isQsb, boolean dispatchOnAnimationStart) { Rect viewBounds = new Rect(0, 0, view.getWidth(), view.getHeight()); int centerY = viewBounds.centerY(); int halfHandleHeight = mStashedHandleHeight / 2; final int top = centerY - halfHandleHeight; final int bottom = centerY + halfHandleHeight; final int left; final int right; // QSB will crop from the 'start' whereas all other icons will crop from the center. if (isQsb) { if (mIsRtl) { right = viewBounds.right; left = (int) (right - newWidth); } else { left = viewBounds.left; right = (int) (left + newWidth); } } else { int widthDelta = (int) ((viewBounds.width() - newWidth) / 2); left = viewBounds.left + widthDelta; right = viewBounds.right - widthDelta; } Rect stashedRect = new Rect(left, top, right, bottom); // QSB radius can be > 0 since it does not have any UI elements outside of it bounds. float radius = isQsb ? viewBounds.height() / 2f : 0f; float stashedRadius = stashedRect.height() / 2f; ValueAnimator reveal = new RoundedRectRevealOutlineProvider(radius, stashedRadius, viewBounds, stashedRect) .createRevealAnimator(view, !isStashed, 0); // SUW animation does not dispatch animation start until *after* the animation is complete. // In order to work properly, the reveal animation start needs to be called immediately. if (dispatchOnAnimationStart) { for (Animator.AnimatorListener listener : reveal.getListeners()) { listener.onAnimationStart(reveal); } } return reveal; } public View getTaskbarDividerView() { return mTaskbarView.getTaskbarDividerViewContainer(); } /** * Updates which icons are marked as running or minimized given the Sets of currently running * and minimized tasks. */ public void updateIconViewsRunningStates() { for (View iconView : getIconViews()) { if (iconView instanceof BubbleTextView btv) { btv.updateRunningState(getRunningAppState(btv)); } } } /** * @return A set of Task ids of running apps that are pinned in the taskbar. */ protected Set getTaskIdsForPinnedApps() { if (!taskbarOverflow()) { return Collections.emptySet(); } Set pinnedAppsWithTasks = new HashSet<>(); for (View iconView : getIconViews()) { if (iconView instanceof BubbleTextView btv && btv.getTag() instanceof TaskItemInfo itemInfo) { pinnedAppsWithTasks.add(itemInfo.getTaskId()); } } return pinnedAppsWithTasks; } private BubbleTextView.RunningAppState getRunningAppState(BubbleTextView btv) { Object tag = btv.getTag(); if (tag instanceof TaskItemInfo itemInfo) { return mControllers.taskbarRecentAppsController.getRunningAppState( itemInfo.getTaskId()); } if (tag instanceof GroupTask groupTask && !groupTask.hasMultipleTasks()) { return mControllers.taskbarRecentAppsController.getRunningAppState( groupTask.task1.key.id); } return BubbleTextView.RunningAppState.NOT_RUNNING; } /** * Defers any updates to the UI for the setup wizard animation. */ public void setDeferUpdatesForSUW(boolean defer) { mModelCallbacks.setDeferUpdatesForSUW(defer); } /** * Creates and returns a {@link RevealOutlineAnimation} Animator that updates the icon shape * and size. * @param as The AnimatorSet to add all animations to. * @param isStashed When true, the icon crops vertically to the size of the stashed handle. * When false, the reverse happens. * @param duration The duration of the animation. * @param interpolator The interpolator to use for all animations. */ public void addRevealAnimToIsStashed(AnimatorSet as, boolean isStashed, long duration, Interpolator interpolator, boolean dispatchOnAnimationStart) { AnimatorSet reveal = new AnimatorSet(); Rect stashedBounds = new Rect(); mControllers.stashedHandleViewController.getStashedHandleBounds(stashedBounds); int numIcons = mTaskbarView.getChildCount(); float newChildWidth = stashedBounds.width() / (float) numIcons; // All children move the same y-amount since they will be cropped to the same centerY. float croppedTransY = mTaskbarView.getIconTouchSize() - stashedBounds.height(); for (int i = mTaskbarView.getChildCount() - 1; i >= 0; i--) { View child = mTaskbarView.getChildAt(i); boolean isQsb = child == mTaskbarView.getQsb(); // Crop the icons to/from the nav handle shape. reveal.play(createRevealAnimForView(child, isStashed, newChildWidth, isQsb, dispatchOnAnimationStart).setDuration(duration)); // Translate the icons to/from their locations as the "nav handle." // All of the Taskbar icons will overlap the entirety of the stashed handle // And the QSB, if inline, will overlap part of stashed handle as well. float currentPosition = isQsb ? child.getX() : child.getLeft(); float newPosition = stashedBounds.left + (newChildWidth * i); final float croppedTransX; // We look at 'left' and 'right' values to ensure that the children stay within the // bounds of the stashed handle since the new width only occurs at the end of the anim. if (currentPosition > newPosition) { float newRight = stashedBounds.right - (newChildWidth * (numIcons - 1 - i)); croppedTransX = -(currentPosition + child.getWidth() - newRight); } else { croppedTransX = newPosition - currentPosition; } float[] transX = isStashed ? new float[] {croppedTransX} : new float[] {croppedTransX, 0}; float[] transY = isStashed ? new float[] {croppedTransY} : new float[] {croppedTransY, 0}; if (child instanceof Reorderable) { MultiTranslateDelegate mtd = ((Reorderable) child).getTranslateDelegate(); reveal.play(ObjectAnimator.ofFloat(mtd.getTranslationX(INDEX_TASKBAR_REVEAL_ANIM), MULTI_PROPERTY_VALUE, transX) .setDuration(duration)); reveal.play(ObjectAnimator.ofFloat(mtd.getTranslationY(INDEX_TASKBAR_REVEAL_ANIM), MULTI_PROPERTY_VALUE, transY)); as.addListener(forEndCallback(() -> mtd.setTranslation(INDEX_TASKBAR_REVEAL_ANIM, 0, 0))); } else { reveal.play(ObjectAnimator.ofFloat(child, VIEW_TRANSLATE_X, transX) .setDuration(duration)); reveal.play(ObjectAnimator.ofFloat(child, VIEW_TRANSLATE_Y, transY)); as.addListener(forEndCallback(() -> { child.setTranslationX(0); child.setTranslationY(0); })); } } reveal.setInterpolator(interpolator); as.play(reveal); } /** * Sets the Taskbar icon alignment relative to Launcher hotseat icons * @param alignmentRatio [0, 1] * 0 => not aligned * 1 => fully aligned */ public void setLauncherIconAlignment(float alignmentRatio, DeviceProfile launcherDp) { if (mActivity.isPhoneMode()) { mIconAlignControllerLazy = null; return; } boolean isHotseatIconOnTopWhenAligned = mControllers.uiController.isHotseatIconOnTopWhenAligned(); boolean isIconAlignedWithHotseat = mControllers.uiController.isIconAlignedWithHotseat(); boolean isStashed = mControllers.taskbarStashController.isStashed(); // Re-create animation when any of these values change. if (mIconAlignControllerLazy == null || mIsHotseatIconOnTopWhenAligned != isHotseatIconOnTopWhenAligned || mIsIconAlignedWithHotseat != isIconAlignedWithHotseat || mIsStashed != isStashed) { mIsHotseatIconOnTopWhenAligned = isHotseatIconOnTopWhenAligned; mIsIconAlignedWithHotseat = isIconAlignedWithHotseat; mIsStashed = isStashed; mIconAlignControllerLazy = createIconAlignmentController(launcherDp); } mIconAlignControllerLazy.setPlayFraction(alignmentRatio); if (alignmentRatio <= 0 || alignmentRatio >= 1) { // Cleanup lazy controller so that it is created again in next animation mIconAlignControllerLazy = null; } } /** * Resets the icon alignment controller so that it can be recreated again later, and updates * the list of icons shown in the taskbar if the bubble bar visibility changes the taskbar * overflow state. */ void adjustTaskbarForBubbleBar() { mIconAlignControllerLazy = null; if (mTaskbarView.updateMaxNumIcons()) { commitRunningAppsToUI(); } } /** * Creates an animation for aligning the Taskbar icons with the provided Launcher device profile */ private AnimatorPlaybackController createIconAlignmentController(DeviceProfile launcherDp) { PendingAnimation setter = new PendingAnimation(100); // icon alignment not needed for pinned taskbar. if (DisplayController.isPinnedTaskbar(mActivity)) return setter.createPlaybackController(); mOnControllerPreCreateCallback.run(); DeviceProfile taskbarDp = mActivity.getDeviceProfile(); Rect hotseatPadding = launcherDp.getHotseatLayoutPadding(mActivity); boolean isTransientTaskbar = DisplayController.isTransientTaskbar(mActivity); float scaleUp = ((float) launcherDp.iconSizePx) / taskbarDp.taskbarIconSize; int borderSpacing = launcherDp.hotseatBorderSpace; int hotseatCellSize = DeviceProfile.calculateCellWidth( launcherDp.availableWidthPx - hotseatPadding.left - hotseatPadding.right, borderSpacing, launcherDp.numShownHotseatIcons); boolean isToHome = mControllers.uiController.isIconAlignedWithHotseat(); // If Hotseat is not the top element, Taskbar should maintain in-app state as it fades out, // or fade in while already in in-app state. Interpolator interpolator = mIsHotseatIconOnTopWhenAligned ? LINEAR : FINAL_FRAME; int offsetY = launcherDp.getTaskbarOffsetY(); setter.setFloat(mTaskbarIconTranslationYForHome, VALUE, -offsetY, interpolator); setter.setFloat(mTaskbarNavButtonTranslationY, VALUE, -offsetY, interpolator); setter.setFloat(mTaskbarNavButtonTranslationYForInAppDisplay, VALUE, offsetY, interpolator); int collapsedHeight = mActivity.getDefaultTaskbarWindowSize(); int expandedHeight = Math.max(collapsedHeight, taskbarDp.taskbarHeight + offsetY); setter.addOnFrameListener(anim -> mActivity.setTaskbarWindowSize( anim.getAnimatedFraction() > 0 ? expandedHeight : collapsedHeight)); mTaskbarBottomMargin = isTransientTaskbar ? mTransientTaskbarDp.taskbarBottomMargin : mPersistentTaskbarDp.taskbarBottomMargin; int firstRecentTaskIndex = -1; int hotseatNavBarTranslationX = 0; if (mCurrentBubbleBarLocation != null) { boolean isBubblesOnLeft = mCurrentBubbleBarLocation .isOnLeft(mTaskbarView.isLayoutRtl()); hotseatNavBarTranslationX = taskbarDp .getHotseatTranslationXForNavBar(mActivity, isBubblesOnLeft); } for (int i = 0; i < mTaskbarView.getChildCount(); i++) { View child = mTaskbarView.getChildAt(i); boolean isAllAppsButton = child == mTaskbarView.getAllAppsButtonContainer(); boolean isTaskbarDividerView = child == mTaskbarView.getTaskbarDividerViewContainer(); boolean isTaskbarOverflowView = child == mTaskbarView.getTaskbarOverflowView(); boolean isRecentTask = child.getTag() instanceof GroupTask; // TODO(b/343522351): show recents on the home screen. final boolean isRecentsInHotseat = false; if (!mIsHotseatIconOnTopWhenAligned) { // When going to home, the EMPHASIZED interpolator in TaskbarLauncherStateController // plays iconAlignment to 1 really fast, therefore moving the fading towards the end // to avoid icons disappearing rather than fading out visually. setter.setViewAlpha(child, 0, Interpolators.clampToProgress(LINEAR, 0.8f, 1f)); } else if ((isAllAppsButton && !FeatureFlags.enableAllAppsButtonInHotseat()) || (isTaskbarDividerView && enableTaskbarPinning()) || (isRecentTask && !isRecentsInHotseat) || isTaskbarOverflowView) { if (!isToHome && mIsHotseatIconOnTopWhenAligned && mIsStashed) { // Prevent All Apps icon from appearing when going from hotseat to nav handle. setter.setViewAlpha(child, 0, Interpolators.clampToProgress(LINEAR, 0f, 0f)); } else if (enableScalingRevealHomeAnimation()) { // Tighten clamp so that these icons do not linger as the spring settles. setter.setViewAlpha(child, 0, isToHome ? Interpolators.clampToProgress(LINEAR, 0f, 0.07f) : Interpolators.clampToProgress(LINEAR, 0.93f, 1f)); } else { setter.setViewAlpha(child, 0, isToHome ? Interpolators.clampToProgress(LINEAR, 0f, 0.17f) : Interpolators.clampToProgress(LINEAR, 0.72f, 0.84f)); } } if (child == mTaskbarView.getQsb()) { boolean isRtl = Utilities.isRtl(child.getResources()); float hotseatIconCenter = isRtl ? launcherDp.widthPx - hotseatPadding.right + borderSpacing + launcherDp.hotseatQsbWidth / 2f : hotseatPadding.left - borderSpacing - launcherDp.hotseatQsbWidth / 2f; if (taskbarDp.isQsbInline) { hotseatIconCenter += hotseatNavBarTranslationX; } float childCenter = (child.getLeft() + child.getRight()) / 2f; if (child instanceof Reorderable reorderableChild) { childCenter += reorderableChild.getTranslateDelegate().getTranslationX( INDEX_TASKBAR_PINNING_ANIM).getValue(); } float halfQsbIconWidthDiff = (launcherDp.hotseatQsbWidth - taskbarDp.taskbarIconSize) / 2f; float scale = ((float) taskbarDp.taskbarIconSize) / launcherDp.hotseatQsbVisualHeight; setter.addFloat(child, SCALE_PROPERTY, scale, 1f, interpolator); float fromX = isRtl ? -halfQsbIconWidthDiff : halfQsbIconWidthDiff; float toX = hotseatIconCenter - childCenter; if (child instanceof Reorderable reorderableChild) { MultiTranslateDelegate mtd = reorderableChild.getTranslateDelegate(); setter.addFloat(mtd.getTranslationX(INDEX_TASKBAR_ALIGNMENT_ANIM), MULTI_PROPERTY_VALUE, fromX, toX, interpolator); setter.setFloat(mtd.getTranslationY(INDEX_TASKBAR_ALIGNMENT_ANIM), MULTI_PROPERTY_VALUE, mTaskbarBottomMargin, interpolator); } else { setter.addFloat(child, VIEW_TRANSLATE_X, fromX, toX, interpolator); setter.setFloat(child, VIEW_TRANSLATE_Y, mTaskbarBottomMargin, interpolator); } if (mIsHotseatIconOnTopWhenAligned) { setter.addFloat(child, VIEW_ALPHA, 0f, 1f, isToHome ? Interpolators.clampToProgress(LINEAR, 0f, 0.35f) : mActivity.getDeviceProfile().isQsbInline ? Interpolators.clampToProgress(LINEAR, 0f, 1f) : Interpolators.clampToProgress(LINEAR, 0.84f, 1f)); } setter.addOnFrameListener(animator -> AlphaUpdateListener.updateVisibility(child)); continue; } int recentTaskIndex = -1; if (isRecentTask) { if (firstRecentTaskIndex < 0) { firstRecentTaskIndex = i; } recentTaskIndex = i - firstRecentTaskIndex; } float positionInHotseat = getPositionInHotseat(taskbarDp.numShownHotseatIcons, child, mIsRtl, isAllAppsButton, isTaskbarDividerView, mTaskbarView.isDividerForRecents(), recentTaskIndex); if (positionInHotseat == ERROR_POSITION_IN_HOTSEAT_NOT_FOUND) continue; float hotseatIconCenter; if (launcherDp.shouldAdjustHotseatForBubbleBar(child.getContext(), bubbleBarHasBubbles())) { float hotseatAdjustedBorderSpace = launcherDp.getHotseatAdjustedBorderSpaceForBubbleBar(child.getContext()); hotseatIconCenter = hotseatPadding.left + hotseatCellSize + (hotseatCellSize + hotseatAdjustedBorderSpace) * positionInHotseat + hotseatCellSize / 2f; } else { hotseatIconCenter = hotseatPadding.left + (hotseatCellSize + borderSpacing) * positionInHotseat + hotseatCellSize / 2f; } hotseatIconCenter += hotseatNavBarTranslationX; float childCenter = (child.getLeft() + child.getRight()) / 2f; childCenter += ((Reorderable) child).getTranslateDelegate().getTranslationX( INDEX_TASKBAR_PINNING_ANIM).getValue(); float toX = hotseatIconCenter - childCenter; if (child instanceof Reorderable) { MultiTranslateDelegate mtd = ((Reorderable) child).getTranslateDelegate(); setter.setFloat(mtd.getTranslationX(INDEX_TASKBAR_ALIGNMENT_ANIM), MULTI_PROPERTY_VALUE, toX, interpolator); setter.setFloat(mtd.getTranslationY(INDEX_TASKBAR_ALIGNMENT_ANIM), MULTI_PROPERTY_VALUE, mTaskbarBottomMargin, interpolator); } else { setter.setFloat(child, VIEW_TRANSLATE_X, toX, interpolator); setter.setFloat(child, VIEW_TRANSLATE_Y, mTaskbarBottomMargin, interpolator); } setter.setFloat(child, SCALE_PROPERTY, scaleUp, interpolator); } AnimatorPlaybackController controller = setter.createPlaybackController(); mOnControllerPreCreateCallback = () -> controller.setPlayFraction(0); return controller; } /** * Returns the index of the given child relative to its position in hotseat. * Examples: * -1 is the item before the first hotseat item. * -0.5 is between those (e.g. for the divider). * {@link #ERROR_POSITION_IN_HOTSEAT_NOT_FOUND} if there's no calculation relative to hotseat. */ @VisibleForTesting float getPositionInHotseat(int numShownHotseatIcons, View child, boolean isRtl, boolean isAllAppsButton, boolean isTaskbarDividerView, boolean isDividerForRecents, int recentTaskIndex) { float positionInHotseat; // Note that there is no All Apps button in the hotseat, // this position is only used as it's convenient for animation purposes. float allAppsButtonPositionInHotseat = isRtl // Right after all hotseat items. // [HHHHHH]|[>A<] ? numShownHotseatIcons // Right before all hotseat items. // [>A<]|[HHHHHH] : -1; // Note that there are no recent tasks in the hotseat, // this position is only used as it's convenient for animation purposes. float firstRecentTaskPositionInHotseat = isRtl // After all hotseat icons and All Apps button. // [HHHHHH][A]|[>RR 0 ? relativePosition - DIVIDER_VIEW_POSITION_OFFSET : relativePosition + DIVIDER_VIEW_POSITION_OFFSET; } else if (child.getTag() instanceof ItemInfo) { positionInHotseat = ((ItemInfo) child.getTag()).screenId; } else if (recentTaskIndex >= 0) { positionInHotseat = firstRecentTaskPositionInHotseat + recentTaskIndex; } else { Log.w(TAG, "Unsupported view found in createIconAlignmentController, v=" + child); return ERROR_POSITION_IN_HOTSEAT_NOT_FOUND; } return positionInHotseat; } private boolean bubbleBarHasBubbles() { return mControllers.bubbleControllers.isPresent() && mControllers.bubbleControllers.get().bubbleBarViewController.hasBubbles(); } public void onRotationChanged(DeviceProfile deviceProfile) { if (!mControllers.uiController.isIconAlignedWithHotseat()) { // We only translate on rotation when icon is aligned with hotseat return; } int taskbarWindowSize; if (mActivity.isPhoneMode()) { taskbarWindowSize = mActivity.getResources().getDimensionPixelSize( mActivity.isThreeButtonNav() ? R.dimen.taskbar_phone_size : R.dimen.taskbar_stashed_size); } else { taskbarWindowSize = deviceProfile.taskbarHeight + deviceProfile.getTaskbarOffsetY(); } mActivity.setTaskbarWindowSize(taskbarWindowSize); mTaskbarNavButtonTranslationY.updateValue(-deviceProfile.getTaskbarOffsetY()); } /** * Maps the given operator to all the top-level children of TaskbarView. */ public void mapOverItems(LauncherBindableItemsContainer.ItemOperator op) { mTaskbarView.mapOverItems(op); } /** * Returns the first icon to match the given parameter, in priority from: * 1) Icons directly on Taskbar * 2) FolderIcon of the Folder containing the given icon * 3) All Apps button */ public View getFirstIconMatch(Predicate matcher) { Predicate collectionMatcher = ItemInfoMatcher.forFolderMatch(matcher); return mTaskbarView.getFirstMatch(matcher, collectionMatcher); } /** * Returns whether the given MotionEvent, *in screen coorindates*, is within any Taskbar item's * touch bounds. */ public boolean isEventOverAnyItem(MotionEvent ev) { return mTaskbarView.isEventOverAnyItem(ev); } /** Called when there's a change in running apps to update the UI. */ public void commitRunningAppsToUI() { mModelCallbacks.commitRunningAppsToUI(); if (taskbarRecentsLayoutTransition() && mTaskbarView.getLayoutTransition() == null) { // Set up after the first commit so that the initial recents do not animate (janky). mTaskbarView.setLayoutTransition(createLayoutTransitionForRunningApps()); } } private LayoutTransition createLayoutTransitionForRunningApps() { LayoutTransition layoutTransition = new LayoutTransition(); layoutTransition.setDuration(TRANSITION_DEFAULT_DURATION); layoutTransition.addTransitionListener(new TransitionListener() { @Override public void startTransition( LayoutTransition transition, ViewGroup container, View view, int type) { if (type == APPEARING) { view.setAlpha(0f); view.setScaleX(0f); view.setScaleY(0f); } } @Override public void endTransition( LayoutTransition transition, ViewGroup container, View view, int type) { // Do nothing. } }); // Appearing. AnimatorSet appearingSet = new AnimatorSet(); Animator appearingAlphaAnimator = ObjectAnimator.ofFloat(null, "alpha", 0f, 1f); appearingAlphaAnimator.setInterpolator(Interpolators.clampToProgress(LINEAR, 0f, (float) TRANSITION_FADE_IN_DURATION / TRANSITION_DEFAULT_DURATION)); Animator appearingScaleAnimator = ObjectAnimator.ofFloat(null, SCALE_PROPERTY, 0f, 1f); appearingScaleAnimator.setInterpolator(EMPHASIZED); appearingSet.playTogether(appearingAlphaAnimator, appearingScaleAnimator); layoutTransition.setAnimator(APPEARING, appearingSet); layoutTransition.setStartDelay(APPEARING, TRANSITION_DELAY); // Disappearing. AnimatorSet disappearingSet = new AnimatorSet(); Animator disappearingAlphaAnimator = ObjectAnimator.ofFloat(null, "alpha", 1f, 0f); disappearingAlphaAnimator.setInterpolator(Interpolators.clampToProgress(LINEAR, (float) TRANSITION_DELAY / TRANSITION_DEFAULT_DURATION, (float) (TRANSITION_DELAY + TRANSITION_FADE_OUT_DURATION) / TRANSITION_DEFAULT_DURATION)); Animator disappearingScaleAnimator = ObjectAnimator.ofFloat(null, SCALE_PROPERTY, 1f, 0f); disappearingScaleAnimator.setInterpolator(EMPHASIZED); disappearingSet.playTogether(disappearingAlphaAnimator, disappearingScaleAnimator); layoutTransition.setAnimator(DISAPPEARING, disappearingSet); // Change transitions. FloatProperty translateXPinning = new FloatProperty<>("translateXPinning") { @Override public void setValue(View view, float value) { getTranslationXForPinning(view).setValue(value); } @Override public Float get(View view) { return getTranslationXForPinning(view).getValue(); } private MultiProperty getTranslationXForPinning(View view) { return ((Reorderable) view).getTranslateDelegate() .getTranslationX(INDEX_TASKBAR_PINNING_ANIM); } }; AnimatorSet changeSet = new AnimatorSet(); changeSet.playTogether( layoutTransition.getAnimator(CHANGE_APPEARING), ObjectAnimator.ofFloat(null, translateXPinning, 0f, 1f)); // Change appearing. layoutTransition.setAnimator(CHANGE_APPEARING, changeSet); layoutTransition.setInterpolator(CHANGE_APPEARING, EMPHASIZED); // Change disappearing. layoutTransition.setAnimator(CHANGE_DISAPPEARING, changeSet); layoutTransition.setInterpolator(CHANGE_DISAPPEARING, EMPHASIZED); layoutTransition.setStartDelay(CHANGE_DISAPPEARING, TRANSITION_DELAY); return layoutTransition; } /** * To be called when the given Task is updated, so that we can tell TaskbarView to also update. * @param task The Task whose e.g. icon changed. */ public void onTaskUpdated(Task task) { // Find the icon view(s) that changed. for (View view : mTaskbarView.getIconViews()) { if (view instanceof BubbleTextView btv && view.getTag() instanceof GroupTask groupTask) { if (groupTask.containsTask(task.key.id)) { mTaskbarView.applyGroupTaskToBubbleTextView(btv, groupTask); } } else if (view instanceof TaskbarOverflowView overflowButton) { overflowButton.updateTaskIsShown(task); } } } @Override public void dumpLogs(String prefix, PrintWriter pw) { pw.println(prefix + "TaskbarViewController:"); mTaskbarIconAlpha.dump( prefix + "\t", pw, "mTaskbarIconAlpha", "ALPHA_INDEX_HOME", "ALPHA_INDEX_KEYGUARD", "ALPHA_INDEX_STASH", "ALPHA_INDEX_RECENTS_DISABLED", "ALPHA_INDEX_NOTIFICATION_EXPANDED", "ALPHA_INDEX_ASSISTANT_INVOKED", "ALPHA_INDEX_SMALL_SCREEN"); mModelCallbacks.dumpLogs(prefix + "\t", pw); } /** Enables model loading for tests. */ @VisibleForTesting public static void enableModelLoadingForTests(boolean enable) { sEnableModelLoadingForTests = enable; } private ObjectAnimator createTaskbarIconsShiftAnimator(float translationX) { ObjectAnimator animator = mIconsTranslationXForNavbar.animateToValue(translationX); animator.setStartDelay(FADE_OUT_ANIM_POSITION_DURATION_MS); animator.setDuration(FADE_IN_ANIM_ALPHA_DURATION_MS); animator.setInterpolator(Interpolators.EMPHASIZED); return animator; } }