diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index cb3c446dd9..9a68389354 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -372,6 +372,9 @@
2dp
12dp
4dp
+ 6dp
+ 20dp
+ 4dp
12dp
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarRunningAppStateAnimationController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarRunningAppStateAnimationController.kt
new file mode 100644
index 0000000000..4e690f192b
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarRunningAppStateAnimationController.kt
@@ -0,0 +1,276 @@
+/*
+ * Copyright (C) 2025 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 android.animation.AnimatorSet
+import android.animation.ArgbEvaluator
+import android.animation.ObjectAnimator
+import android.content.Context
+import android.graphics.Color.TRANSPARENT
+import android.util.FloatProperty
+import android.util.IntProperty
+import androidx.annotation.ColorInt
+import androidx.annotation.Px
+import androidx.annotation.UiThread
+import androidx.core.animation.doOnEnd
+import com.android.app.animation.Interpolators.EMPHASIZED
+import com.android.app.animation.Interpolators.LINEAR
+import com.android.internal.dynamicanimation.animation.FloatValueHolder
+import com.android.internal.dynamicanimation.animation.SpringAnimation
+import com.android.internal.dynamicanimation.animation.SpringForce
+import com.android.internal.dynamicanimation.animation.SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY
+import com.android.internal.dynamicanimation.animation.SpringForce.DAMPING_RATIO_NO_BOUNCY
+import com.android.launcher3.BubbleTextView
+import com.android.launcher3.BubbleTextView.RunningAppState
+import com.android.launcher3.BubbleTextView.RunningAppState.MINIMIZED
+import com.android.launcher3.BubbleTextView.RunningAppState.NOT_RUNNING
+import com.android.launcher3.BubbleTextView.RunningAppState.RUNNING
+import com.android.launcher3.R
+import com.android.launcher3.Utilities as LauncherUtilities
+import com.android.launcher3.model.data.TaskItemInfo
+import com.android.launcher3.taskbar.TaskbarViewController.TRANSITION_DEFAULT_DURATION
+import com.android.launcher3.util.MultiPropertyFactory
+import com.android.launcher3.util.MultiTranslateDelegate.INDEX_TASKBAR_APP_RUNNING_STATE_ANIM
+
+private const val SPRING_START = 0f
+private const val SPRING_END = 100f
+
+private val SPRING_NO_BOUNCY =
+ SpringForce(SPRING_END).apply {
+ dampingRatio = DAMPING_RATIO_NO_BOUNCY
+ stiffness = 3800f
+ }
+private val SPRING_MEDIUM_BOUNCY =
+ SpringForce(SPRING_END).apply {
+ dampingRatio = DAMPING_RATIO_MEDIUM_BOUNCY
+ stiffness = 400f
+ }
+
+private val TRANSLATE_Y_PROPERTY =
+ object : FloatProperty("runningStateTranslateY") {
+ private val BubbleTextView.runningStateTranslateYProp: MultiPropertyFactory<*>.MultiProperty
+ get() = translateDelegate.getTranslationY(INDEX_TASKBAR_APP_RUNNING_STATE_ANIM)
+
+ override fun get(btv: BubbleTextView): Float = btv.runningStateTranslateYProp.value
+
+ override fun setValue(btv: BubbleTextView, translateY: Float) {
+ btv.runningStateTranslateYProp.value = translateY
+ btv.invalidate()
+ }
+ }
+
+private val LINE_COLOR_PROPERTY =
+ object : IntProperty("lineIndicatorColor") {
+ override fun get(btv: BubbleTextView): Int = btv.lineIndicatorColor
+
+ override fun setValue(btv: BubbleTextView, color: Int) {
+ btv.lineIndicatorColor = color
+ }
+ }
+
+private val LINE_WIDTH_PROPERTY =
+ object : FloatProperty("lineIndicatorWidth") {
+ override fun get(btv: BubbleTextView): Float = btv.lineIndicatorWidth
+
+ override fun setValue(btv: BubbleTextView, width: Float) {
+ btv.lineIndicatorWidth = width
+ }
+ }
+
+/** Manages Taskbar [BubbleTextView] animations from changes in [RunningAppState]. */
+class TaskbarRunningAppStateAnimationController(context: Context) {
+
+ /** Animating [BubbleTextView] instances, where invoking each value cancels the animation. */
+ private val runningAnimations = mutableMapOf Unit>()
+
+ private val argbEvaluator = ArgbEvaluator()
+ private val runningLineColor =
+ context.resources.getColor(R.color.taskbar_running_app_indicator_color, context.theme)
+ private val minimizedLineColor =
+ context.resources.getColor(R.color.taskbar_minimized_app_indicator_color, context.theme)
+
+ private val runningLineWidth =
+ context.resources
+ .getDimensionPixelSize(R.dimen.taskbar_running_app_indicator_width)
+ .toFloat()
+ private val minimizedLineWidth =
+ context.resources
+ .getDimensionPixelSize(R.dimen.taskbar_minimized_app_indicator_width)
+ .toFloat()
+
+ private val appTranslateYSpring =
+ context.resources.getDimensionPixelSize(R.dimen.taskbar_app_translate_y_spring).toFloat()
+ private val lineWidthSpring =
+ context.resources
+ .getDimensionPixelSize(R.dimen.taskbar_line_indicator_width_spring)
+ .toFloat()
+
+ // Copies the keys to avoid concurrent modification due to value callbacks modifying the map.
+ fun onDestroy() = runningAnimations.keys.toList().forEach { cancelAnimation(it) }
+
+ @UiThread
+ fun updateRunningState(btv: BubbleTextView, runningState: RunningAppState, animate: Boolean) {
+ val prevRunningState = btv.runningAppState ?: NOT_RUNNING
+ if (runningState == prevRunningState) return
+
+ cancelAnimation(btv)
+ btv.runningAppState = runningState
+ if (!animate) return applyRunningState(btv)
+
+ val isPinnedApp = btv.tag is TaskItemInfo
+ if (
+ (prevRunningState == RUNNING && runningState == MINIMIZED) ||
+ (prevRunningState == MINIMIZED && runningState == RUNNING) ||
+ (isPinnedApp && runningState == RUNNING)
+ ) {
+ return startAppBounceAnimation(btv)
+ }
+
+ // Otherwise just animate line width and color.
+ AnimatorSet().run {
+ if (runningState == RUNNING) {
+ // New unpinned app - delay animation until icon is mostly scaled in.
+ startDelay = UNPINNED_APP_LINE_ANIM_DELAY
+ }
+
+ duration = LINE_ANIM_DURATION
+ interpolator = EMPHASIZED
+
+ playTogether(
+ ObjectAnimator.ofFloat(btv, LINE_WIDTH_PROPERTY, runningState.lineWidth),
+ ObjectAnimator.ofArgb(btv, LINE_COLOR_PROPERTY, runningState.lineColor),
+ )
+
+ doOnEnd {
+ runningAnimations.remove(btv)
+ applyRunningState(btv)
+ }
+
+ runningAnimations[btv] = this::cancel
+ start()
+ }
+ }
+
+ fun isAnimationRunning(btv: BubbleTextView): Boolean = runningAnimations.containsKey(btv)
+
+ private fun cancelAnimation(btv: BubbleTextView) = runningAnimations[btv]?.invoke()
+
+ private fun startAppBounceAnimation(btv: BubbleTextView) {
+ val isMinimized = btv.runningAppState == MINIMIZED
+ val translateYSpring = if (isMinimized) appTranslateYSpring else -appTranslateYSpring
+ val prevLineWidth = btv.lineIndicatorWidth
+ val prevLineColor = btv.lineIndicatorColor
+
+ val translateYProp =
+ btv.translateDelegate.getTranslationY(INDEX_TASKBAR_APP_RUNNING_STATE_ANIM)
+ fun updateTranslateY(value: Float) {
+ translateYProp.value = value
+ btv.invalidate()
+ }
+
+ SpringAnimation(FloatValueHolder()).run {
+ spring = SPRING_NO_BOUNCY
+ addUpdateListener { _, v, _ ->
+ updateTranslateY(mapValue(v, 0f, translateYSpring))
+ if (isMinimized) {
+ btv.lineIndicatorWidth = mapValue(v, prevLineWidth, lineWidthSpring)
+ }
+ }
+
+ addEndListener { _, canceled, _, _ ->
+ runningAnimations.remove(btv)
+ if (canceled) return@addEndListener applyRunningState(btv)
+
+ val startLineWidth = if (isMinimized) lineWidthSpring else prevLineWidth
+ val endLineWidth = btv.runningAppState.lineWidth
+ val endLineColor = btv.runningAppState.lineColor
+
+ val springs =
+ listOf(
+ SpringAnimation(FloatValueHolder()).apply {
+ spring = SPRING_MEDIUM_BOUNCY
+ addUpdateListener { _, v, _ ->
+ updateTranslateY(mapValue(v, translateYSpring, 0f))
+ btv.lineIndicatorWidth = mapValue(v, startLineWidth, endLineWidth)
+ }
+ },
+ SpringAnimation(FloatValueHolder()).apply {
+ spring = SPRING_NO_BOUNCY
+ addUpdateListener { _, v, _ ->
+ btv.lineIndicatorColor =
+ argbEvaluator.evaluate(
+ v / SPRING_END,
+ prevLineColor,
+ endLineColor,
+ ) as Int
+ }
+ },
+ )
+
+ runningAnimations[btv] = { for (s in springs) s.cancel() }
+ var runningSprings = springs.size
+ for (s in springs) {
+ s.addEndListener { _, canceled2, _, _ ->
+ if (--runningSprings == 0) {
+ runningAnimations.remove(btv)
+ if (canceled2) applyRunningState(btv)
+ }
+ }
+ s.start()
+ }
+ }
+
+ runningAnimations[btv] = this::cancel
+ start()
+ }
+ }
+
+ private fun applyRunningState(btv: BubbleTextView) {
+ btv.lineIndicatorWidth = btv.runningAppState.lineWidth
+ btv.lineIndicatorColor = btv.runningAppState.lineColor
+ TRANSLATE_Y_PROPERTY[btv] = 0f
+ }
+
+ private fun mapValue(value: Float, min: Float, max: Float): Float {
+ return LauncherUtilities.mapToRange(value, SPRING_START, SPRING_END, min, max, LINEAR)
+ }
+
+ @get:ColorInt
+ val RunningAppState.lineColor: Int
+ get() {
+ return when (this) {
+ NOT_RUNNING -> TRANSPARENT
+ RUNNING -> runningLineColor
+ MINIMIZED -> minimizedLineColor
+ }
+ }
+
+ @get:Px
+ val RunningAppState.lineWidth: Float
+ get() {
+ return when (this) {
+ NOT_RUNNING -> 0f
+ RUNNING -> runningLineWidth
+ MINIMIZED -> minimizedLineWidth
+ }
+ }
+
+ companion object {
+ const val LINE_ANIM_DURATION = 100L
+ const val UNPINNED_APP_LINE_ANIM_DELAY = TRANSITION_DEFAULT_DURATION - LINE_ANIM_DURATION
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
index c5b97e7bbc..eca2e11735 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
@@ -25,7 +25,6 @@ import static android.window.DesktopModeFlags.ENABLE_TASKBAR_RECENTS_LAYOUT_TRAN
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.BubbleTextView.LINE_INDICATOR_ANIM_DURATION;
import static com.android.launcher3.Flags.enableScalingRevealHomeAnimation;
import static com.android.launcher3.Flags.taskbarOverflow;
import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
@@ -136,11 +135,9 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar
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;
+ 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 final int APPEARING_LINE_INDICATOR_ANIM_DELAY =
- TRANSITION_DEFAULT_DURATION - LINE_INDICATOR_ANIM_DURATION;
private final TaskbarActivityContext mActivity;
private @Nullable TaskbarDragLayerController mDragLayerController;
@@ -224,6 +221,8 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar
private final float mTaskbarLeftRightMargin;
+ private final TaskbarRunningAppStateAnimationController mRunningStateController;
+
public TaskbarViewController(TaskbarActivityContext activity, TaskbarView taskbarView) {
mActivity = activity;
mTransientTaskbarDp = mActivity.getTransientTaskbarDeviceProfile();
@@ -242,6 +241,7 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar
mIsRtl = Utilities.isRtl(mTaskbarView.getResources());
mTaskbarLeftRightMargin = mActivity.getResources().getDimensionPixelSize(
R.dimen.transient_taskbar_padding);
+ mRunningStateController = new TaskbarRunningAppStateAnimationController(mActivity);
}
/**
@@ -395,6 +395,7 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar
}
LauncherAppState.getInstance(mActivity).getModel().removeCallbacks(mModelCallbacks);
mActivity.removeOnDeviceProfileChangeListener(mDeviceProfileChangeListener);
+ mRunningStateController.onDestroy();
}
/**
@@ -773,7 +774,10 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar
}
private void updateRunningState(BubbleTextView btv) {
- btv.updateRunningState(getRunningAppState(btv), mTaskbarView.getLayoutTransition() != null);
+ mRunningStateController.updateRunningState(
+ btv,
+ getRunningAppState(btv),
+ /* animate = */ mTaskbarView.getLayoutTransition() != null);
}
private BubbleTextView.RunningAppState getRunningAppState(BubbleTextView btv) {
@@ -1233,10 +1237,6 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar
view.setAlpha(0f);
view.setScaleX(0f);
view.setScaleY(0f);
- if (view instanceof BubbleTextView btv) {
- // Defer so that app is mostly scaled in before showing indicator.
- btv.setLineIndicatorAnimStartDelay(APPEARING_LINE_INDICATOR_ANIM_DELAY);
- }
} else if (type == DISAPPEARING && view instanceof BubbleTextView btv) {
// Running state updates happen after removing this view, so update it here.
updateRunningState(btv);
@@ -1246,9 +1246,7 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar
@Override
public void endTransition(
LayoutTransition transition, ViewGroup container, View view, int type) {
- if (type == APPEARING && view instanceof BubbleTextView btv) {
- btv.setLineIndicatorAnimStartDelay(0);
- }
+ // Do nothing.
}
});
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarRunningAppStateAnimationControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarRunningAppStateAnimationControllerTest.kt
new file mode 100644
index 0000000000..bdc8901ec7
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarRunningAppStateAnimationControllerTest.kt
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2025 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 android.animation.AnimatorTestRule
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.launcher3.BubbleTextView
+import com.android.launcher3.BubbleTextView.RunningAppState
+import com.android.launcher3.BubbleTextView.RunningAppState.MINIMIZED
+import com.android.launcher3.BubbleTextView.RunningAppState.NOT_RUNNING
+import com.android.launcher3.BubbleTextView.RunningAppState.RUNNING
+import com.android.launcher3.model.data.TaskItemInfo
+import com.android.launcher3.taskbar.TaskbarControllerTestUtil.runOnMainSync
+import com.android.launcher3.taskbar.TaskbarRunningAppStateAnimationController.Companion.LINE_ANIM_DURATION
+import com.android.launcher3.taskbar.TaskbarRunningAppStateAnimationController.Companion.UNPINNED_APP_LINE_ANIM_DELAY
+import com.android.launcher3.util.ActivityContextWrapper
+import com.android.launcher3.util.LauncherMultivalentJUnit
+import com.android.launcher3.util.MultiTranslateDelegate.INDEX_TASKBAR_APP_RUNNING_STATE_ANIM
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val FRAME_TIME_MS = 16L // Simulates 60 Hz.
+private val PINNED_APP = TaskItemInfo(0, TaskbarViewTestUtil.createHotseatWorkspaceItem(0))
+private val UNPINNED_APP = TaskbarViewTestUtil.createRecentTask(1)
+
+@RunWith(LauncherMultivalentJUnit::class)
+class TaskbarRunningAppStateAnimationControllerTest {
+
+ @get:Rule val animatorTestRule = AnimatorTestRule(this)
+
+ private val context = ActivityContextWrapper(getInstrumentation().targetContext)
+ private val btv = BubbleTextView(context)
+ private val controller = TaskbarRunningAppStateAnimationController(context)
+
+ @Test
+ fun updateRunningState_minimizeApp_verifySpringEndState() {
+ startStateChange(start = RUNNING, end = MINIMIZED)
+ verifySpringAnimationEnd(MINIMIZED)
+ }
+
+ @Test
+ fun updateRunningState_minimizeApp_verifyCancelEndState() {
+ startStateChange(start = RUNNING, end = MINIMIZED)
+ runOnMainSync { controller.onDestroy() }
+ verifyStateSettled(state = MINIMIZED)
+ }
+
+ @Test
+ fun updateRunningState_restoreApp_verifySpringEndState() {
+ startStateChange(start = MINIMIZED, end = RUNNING)
+ verifySpringAnimationEnd(RUNNING)
+ }
+
+ @Test
+ fun updateRunningState_openPinnedApp_verifySpringEndState() {
+ btv.tag = PINNED_APP
+ startStateChange(start = NOT_RUNNING, end = RUNNING)
+ verifySpringAnimationEnd(RUNNING)
+ }
+
+ @Test
+ fun updateRunningState_openUnpinnedApp_verifyStartDelay() {
+ btv.tag = UNPINNED_APP
+ startStateChange(start = NOT_RUNNING, end = RUNNING)
+ runOnMainSync { animatorTestRule.advanceTimeBy(UNPINNED_APP_LINE_ANIM_DELAY) }
+ verifyLineIndicator(state = NOT_RUNNING)
+ }
+
+ @Test
+ fun updateRunningState_openUnpinnedApp_verifyEndState() {
+ btv.tag = UNPINNED_APP
+ startStateChange(start = NOT_RUNNING, end = RUNNING)
+ runOnMainSync {
+ animatorTestRule.advanceTimeBy(UNPINNED_APP_LINE_ANIM_DELAY + LINE_ANIM_DURATION)
+ }
+
+ verifyStateSettled(state = RUNNING)
+ }
+
+ @Test
+ fun updateRunningState_openUnpinnedApp_verifyCancelEndState() {
+ btv.tag = UNPINNED_APP
+ startStateChange(start = NOT_RUNNING, end = RUNNING)
+ runOnMainSync { controller.onDestroy() }
+ verifyStateSettled(state = RUNNING)
+ }
+
+ @Test
+ fun updateRunningState_closeApp_verifyEndState() {
+ startStateChange(start = RUNNING, end = NOT_RUNNING)
+ runOnMainSync { animatorTestRule.advanceTimeBy(LINE_ANIM_DURATION) }
+ verifyStateSettled(state = NOT_RUNNING)
+ }
+
+ @Test
+ fun updateRunningState_repeatUpdateDuringAnimation_animationNotCanceled() {
+ startStateChange(start = MINIMIZED, end = RUNNING)
+ runOnMainSync {
+ animatorTestRule.advanceTimeBy(FRAME_TIME_MS)
+ controller.updateRunningState(btv, RUNNING, animate = false)
+ }
+ assertThat(controller.isAnimationRunning(btv)).isTrue()
+ }
+
+ @Test
+ fun updateRunningState_minimizedDuringOpen_verifyMinimizedEndState() {
+ startStateChange(start = NOT_RUNNING, end = RUNNING)
+ runOnMainSync { controller.updateRunningState(btv, MINIMIZED, animate = true) }
+ verifySpringAnimationEnd(MINIMIZED)
+ }
+
+ @Test
+ fun onDestroy_multipleAnimations_cancelsAll() {
+ startStateChange(start = RUNNING, end = MINIMIZED)
+ val btv2 = BubbleTextView(context)
+ startStateChange(btv = btv2, start = RUNNING, end = MINIMIZED)
+
+ runOnMainSync { controller.onDestroy() }
+ verifyStateSettled(state = MINIMIZED)
+ verifyStateSettled(btv = btv2, state = MINIMIZED)
+ }
+
+ private fun startStateChange(
+ btv: BubbleTextView = this.btv,
+ start: RunningAppState,
+ end: RunningAppState,
+ ) {
+ runOnMainSync {
+ controller.updateRunningState(btv, start, animate = false)
+ controller.updateRunningState(btv, end, animate = true)
+ }
+ verifyLineIndicator(btv, start)
+ assertThat(controller.isAnimationRunning(btv)).isTrue()
+ }
+
+ /** Verifies [btv] spring animation ends at [state]. */
+ private fun verifySpringAnimationEnd(state: RunningAppState) {
+ while (controller.isAnimationRunning(btv)) {
+ runOnMainSync { animatorTestRule.advanceTimeBy(FRAME_TIME_MS) }
+ }
+ verifyStateSettled(state = state)
+ }
+
+ private fun verifyStateSettled(btv: BubbleTextView = this.btv, state: RunningAppState) {
+ assertThat(controller.isAnimationRunning(btv)).isFalse()
+ verifyLineIndicator(btv, state)
+
+ val translateYProp =
+ btv.translateDelegate.getTranslationY(INDEX_TASKBAR_APP_RUNNING_STATE_ANIM)
+ assertThat(translateYProp.value).isZero()
+ }
+
+ private fun verifyLineIndicator(btv: BubbleTextView = this.btv, state: RunningAppState) {
+ controller.run {
+ assertThat(btv.lineIndicatorWidth).isEqualTo(state.lineWidth)
+ assertThat(btv.lineIndicatorColor).isEqualTo(state.lineColor)
+ }
+ }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTestUtil.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTestUtil.kt
index 92abbbaa0a..4c7c81caae 100644
--- a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTestUtil.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarViewTestUtil.kt
@@ -83,21 +83,23 @@ object TaskbarViewTestUtil {
/** Creates a list of fake recent tasks. */
fun createRecents(size: Int): List {
- return List(size) {
- SingleTask(
- Task().apply {
- key =
- TaskKey(
- it,
- 5,
- TEST_INTENT,
- TEST_COMPONENT,
- Process.myUserHandle().identifier,
- System.currentTimeMillis(),
- )
- }
- )
- }
+ return List(size) { createRecentTask(it) }
+ }
+
+ fun createRecentTask(id: Int = 0): GroupTask {
+ return SingleTask(
+ Task().apply {
+ key =
+ TaskKey(
+ id,
+ 5,
+ TEST_INTENT,
+ TEST_COMPONENT,
+ Process.myUserHandle().identifier,
+ System.currentTimeMillis(),
+ )
+ }
+ )
}
}
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index a15c130076..39dc4fb1ac 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -445,6 +445,9 @@
0dp
0dp
0dp
+ 0dp
+ 0dp
+ 0dp
0dp
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index 30e3a2b577..1225ae6b72 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -20,9 +20,7 @@ import static android.graphics.fonts.FontStyle.FONT_WEIGHT_BOLD;
import static android.graphics.fonts.FontStyle.FONT_WEIGHT_NORMAL;
import static android.text.Layout.Alignment.ALIGN_NORMAL;
-import static com.android.app.animation.Interpolators.EMPHASIZED;
import static com.android.launcher3.BubbleTextView.RunningAppState.RUNNING;
-import static com.android.launcher3.BubbleTextView.RunningAppState.NOT_RUNNING;
import static com.android.launcher3.BubbleTextView.RunningAppState.MINIMIZED;
import static com.android.launcher3.Flags.enableContrastTiles;
import static com.android.launcher3.Flags.enableCursorHoverStates;
@@ -36,10 +34,10 @@ import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTO
import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_INCREMENTAL_DOWNLOAD_ACTIVE;
import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE;
import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_SHOW_DOWNLOAD_PROGRESS_MASK;
+import static com.android.launcher3.util.MultiTranslateDelegate.INDEX_TASKBAR_APP_RUNNING_STATE_ANIM;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.content.res.ColorStateList;
@@ -134,9 +132,6 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver,
StringMatcherUtility.StringMatcher.getInstance();
private static final int BOLD_TEXT_ADJUSTMENT = FONT_WEIGHT_BOLD - FONT_WEIGHT_NORMAL;
- public static final int LINE_INDICATOR_ANIM_DURATION = 150;
- private static final float MINIMIZED_APP_INDICATOR_SCALE = 0.5f;
-
private static final int[] STATE_PRESSED = new int[]{android.R.attr.state_pressed};
private float mScaleForReorderBounce = 1f;
@@ -172,36 +167,6 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver,
}
};
- private static final Property LINE_INDICATOR_COLOR_PROPERTY =
- new Property<>(Integer.class, "lineIndicatorColor") {
-
- @Override
- public Integer get(BubbleTextView bubbleTextView) {
- return bubbleTextView.mLineIndicatorColor;
- }
-
- @Override
- public void set(BubbleTextView bubbleTextView, Integer color) {
- bubbleTextView.mLineIndicatorColor = color;
- bubbleTextView.invalidate();
- }
- };
-
- private static final Property LINE_INDICATOR_SCALE_PROPERTY =
- new Property<>(Float.TYPE, "lineIndicatorScale") {
-
- @Override
- public Float get(BubbleTextView bubbleTextView) {
- return bubbleTextView.mLineIndicatorScale;
- }
-
- @Override
- public void set(BubbleTextView bubbleTextView, Float scale) {
- bubbleTextView.mLineIndicatorScale = scale;
- bubbleTextView.invalidate();
- }
- };
-
private final MultiTranslateDelegate mTranslateDelegate = new MultiTranslateDelegate(this);
protected final ActivityContext mActivity;
private FastBitmapDrawable mIcon;
@@ -238,20 +203,16 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver,
private boolean mForceHideDot;
// These fields, related to showing running apps, are only used for Taskbar.
- private final int mRunningAppIndicatorWidth;
private final int mRunningAppIndicatorHeight;
private final int mRunningAppIndicatorTopMargin;
private final Paint mRunningAppIndicatorPaint;
private final Rect mRunningAppIconBounds = new Rect();
private RunningAppState mRunningAppState;
- private final int mRunningAppIndicatorColor;
- private final int mMinimizedAppIndicatorColor;
+
@ViewDebug.ExportedProperty(category = "launcher")
private int mLineIndicatorColor;
@ViewDebug.ExportedProperty(category = "launcher")
- private float mLineIndicatorScale;
- private int mLineIndicatorAnimStartDelay;
- private Animator mLineIndicatorAnim;
+ private float mLineIndicatorWidth;
private final String mMinimizedStateDescription;
private final String mRunningStateDescription;
@@ -334,19 +295,12 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver,
defaultIconSize);
a.recycle();
- mRunningAppIndicatorWidth =
- getResources().getDimensionPixelSize(R.dimen.taskbar_running_app_indicator_width);
mRunningAppIndicatorHeight =
getResources().getDimensionPixelSize(R.dimen.taskbar_running_app_indicator_height);
mRunningAppIndicatorTopMargin =
getResources().getDimensionPixelSize(
R.dimen.taskbar_running_app_indicator_top_margin);
-
mRunningAppIndicatorPaint = new Paint();
- mRunningAppIndicatorColor = getResources().getColor(
- R.color.taskbar_running_app_indicator_color, context.getTheme());
- mMinimizedAppIndicatorColor = getResources().getColor(
- R.color.taskbar_minimized_app_indicator_color, context.getTheme());
mLongPressHelper = new CheckLongPressHelper(this);
@@ -385,9 +339,7 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver,
setBackground(null);
mLineIndicatorColor = Color.TRANSPARENT;
- mLineIndicatorScale = 0;
- mLineIndicatorAnimStartDelay = 0;
- cancelLineIndicatorAnim();
+ mLineIndicatorWidth = 0;
setTag(null);
if (mIconLoadRequest != null) {
@@ -479,52 +431,30 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver,
setContentDescription(label);
}
- /** Updates whether the app this view represents is currently running. */
- @UiThread
- public void updateRunningState(RunningAppState runningAppState, boolean animate) {
- if (runningAppState.equals(mRunningAppState)) {
- return;
- }
+ public void setRunningAppState(RunningAppState runningAppState) {
mRunningAppState = runningAppState;
- cancelLineIndicatorAnim();
-
- int color = switch (mRunningAppState) {
- case NOT_RUNNING -> Color.TRANSPARENT;
- case RUNNING -> mRunningAppIndicatorColor;
- case MINIMIZED -> mMinimizedAppIndicatorColor;
- };
- float scale = switch (mRunningAppState) {
- case NOT_RUNNING -> 0;
- case RUNNING -> 1;
- case MINIMIZED -> MINIMIZED_APP_INDICATOR_SCALE;
- };
-
- if (!animate) {
- mLineIndicatorColor = color;
- mLineIndicatorScale = scale;
- invalidate();
- return;
- }
-
- AnimatorSet lineIndicatorAnim = new AnimatorSet();
- mLineIndicatorAnim = lineIndicatorAnim;
- Animator colorAnimator = ObjectAnimator.ofArgb(this, LINE_INDICATOR_COLOR_PROPERTY, color);
- Animator scaleAnimator = ObjectAnimator.ofFloat(this, LINE_INDICATOR_SCALE_PROPERTY, scale);
- lineIndicatorAnim.playTogether(colorAnimator, scaleAnimator);
-
- lineIndicatorAnim.setInterpolator(EMPHASIZED);
- lineIndicatorAnim.setStartDelay(mLineIndicatorAnimStartDelay);
- lineIndicatorAnim.setDuration(LINE_INDICATOR_ANIM_DURATION).start();
}
- public void setLineIndicatorAnimStartDelay(int lineIndicatorAnimStartDelay) {
- mLineIndicatorAnimStartDelay = lineIndicatorAnimStartDelay;
+ public RunningAppState getRunningAppState() {
+ return mRunningAppState;
}
- private void cancelLineIndicatorAnim() {
- if (mLineIndicatorAnim != null) {
- mLineIndicatorAnim.cancel();
- }
+ public int getLineIndicatorColor() {
+ return mLineIndicatorColor;
+ }
+
+ public void setLineIndicatorColor(int lineIndicatorColor) {
+ mLineIndicatorColor = lineIndicatorColor;
+ invalidate();
+ }
+
+ public float getLineIndicatorWidth() {
+ return mLineIndicatorWidth;
+ }
+
+ public void setLineIndicatorWidth(float lineIndicatorWidth) {
+ mLineIndicatorWidth = lineIndicatorWidth;
+ invalidate();
}
/**
@@ -879,22 +809,25 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver,
/** Draws a line under the app icon if this is representing a running app in Desktop Mode. */
protected void drawRunningAppIndicatorIfNecessary(Canvas canvas) {
if (mDisplay != DISPLAY_TASKBAR
- || mLineIndicatorScale == 0
+ || Float.compare(mLineIndicatorWidth, 0) == 0
|| mLineIndicatorColor == Color.TRANSPARENT) {
return;
}
getIconBounds(mRunningAppIconBounds);
Utilities.scaleRectAboutCenter(mRunningAppIconBounds, ICON_VISIBLE_AREA_FACTOR);
- final int indicatorTop = mRunningAppIconBounds.bottom + mRunningAppIndicatorTopMargin;
- final float indicatorWidth = mRunningAppIndicatorWidth * mLineIndicatorScale;
+ float taskbarAppRunningStateAnimOffset =
+ mTranslateDelegate.getTranslationY(INDEX_TASKBAR_APP_RUNNING_STATE_ANIM).getValue();
+ final float indicatorTop = mRunningAppIconBounds.bottom
+ + mRunningAppIndicatorTopMargin
+ - taskbarAppRunningStateAnimOffset;
final float cornerRadius = mRunningAppIndicatorHeight / 2f;
mRunningAppIndicatorPaint.setColor(mLineIndicatorColor);
canvas.drawRoundRect(
- mRunningAppIconBounds.centerX() - indicatorWidth / 2f,
+ mRunningAppIconBounds.centerX() - mLineIndicatorWidth / 2f,
indicatorTop,
- mRunningAppIconBounds.centerX() + indicatorWidth / 2f,
+ mRunningAppIconBounds.centerX() + mLineIndicatorWidth / 2f,
indicatorTop + mRunningAppIndicatorHeight,
cornerRadius,
cornerRadius,
diff --git a/src/com/android/launcher3/util/MultiTranslateDelegate.java b/src/com/android/launcher3/util/MultiTranslateDelegate.java
index ce006c4184..ecec39c2ef 100644
--- a/src/com/android/launcher3/util/MultiTranslateDelegate.java
+++ b/src/com/android/launcher3/util/MultiTranslateDelegate.java
@@ -39,6 +39,7 @@ public class MultiTranslateDelegate {
public static final int INDEX_TASKBAR_PINNING_ANIM = 5;
public static final int INDEX_NAV_BAR_ANIM = 6;
public static final int INDEX_BUBBLE_BAR_ANIM = 7;
+ public static final int INDEX_TASKBAR_APP_RUNNING_STATE_ANIM = 8;
// Affect all items inside of a MultipageCellLayout
public static final int INDEX_CELLAYOUT_MULTIPAGE_SPACING = 3;
@@ -49,7 +50,7 @@ public class MultiTranslateDelegate {
// Specific for hotseat items when adjusting for bubbles
public static final int INDEX_BUBBLE_ADJUSTMENT_ANIM = 3;
- public static final int COUNT = 8;
+ public static final int COUNT = 9;
private final MultiPropertyFactory mTranslationX;
private final MultiPropertyFactory mTranslationY;