Refactor: Update task container and thumbnail dimming progress

This commit updates the mechanism for dimming task containers and thumbnails in the recents view. These changes simplify the dimming logic and improve the performance of the recents view. It reduces the usage of Flows to propagate progress and animation states through the views and view models.

The following changes were made:

- The `TaskThumbnailViewModel` no longer provides a dimming level.
- The `TaskViewModel` now provides a `tintAmount` flow that represents the dimming level for a task.
- The `TaskContainer` and `TaskThumbnailView` now use the `tintAmount` flow to update their dimming level.
- The `TaskContainer` now also updates the dimming level based on the menu open progress.
- The `TaskThumbnailView` now uses a `MultiPropertyFactory` to manage the dimming level, allowing it to be controlled by both the tint amount and the menu open progress.

Bug: 390581380
Doc: go/launcher-overview-unified-taskviewmodel
Flag: com.android.launcher3.enable_refactor_task_thumbnail
Test: OverviewImageTest
Test: TaskViewModelTest
Test: TaskThumbnailViewModelImplTest
Test: TaskThumbnailViewScreenshotTest
Change-Id: I04d5dee534d56857b977d7f3ed6d4eda9f93be45
This commit is contained in:
Jordan Silva
2025-01-28 10:33:36 +00:00
parent fe4152e608
commit beb6122fbe
12 changed files with 74 additions and 63 deletions
@@ -178,8 +178,6 @@ class RecentsDependencies private constructor(private val appContext: Context) {
TaskThumbnailViewData::class.java -> TaskThumbnailViewData()
TaskThumbnailViewModel::class.java ->
TaskThumbnailViewModelImpl(
recentsViewData = inject(),
taskContainerData = inject(scopeId),
dispatcherProvider = inject(),
getThumbnailPositionUseCase = inject(),
splashAlphaUseCase = inject(scopeId),
@@ -57,6 +57,8 @@ class TaskViewModel(
}
.distinctUntilChanged()
val tintAmount: Flow<Float> = recentsViewData.tintAmount
val state: Flow<TaskTileUiState> =
taskIds
.flatMapLatest { ids ->
@@ -29,7 +29,9 @@ import android.widget.FrameLayout
import androidx.annotation.ColorInt
import androidx.core.view.isInvisible
import com.android.launcher3.Flags.enableDesktopExplodedView
import com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA
import com.android.launcher3.R
import com.android.launcher3.util.MultiPropertyFactory
import com.android.launcher3.util.ViewPool
import com.android.launcher3.util.coroutines.DispatcherProvider
import com.android.quickstep.recents.di.RecentsDependencies
@@ -70,11 +72,13 @@ class TaskThumbnailView : FrameLayout, ViewPool.Reusable {
private val thumbnailView: FixedSizeImageView by lazy { findViewById(R.id.task_thumbnail) }
private val splashBackground: View by lazy { findViewById(R.id.splash_background) }
private val splashIcon: FixedSizeImageView by lazy { findViewById(R.id.splash_icon) }
private val dimAlpha: MultiPropertyFactory<View> by lazy {
MultiPropertyFactory(scrimView, VIEW_ALPHA, ScrimViewAlpha.entries.size, ::maxOf)
}
private var taskThumbnailViewHeader: TaskThumbnailViewHeader? = null
private var uiState: TaskThumbnailUiState = Uninitialized
private val bounds = Rect()
var cornerRadius: Float = 0f
@@ -108,11 +112,6 @@ class TaskThumbnailView : FrameLayout, ViewPool.Reusable {
viewData = RecentsDependencies.get(this)
updateViewDataValues()
viewModel = RecentsDependencies.get(this)
viewModel.dimProgress
.dropWhile { it == 0f }
.flowOn(dispatcherProvider.background)
.onEach { dimProgress -> scrimView.alpha = dimProgress }
.launchIn(viewAttachedScope)
viewModel.splashAlpha
.dropWhile { it == 0f }
.flowOn(dispatcherProvider.background)
@@ -164,6 +163,14 @@ class TaskThumbnailView : FrameLayout, ViewPool.Reusable {
}
}
fun updateTintAmount(tintAmount: Float) {
dimAlpha[ScrimViewAlpha.TintAmount.ordinal].value = tintAmount
}
fun updateMenuOpenProgress(progress: Float) {
dimAlpha[ScrimViewAlpha.MenuProgress.ordinal].value = progress * MAX_SCRIM_ALPHA
}
private fun updateViewDataValues() {
viewData.width.value = width
viewData.height.value = height
@@ -248,5 +255,11 @@ class TaskThumbnailView : FrameLayout, ViewPool.Reusable {
private companion object {
const val TAG = "TaskThumbnailView"
private const val MAX_SCRIM_ALPHA = 0.4f
enum class ScrimViewAlpha {
MenuProgress,
TintAmount,
}
}
}
@@ -19,7 +19,5 @@ package com.android.quickstep.task.viewmodel
import kotlinx.coroutines.flow.MutableStateFlow
class TaskContainerData {
val taskMenuOpenProgress = MutableStateFlow(0f)
val thumbnailSplashProgress = MutableStateFlow(0f)
}
@@ -21,9 +21,6 @@ import kotlinx.coroutines.flow.Flow
/** ViewModel for representing TaskThumbnails */
interface TaskThumbnailViewModel {
/** Provides the level of dimming that the View should have */
val dimProgress: Flow<Float>
/** Provides the alpha of the splash icon */
val splashAlpha: Flow<Float>
@@ -22,21 +22,15 @@ import android.util.Log
import com.android.launcher3.util.coroutines.DispatcherProvider
import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
import com.android.quickstep.recents.usecase.ThumbnailPositionState
import com.android.quickstep.recents.viewmodel.RecentsViewData
import com.android.quickstep.task.thumbnail.SplashAlphaUseCase
import kotlin.math.max
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
@OptIn(ExperimentalCoroutinesApi::class)
class TaskThumbnailViewModelImpl(
recentsViewData: RecentsViewData,
taskContainerData: TaskContainerData,
dispatcherProvider: DispatcherProvider,
private val getThumbnailPositionUseCase: GetThumbnailPositionUseCase,
private val splashAlphaUseCase: SplashAlphaUseCase,
@@ -44,14 +38,6 @@ class TaskThumbnailViewModelImpl(
private val splashProgress = MutableStateFlow(flowOf(0f))
private var taskId: Int = INVALID_TASK_ID
override val dimProgress: Flow<Float> =
combine(taskContainerData.taskMenuOpenProgress, recentsViewData.tintAmount) {
taskMenuOpenProgress,
tintAmount ->
max(taskMenuOpenProgress * MAX_SCRIM_ALPHA, tintAmount)
}
.flowOn(dispatcherProvider.background)
override val splashAlpha =
splashProgress.flatMapLatest { it }.flowOn(dispatcherProvider.background)
@@ -71,7 +57,6 @@ class TaskThumbnailViewModelImpl(
}
private companion object {
const val MAX_SCRIM_ALPHA = 0.4f
const val TAG = "TaskThumbnailViewModel"
}
}
@@ -148,7 +148,7 @@ class TaskContainer(
thumbnailView.destroyScopes()
}
fun bindThumbnailView() {
private fun bindThumbnailView() {
taskThumbnailViewModel.bind(task.key.id)
}
@@ -169,4 +169,12 @@ class TaskContainer(
fun setState(state: TaskData?, liveTile: Boolean, hasHeader: Boolean) {
thumbnailView.setState(TaskUiStateMapper.toTaskThumbnailUiState(state, liveTile, hasHeader))
}
fun updateTintAmount(tintAmount: Float) {
thumbnailView.updateTintAmount(tintAmount)
}
fun updateMenuOpenProgress(progress: Float) {
thumbnailView.updateMenuOpenProgress(progress)
}
}
@@ -368,8 +368,7 @@ public class TaskMenuView extends AbstractFloatingView {
mRevealAnimator.addUpdateListener(animation -> {
float animatedFraction = animation.getAnimatedFraction();
float openProgress = closing ? (1 - animatedFraction) : animatedFraction;
mTaskContainer.getTaskContainerData()
.getTaskMenuOpenProgress().setValue(openProgress);
mTaskContainer.updateMenuOpenProgress(openProgress);
});
} else {
openCloseAnimatorBuilder.with(ObjectAnimator.ofFloat(
@@ -99,6 +99,8 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
/** A task in the Recents view. */
@@ -740,6 +742,9 @@ constructor(
// The TaskView lifecycle is starts the ViewModel during onBind, and cleans it in
// onRecycle. So it should be initialized at this point. TaskView Lifecycle:
// `bind` -> `onBind` -> onAttachedToWindow() -> onDetachFromWindow -> onRecycle
coroutineJobs +=
viewModel!!.tintAmount.onEach(::updateTintAmount).launchIn(coroutineScope)
coroutineJobs +=
coroutineScope.launch {
viewModel!!.state.collectLatest(::updateTaskContainerState)
@@ -747,6 +752,10 @@ constructor(
}
}
private fun updateTintAmount(amount: Float) {
taskContainers.forEach { it.updateTintAmount(amount) }
}
private fun updateTaskContainerState(state: TaskTileUiState) {
val mapOfTasks = state.tasks.associateBy { it.taskId }
taskContainers.forEach { container ->
@@ -21,7 +21,6 @@ import com.android.quickstep.task.viewmodel.TaskThumbnailViewModel
import kotlinx.coroutines.flow.MutableStateFlow
class FakeTaskThumbnailViewModel : TaskThumbnailViewModel {
override val dimProgress = MutableStateFlow(0f)
override val splashAlpha = MutableStateFlow(0f)
override fun bind(taskId: Int) {
@@ -84,6 +84,40 @@ class TaskThumbnailViewScreenshotTest(emulationSpec: DeviceEmulationSpec) {
}
}
@Test
fun taskThumbnailView_dimmed_tintAmount() {
screenshotRule.screenshotTest("taskThumbnailView_dimmed_40") { activity ->
activity.actionBar?.hide()
createTaskThumbnailView(activity).apply {
setState(BackgroundOnly(Color.YELLOW))
updateTintAmount(.4f)
}
}
}
@Test
fun taskThumbnailView_dimmed_menuOpen() {
screenshotRule.screenshotTest("taskThumbnailView_dimmed_40") { activity ->
activity.actionBar?.hide()
createTaskThumbnailView(activity).apply {
setState(BackgroundOnly(Color.YELLOW))
updateMenuOpenProgress(1f)
}
}
}
@Test
fun taskThumbnailView_dimmed_tintAmountAndMenuOpen() {
screenshotRule.screenshotTest("taskThumbnailView_dimmed_80") { activity ->
activity.actionBar?.hide()
createTaskThumbnailView(activity).apply {
setState(BackgroundOnly(Color.YELLOW))
updateTintAmount(.8f)
updateMenuOpenProgress(1f)
}
}
}
private fun createTaskThumbnailView(context: Context): TaskThumbnailView {
val di = RecentsDependencies.initialize(context)
val taskThumbnailView =
@@ -23,11 +23,8 @@ import com.android.launcher3.util.TestDispatcherProvider
import com.android.quickstep.recents.usecase.GetThumbnailPositionUseCase
import com.android.quickstep.recents.usecase.ThumbnailPositionState.MatrixScaling
import com.android.quickstep.recents.usecase.ThumbnailPositionState.MissingThumbnail
import com.android.quickstep.recents.viewmodel.RecentsViewData
import com.android.quickstep.task.viewmodel.TaskContainerData
import com.android.quickstep.task.viewmodel.TaskThumbnailViewModelImpl
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
@@ -45,16 +42,12 @@ class TaskThumbnailViewModelImplTest {
private val dispatcher = StandardTestDispatcher()
private val testScope = TestScope(dispatcher)
private val recentsViewData = RecentsViewData()
private val taskContainerData = TaskContainerData()
private val dispatcherProvider = TestDispatcherProvider(dispatcher)
private val mGetThumbnailPositionUseCase = mock<GetThumbnailPositionUseCase>()
private val splashAlphaUseCase: SplashAlphaUseCase = mock()
private val systemUnderTest by lazy {
TaskThumbnailViewModelImpl(
recentsViewData,
taskContainerData,
dispatcherProvider,
mGetThumbnailPositionUseCase,
splashAlphaUseCase,
@@ -93,30 +86,6 @@ class TaskThumbnailViewModelImplTest {
.isEqualTo(MATRIX)
}
@Test
fun getForegroundScrimDimProgress_returnsForegroundMaxScrim() =
testScope.runTest {
recentsViewData.tintAmount.value = 0.32f
taskContainerData.taskMenuOpenProgress.value = 0f
assertThat(systemUnderTest.dimProgress.first()).isEqualTo(0.32f)
}
@Test
fun getTaskMenuScrimDimProgress_returnsTaskMenuScrim() =
testScope.runTest {
recentsViewData.tintAmount.value = 0f
taskContainerData.taskMenuOpenProgress.value = 1f
assertThat(systemUnderTest.dimProgress.first()).isEqualTo(0.4f)
}
@Test
fun getForegroundScrimDimProgress_returnsNoScrim() =
testScope.runTest {
recentsViewData.tintAmount.value = 0f
taskContainerData.taskMenuOpenProgress.value = 0f
assertThat(systemUnderTest.dimProgress.first()).isEqualTo(0f)
}
private companion object {
const val CANVAS_WIDTH = 300
const val CANVAS_HEIGHT = 600