Add corner rounding to TaskThumbnailView

Fix: 334826840
Test: TaskThumbnailViewModelTest
Flag: ACONFIG com.android.launcher3.enable_refactor_task_thumbnail DEVELOPMENT
Change-Id: Iba4d49d43abc09363f61186c3fcc07f2281b7006
This commit is contained in:
Uwais Ashraf
2024-05-03 18:29:59 +00:00
parent 906df388ef
commit 9533b0fb27
7 changed files with 188 additions and 42 deletions
@@ -0,0 +1,27 @@
/*
* Copyright (C) 2024 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.quickstep.recents.viewmodel
import kotlinx.coroutines.flow.MutableStateFlow
// This is far from complete but serves the purpose of enabling refactoring in other areas
class RecentsViewData {
val fullscreenProgress = MutableStateFlow(1f)
// This is typically a View concern but it is used to invalidate rendering in other Views
val scale = MutableStateFlow(1f)
}
@@ -17,22 +17,44 @@
package com.android.quickstep.task.thumbnail
import android.content.Context
import android.content.res.Configuration
import android.graphics.Canvas
import android.graphics.Outline
import android.graphics.Paint
import android.graphics.PorterDuff
import android.graphics.PorterDuffXfermode
import android.util.AttributeSet
import android.view.View
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.*
import android.view.ViewOutlineProvider
import com.android.launcher3.Utilities
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
import com.android.quickstep.util.TaskCornerRadius
import com.android.quickstep.views.RecentsView
import com.android.quickstep.views.RecentsViewContainer
import com.android.quickstep.views.TaskView
import com.android.systemui.shared.system.QuickStepContract
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
class TaskThumbnailView : View {
// TODO(b/335649589): Ideally create and obtain this from DI. This ViewModel should be scoped
// to [TaskView], and also shared between [TaskView] and [TaskThumbnailView]
val viewModel = TaskThumbnailViewModel()
// This is using a lazy for now because the dependencies cannot be obtained without DI.
val viewModel by lazy {
TaskThumbnailViewModel(
RecentsViewContainer.containerFromContext<RecentsViewContainer>(context)
.getOverviewPanel<RecentsView<*, *>>()
.mRecentsViewData,
(parent as TaskView).mTaskViewData
)
}
private var uiState: TaskThumbnailUiState = Uninitialized
private var inheritedScale: Float = 1f
private var cornerRadius: Float = TaskCornerRadius.get(context)
private var fullscreenCornerRadius: Float = QuickStepContract.getWindowCornerRadius(context)
constructor(context: Context?) : super(context)
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
@@ -51,6 +73,27 @@ class TaskThumbnailView : View {
invalidate()
}
}
MainScope().launch { viewModel.recentsFullscreenProgress.collect { invalidateOutline() } }
MainScope().launch {
viewModel.inheritedScale.collect { viewModelInheritedScale ->
inheritedScale = viewModelInheritedScale
invalidateOutline()
}
}
clipToOutline = true
outlineProvider =
object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
outline.setRoundRect(
0,
0,
view.measuredWidth,
view.measuredHeight,
getCurrentCornerRadius()
)
}
}
}
override fun onDraw(canvas: Canvas) {
@@ -60,19 +103,25 @@ class TaskThumbnailView : View {
}
}
private fun drawTransparentUiState(canvas: Canvas) {
canvas.drawRoundRect(
0f,
0f,
measuredWidth.toFloat(),
measuredHeight.toFloat(),
// TODO(b/334826840) add rounded corners
0f,
0f,
CLEAR_PAINT
)
override fun onConfigurationChanged(newConfig: Configuration?) {
super.onConfigurationChanged(newConfig)
cornerRadius = TaskCornerRadius.get(context)
fullscreenCornerRadius = QuickStepContract.getWindowCornerRadius(context)
invalidateOutline()
}
private fun drawTransparentUiState(canvas: Canvas) {
canvas.drawRect(0f, 0f, measuredWidth.toFloat(), measuredHeight.toFloat(), CLEAR_PAINT)
}
private fun getCurrentCornerRadius() =
Utilities.mapRange(
viewModel.recentsFullscreenProgress.value,
cornerRadius,
fullscreenCornerRadius
) / inheritedScale
companion object {
private val CLEAR_PAINT =
Paint().apply { xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR) }
@@ -16,20 +16,32 @@
package com.android.quickstep.task.thumbnail
import com.android.quickstep.recents.viewmodel.RecentsViewData
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile
import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized
import com.android.quickstep.task.viewmodel.TaskViewData
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
class TaskThumbnailViewModel {
private val _uiState: MutableStateFlow<TaskThumbnailUiState> =
MutableStateFlow(TaskThumbnailUiState.Uninitialized)
val uiState: StateFlow<TaskThumbnailUiState> = _uiState
class TaskThumbnailViewModel(recentsViewData: RecentsViewData, taskViewData: TaskViewData) {
private val task = MutableStateFlow<TaskThumbnail?>(null)
val recentsFullscreenProgress = recentsViewData.fullscreenProgress
val inheritedScale =
combine(recentsViewData.scale, taskViewData.scale) { recentsScale, taskScale ->
recentsScale * taskScale
}
val uiState =
task.map { taskVal ->
when {
taskVal == null -> Uninitialized
taskVal.isRunning -> LiveTile
else -> Uninitialized
}
}
fun bind(task: TaskThumbnail) {
_uiState.value =
if (task.isRunning) {
TaskThumbnailUiState.LiveTile
} else {
TaskThumbnailUiState.Uninitialized
}
this.task.value = task
}
}
@@ -0,0 +1,24 @@
/*
* Copyright (C) 2024 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.quickstep.task.viewmodel
import kotlinx.coroutines.flow.MutableStateFlow
class TaskViewData {
// This is typically a View concern but it is used to invalidate rendering in other Views
val scale = MutableStateFlow(1f)
}
@@ -35,6 +35,7 @@ import static com.android.launcher3.AbstractFloatingView.TYPE_TASK_MENU;
import static com.android.launcher3.AbstractFloatingView.getTopOpenViewWithType;
import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS;
import static com.android.launcher3.Flags.enableGridOnlyOverview;
import static com.android.launcher3.Flags.enableRefactorTaskThumbnail;
import static com.android.launcher3.LauncherAnimUtils.SUCCESS_TRANSITION_PROGRESS;
import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA;
import static com.android.launcher3.LauncherState.BACKGROUND_APP;
@@ -186,6 +187,7 @@ import com.android.quickstep.TaskViewUtils;
import com.android.quickstep.TopTaskTracker;
import com.android.quickstep.ViewUtils;
import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
import com.android.quickstep.recents.viewmodel.RecentsViewData;
import com.android.quickstep.util.ActiveGestureErrorDetector;
import com.android.quickstep.util.ActiveGestureLog;
import com.android.quickstep.util.AnimUtils;
@@ -376,6 +378,9 @@ public abstract class RecentsView<CONTAINER_TYPE extends Context & RecentsViewCo
public void setValue(RecentsView view, float scale) {
view.setScaleX(scale);
view.setScaleY(scale);
if (enableRefactorTaskThumbnail()) {
view.mRecentsViewData.getScale().setValue(scale);
}
view.mLastComputedTaskStartPushOutDistance = null;
view.mLastComputedTaskEndPushOutDistance = null;
view.runActionOnRemoteHandles(new Consumer<RemoteTargetHandle>() {
@@ -446,6 +451,8 @@ public abstract class RecentsView<CONTAINER_TYPE extends Context & RecentsViewCo
private static final float FOREGROUND_SCRIM_TINT = 0.32f;
public final RecentsViewData mRecentsViewData = new RecentsViewData();
protected final RecentsOrientedState mOrientationState;
protected final BaseContainerInterface<STATE_TYPE, CONTAINER_TYPE> mSizeStrategy;
@Nullable
@@ -2012,6 +2019,9 @@ public abstract class RecentsView<CONTAINER_TYPE extends Context & RecentsViewCo
public void setFullscreenProgress(float fullscreenProgress) {
mFullscreenProgress = fullscreenProgress;
if (enableRefactorTaskThumbnail()) {
mRecentsViewData.getFullscreenProgress().setValue(mFullscreenProgress);
}
int taskCount = getTaskViewCount();
for (int i = 0; i < taskCount; i++) {
requireTaskViewAt(i).setFullscreenProgress(mFullscreenProgress);
@@ -107,6 +107,7 @@ import com.android.quickstep.TaskViewUtils;
import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
import com.android.quickstep.task.thumbnail.TaskThumbnail;
import com.android.quickstep.task.thumbnail.TaskThumbnailView;
import com.android.quickstep.task.viewmodel.TaskViewData;
import com.android.quickstep.util.ActiveGestureLog;
import com.android.quickstep.util.BorderAnimator;
import com.android.quickstep.util.RecentsOrientedState;
@@ -326,6 +327,7 @@ public class TaskView extends FrameLayout implements Reusable {
}
};
public TaskViewData mTaskViewData = new TaskViewData();
protected TaskThumbnailViewDeprecated mTaskThumbnailViewDeprecated;
protected TaskThumbnailView mTaskThumbnailView;
protected TaskViewIcon mIconView;
@@ -1462,6 +1464,9 @@ public class TaskView extends FrameLayout implements Reusable {
scale *= mDismissScale;
setScaleX(scale);
setScaleY(scale);
if (enableRefactorTaskThumbnail()) {
mTaskViewData.getScale().setValue(scale);
}
updateSnapshotRadius();
}
@@ -1768,10 +1773,7 @@ public class TaskView extends FrameLayout implements Reusable {
progress = Utilities.boundToRange(progress, 0, 1);
mFullscreenProgress = progress;
mIconView.setVisibility(progress < 1 ? VISIBLE : INVISIBLE);
if (!enableRefactorTaskThumbnail()) {
// TODO(b/334826840) Add corner rounding to new TTV
mTaskThumbnailViewDeprecated.getTaskOverlay().setFullscreenProgress(progress);
}
mTaskThumbnailViewDeprecated.getTaskOverlay().setFullscreenProgress(progress);
RecentsView recentsView = mContainer.getOverviewPanel();
// Animate icons and DWB banners in/out, except in QuickSwitch state, when tiles are
@@ -1785,10 +1787,7 @@ public class TaskView extends FrameLayout implements Reusable {
protected void updateSnapshotRadius() {
updateCurrentFullscreenParams();
if (!enableRefactorTaskThumbnail()) {
// TODO(b/334826840) Add corner rounding to new TTV
mTaskThumbnailViewDeprecated.setFullscreenParams(mCurrentFullscreenParams);
}
mTaskThumbnailViewDeprecated.setFullscreenParams(mCurrentFullscreenParams);
}
void updateCurrentFullscreenParams() {
@@ -1799,8 +1798,8 @@ public class TaskView extends FrameLayout implements Reusable {
if (getRecentsView() == null) {
return;
}
fullscreenParams.setProgress(mFullscreenProgress, getRecentsView().getScaleX(),
getScaleX());
fullscreenParams.setProgress(
mFullscreenProgress, getRecentsView().getScaleX(), getScaleX());
}
/**
@@ -17,38 +17,63 @@
package com.android.quickstep.task.thumbnail
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.quickstep.recents.viewmodel.RecentsViewData
import com.android.quickstep.task.viewmodel.TaskViewData
import com.android.systemui.shared.recents.model.Task
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class TaskThumbnailViewModelTest {
private val systemUnderTest = TaskThumbnailViewModel()
private val recentsViewData = RecentsViewData()
private val taskViewData = TaskViewData()
private val systemUnderTest = TaskThumbnailViewModel(recentsViewData, taskViewData)
@Test
fun initialStateIsUninitialized() {
assertThat(systemUnderTest.uiState.value).isEqualTo(TaskThumbnailUiState.Uninitialized)
fun initialStateIsUninitialized() = runTest {
assertThat(systemUnderTest.uiState.first()).isEqualTo(TaskThumbnailUiState.Uninitialized)
}
@Test
fun bindRunningTask_thenStateIs_LiveTile() {
fun bindRunningTask_thenStateIs_LiveTile() = runTest {
val taskThumbnail = TaskThumbnail(Task(), isRunning = true)
systemUnderTest.bind(taskThumbnail)
assertThat(systemUnderTest.uiState.value).isEqualTo(TaskThumbnailUiState.LiveTile)
assertThat(systemUnderTest.uiState.first()).isEqualTo(TaskThumbnailUiState.LiveTile)
}
@Test
fun bindRunningTaskThenStoppedTask_thenStateIs_Uninitialized() {
fun setRecentsFullscreenProgress_thenProgressIsPassedThrough() = runTest {
recentsViewData.fullscreenProgress.value = 0.5f
assertThat(systemUnderTest.recentsFullscreenProgress.first()).isEqualTo(0.5f)
recentsViewData.fullscreenProgress.value = 0.6f
assertThat(systemUnderTest.recentsFullscreenProgress.first()).isEqualTo(0.6f)
}
@Test
fun setAncestorScales_thenScaleIsCalculated() = runTest {
recentsViewData.scale.value = 0.5f
taskViewData.scale.value = 0.6f
assertThat(systemUnderTest.inheritedScale.first()).isEqualTo(0.3f)
}
@Test
fun bindRunningTaskThenStoppedTask_thenStateIs_Uninitialized() = runTest {
// TODO(b/334825222): Change the expectation here when snapshot state is implemented
val task = Task()
val runningTask = TaskThumbnail(task, isRunning = true)
val stoppedTask = TaskThumbnail(task, isRunning = false)
systemUnderTest.bind(runningTask)
assertThat(systemUnderTest.uiState.value).isEqualTo(TaskThumbnailUiState.LiveTile)
assertThat(systemUnderTest.uiState.first()).isEqualTo(TaskThumbnailUiState.LiveTile)
systemUnderTest.bind(stoppedTask)
assertThat(systemUnderTest.uiState.value).isEqualTo(TaskThumbnailUiState.Uninitialized)
assertThat(systemUnderTest.uiState.first()).isEqualTo(TaskThumbnailUiState.Uninitialized)
}
}