Implement spring animations for running state changes.
Flag: com.android.window.flags.enable_taskbar_recents_layout_transition Fix: 402683858 Test: go/testedequals Change-Id: Icc4a3a61b6c0997564a4dcdd8eb7768459d5f2ef
This commit is contained in:
@@ -372,6 +372,9 @@
|
||||
<dimen name="taskbar_running_app_indicator_height">2dp</dimen>
|
||||
<dimen name="taskbar_running_app_indicator_width">12dp</dimen>
|
||||
<dimen name="taskbar_running_app_indicator_top_margin">4dp</dimen>
|
||||
<dimen name="taskbar_minimized_app_indicator_width">6dp</dimen>
|
||||
<dimen name="taskbar_line_indicator_width_spring">20dp</dimen>
|
||||
<dimen name="taskbar_app_translate_y_spring">4dp</dimen>
|
||||
|
||||
<!-- Transient taskbar -->
|
||||
<dimen name="transient_taskbar_padding">12dp</dimen>
|
||||
|
||||
+276
@@ -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<BubbleTextView>("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<BubbleTextView>("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<BubbleTextView>("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<BubbleTextView, () -> 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
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
+175
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+17
-15
@@ -83,21 +83,23 @@ object TaskbarViewTestUtil {
|
||||
|
||||
/** Creates a list of fake recent tasks. */
|
||||
fun createRecents(size: Int): List<GroupTask> {
|
||||
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(),
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -445,6 +445,9 @@
|
||||
<dimen name="taskbar_running_app_indicator_height">0dp</dimen>
|
||||
<dimen name="taskbar_running_app_indicator_width">0dp</dimen>
|
||||
<dimen name="taskbar_running_app_indicator_top_margin">0dp</dimen>
|
||||
<dimen name="taskbar_minimized_app_indicator_width">0dp</dimen>
|
||||
<dimen name="taskbar_line_indicator_width_spring">0dp</dimen>
|
||||
<dimen name="taskbar_app_translate_y_spring">0dp</dimen>
|
||||
|
||||
<!-- Transient taskbar (placeholders to compile in Launcher3 without Quickstep) -->
|
||||
<dimen name="transient_taskbar_padding">0dp</dimen>
|
||||
|
||||
@@ -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<BubbleTextView, Integer> 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<BubbleTextView, Float> 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,
|
||||
|
||||
@@ -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<View> mTranslationX;
|
||||
private final MultiPropertyFactory<View> mTranslationY;
|
||||
|
||||
Reference in New Issue
Block a user