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:
@@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+33
-8
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user