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:
@@ -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 ->
|
||||
|
||||
-1
@@ -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) {
|
||||
|
||||
+34
@@ -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 =
|
||||
|
||||
-31
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user