Merge "Clock Settings selection animation (1/2)" into udc-dev am: 701f7067cc am: bb135f7e66

Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/apps/ThemePicker/+/22258943

Change-Id: I0a3d2429743692c05232c6b5da461f2fe2113951
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/res/layout/color_option_2.xml b/res/layout/color_option_2.xml
index 8c9c966..dcedaa3 100644
--- a/res/layout/color_option_2.xml
+++ b/res/layout/color_option_2.xml
@@ -16,7 +16,8 @@
 <!-- Content description is set programmatically on the parent FrameLayout -->
 <LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="wrap_content"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="@dimen/option_item_size"
     android:layout_height="wrap_content"
     android:orientation="vertical"
     android:clipChildren="false">
@@ -86,5 +87,16 @@
                 android:importantForAccessibility="no" />
         </FrameLayout>
     </FrameLayout>
+
+    <TextView
+        android:id="@+id/text"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="@dimen/option_bottom_margin"
+        android:textColor="@color/text_color_primary"
+        android:visibility="gone"
+        android:gravity="center"
+        android:text="Placeholder for stable size calculation, please do not remove."
+        tools:ignore="HardcodedText" />
 </LinearLayout>
 
diff --git a/res/layout/fragment_clock_settings.xml b/res/layout/fragment_clock_settings.xml
index 5208222..58232a7 100644
--- a/res/layout/fragment_clock_settings.xml
+++ b/res/layout/fragment_clock_settings.xml
@@ -57,11 +57,13 @@
         android:layout_marginBottom="28dp"
         android:background="@drawable/picker_fragment_background"
         android:paddingTop="22dp"
-        android:paddingBottom="62dp">
+        android:paddingBottom="36dp"
+        android:clipChildren="false">
 
         <FrameLayout
             android:layout_width="wrap_content"
-            android:layout_height="wrap_content">
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="22dp">
 
             <androidx.recyclerview.widget.RecyclerView
                 android:id="@+id/tabs"
@@ -90,25 +92,28 @@
         <FrameLayout
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            android:paddingTop="16dp">
+            android:clipChildren="false">
 
             <LinearLayout
                 android:id="@+id/color_picker_container"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
-                android:orientation="vertical">
+                android:orientation="vertical"
+                android:clipChildren="false">
 
                 <FrameLayout
                     android:layout_width="match_parent"
                     android:layout_height="wrap_content"
-                    android:layout_marginBottom="16dp">
+                    android:layout_marginBottom="16dp"
+                    android:clipChildren="false">
 
                     <androidx.recyclerview.widget.RecyclerView
                         android:id="@+id/color_options"
                         android:layout_width="match_parent"
                         android:layout_height="wrap_content"
                         android:clipToPadding="false"
-                        android:paddingHorizontal="16dp" />
+                        android:paddingHorizontal="16dp"
+                        android:clipChildren="false" />
 
                     <!--
                     This is just an invisible placeholder put in place so that the parent keeps its
@@ -118,7 +123,7 @@
                     without changing its size after the content is loaded into the RecyclerView.
                     -->
                     <include
-                        layout="@layout/color_option_with_background"
+                        layout="@layout/color_option_2"
                         android:layout_width="wrap_content"
                         android:layout_height="wrap_content"
                         android:visibility="invisible" />
diff --git a/src/com/android/customization/picker/clock/ui/binder/ClockSettingsBinder.kt b/src/com/android/customization/picker/clock/ui/binder/ClockSettingsBinder.kt
index c0b7403..1bbb965 100644
--- a/src/com/android/customization/picker/clock/ui/binder/ClockSettingsBinder.kt
+++ b/src/com/android/customization/picker/clock/ui/binder/ClockSettingsBinder.kt
@@ -33,9 +33,11 @@
 import com.android.customization.picker.clock.ui.view.ClockSizeRadioButtonGroup
 import com.android.customization.picker.clock.ui.view.ClockViewFactory
 import com.android.customization.picker.clock.ui.viewmodel.ClockSettingsViewModel
-import com.android.customization.picker.color.ui.adapter.ColorOptionAdapter
+import com.android.customization.picker.color.ui.binder.ColorOptionIconBinder
+import com.android.customization.picker.color.ui.viewmodel.ColorOptionIconViewModel
 import com.android.customization.picker.common.ui.view.ItemSpacing
 import com.android.wallpaper.R
+import com.android.wallpaper.picker.option.ui.adapter.OptionItemAdapter
 import kotlinx.coroutines.flow.mapNotNull
 import kotlinx.coroutines.launch
 
@@ -56,7 +58,15 @@
         tabView.addItemDecoration(ItemSpacing(ItemSpacing.TAB_ITEM_SPACING_DP))
 
         val colorOptionContainerView: RecyclerView = view.requireViewById(R.id.color_options)
-        val colorOptionAdapter = ColorOptionAdapter()
+        val colorOptionAdapter =
+            OptionItemAdapter(
+                layoutResourceId = R.layout.color_option_2,
+                lifecycleOwner = lifecycleOwner,
+                bindIcon = { foregroundView: View, colorIcon: ColorOptionIconViewModel ->
+                    val viewGroup = foregroundView as? ViewGroup
+                    viewGroup?.let { ColorOptionIconBinder.bind(viewGroup, colorIcon) }
+                }
+            )
         colorOptionContainerView.adapter = colorOptionAdapter
         colorOptionContainerView.layoutManager =
             LinearLayoutManager(view.context, RecyclerView.HORIZONTAL, false)
diff --git a/src/com/android/customization/picker/clock/ui/viewmodel/ClockSettingsViewModel.kt b/src/com/android/customization/picker/clock/ui/viewmodel/ClockSettingsViewModel.kt
index c3cd217..1514f1b 100644
--- a/src/com/android/customization/picker/clock/ui/viewmodel/ClockSettingsViewModel.kt
+++ b/src/com/android/customization/picker/clock/ui/viewmodel/ClockSettingsViewModel.kt
@@ -26,9 +26,12 @@
 import com.android.customization.picker.clock.shared.ClockSize
 import com.android.customization.picker.clock.shared.model.ClockMetadataModel
 import com.android.customization.picker.color.domain.interactor.ColorPickerInteractor
+import com.android.customization.picker.color.shared.model.ColorOptionModel
 import com.android.customization.picker.color.shared.model.ColorType
-import com.android.customization.picker.color.ui.viewmodel.ColorOptionViewModel
+import com.android.customization.picker.color.ui.viewmodel.ColorOptionIconViewModel
 import com.android.wallpaper.R
+import com.android.wallpaper.picker.common.text.ui.viewmodel.Text
+import com.android.wallpaper.picker.option.ui.viewmodel.OptionItemViewModel
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
@@ -38,8 +41,8 @@
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.mapLatest
 import kotlinx.coroutines.flow.merge
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.launch
@@ -108,50 +111,48 @@
     }
 
     @OptIn(ExperimentalCoroutinesApi::class)
-    val colorOptions: StateFlow<List<ColorOptionViewModel>> =
-        combine(colorPickerInteractor.colorOptions, clockPickerInteractor.selectedColorId, ::Pair)
-            .mapLatest { (colorOptions, selectedColorId) ->
-                // Use mapLatest and delay(100) here to prevent too many selectedClockColor update
-                // events from ClockRegistry upstream, caused by sliding the saturation level bar.
-                delay(COLOR_OPTIONS_EVENT_UPDATE_DELAY_MILLIS)
-                buildList {
-                    val defaultThemeColorOptionViewModel =
-                        (colorOptions[ColorType.WALLPAPER_COLOR]
-                                ?.find { it.isSelected }
-                                ?.colorOption as? ColorSeedOption)
-                            ?.toColorOptionViewModel(
-                                context,
-                                selectedColorId,
-                            )
-                            ?: (colorOptions[ColorType.PRESET_COLOR]
-                                    ?.find { it.isSelected }
-                                    ?.colorOption as? ColorBundle)
-                                ?.toColorOptionViewModel(
-                                    context,
-                                    selectedColorId,
-                                )
-                    if (defaultThemeColorOptionViewModel != null) {
-                        add(defaultThemeColorOptionViewModel)
-                    }
+    val colorOptions: Flow<List<OptionItemViewModel<ColorOptionIconViewModel>>> =
+        colorPickerInteractor.colorOptions.map { colorOptions ->
+            // Use mapLatest and delay(100) here to prevent too many selectedClockColor update
+            // events from ClockRegistry upstream, caused by sliding the saturation level bar.
+            delay(COLOR_OPTIONS_EVENT_UPDATE_DELAY_MILLIS)
+            buildList {
+                val defaultThemeColorOptionViewModel =
+                    (colorOptions[ColorType.WALLPAPER_COLOR]?.find { it.isSelected })
+                        ?.toOptionItemViewModel(context)
+                        ?: (colorOptions[ColorType.PRESET_COLOR]?.find { it.isSelected })
+                            ?.toOptionItemViewModel(context)
+                if (defaultThemeColorOptionViewModel != null) {
+                    add(defaultThemeColorOptionViewModel)
+                }
 
-                    val selectedColorPosition = colorMap.keys.indexOf(selectedColorId)
-
-                    colorMap.values.forEachIndexed { index, colorModel ->
-                        val isSelected = selectedColorPosition == index
-                        val colorToneProgress = ClockMetadataModel.DEFAULT_COLOR_TONE_PROGRESS
-                        add(
-                            ColorOptionViewModel(
-                                color0 = colorModel.color,
-                                color1 = colorModel.color,
-                                color2 = colorModel.color,
-                                color3 = colorModel.color,
-                                contentDescription =
+                colorMap.values.forEachIndexed { index, colorModel ->
+                    val isSelectedFlow =
+                        selectedColorId
+                            .map { colorMap.keys.indexOf(it) == index }
+                            .stateIn(viewModelScope)
+                    val colorToneProgress = ClockMetadataModel.DEFAULT_COLOR_TONE_PROGRESS
+                    add(
+                        OptionItemViewModel<ColorOptionIconViewModel>(
+                            key = MutableStateFlow(colorModel.colorId) as StateFlow<String>,
+                            payload =
+                                ColorOptionIconViewModel(
+                                    colorModel.color,
+                                    colorModel.color,
+                                    colorModel.color,
+                                    colorModel.color,
+                                ),
+                            text =
+                                Text.Loaded(
                                     context.getString(
                                         R.string.content_description_color_option,
                                         index,
-                                    ),
-                                isSelected = isSelected,
-                                onClick =
+                                    )
+                                ),
+                            isTextUserVisible = false,
+                            isSelected = isSelectedFlow,
+                            onClicked =
+                                isSelectedFlow.map { isSelected ->
                                     if (isSelected) {
                                         null
                                     } else {
@@ -169,74 +170,64 @@
                                                     ),
                                             )
                                         }
-                                    },
-                            )
+                                    }
+                                },
                         )
-                    }
+                    )
                 }
             }
-            .stateIn(
-                scope = viewModelScope,
-                started = SharingStarted.WhileSubscribed(),
-                initialValue = emptyList(),
-            )
+        }
 
     @OptIn(ExperimentalCoroutinesApi::class)
     val selectedColorOptionPosition: Flow<Int> =
-        colorOptions.mapLatest { it.indexOfFirst { colorOption -> colorOption.isSelected } }
+        colorOptions.flatMapLatest { colorOptions ->
+            combine(colorOptions.map { colorOption -> colorOption.isSelected }) { selectedFlags ->
+                selectedFlags.indexOfFirst { it }
+            }
+        }
 
-    private fun ColorSeedOption.toColorOptionViewModel(
-        context: Context,
-        selectedColorId: String?,
-    ): ColorOptionViewModel {
-        val colors = previewInfo.resolveColors(context.resources)
-        return ColorOptionViewModel(
-            color0 = colors[0],
-            color1 = colors[1],
-            color2 = colors[2],
-            color3 = colors[3],
-            contentDescription = getContentDescription(context).toString(),
-            title = context.getString(R.string.default_theme_title),
-            isSelected = selectedColorId == null,
-            onClick =
-                if (selectedColorId == null) {
-                    null
-                } else {
-                    {
-                        clockPickerInteractor.setClockColor(
-                            selectedColorId = null,
-                            colorToneProgress = ClockMetadataModel.DEFAULT_COLOR_TONE_PROGRESS,
-                            seedColor = null,
-                        )
-                    }
-                },
-        )
-    }
-
-    private fun ColorBundle.toColorOptionViewModel(
-        context: Context,
-        selectedColorId: String?
-    ): ColorOptionViewModel {
-        val primaryColor = previewInfo.resolvePrimaryColor(context.resources)
-        val secondaryColor = previewInfo.resolveSecondaryColor(context.resources)
-        return ColorOptionViewModel(
-            color0 = primaryColor,
-            color1 = secondaryColor,
-            color2 = primaryColor,
-            color3 = secondaryColor,
-            contentDescription = getContentDescription(context).toString(),
-            title = context.getString(R.string.default_theme_title),
-            isSelected = selectedColorId == null,
-            onClick =
-                if (selectedColorId == null) {
-                    null
-                } else {
-                    {
-                        clockPickerInteractor.setClockColor(
-                            selectedColorId = null,
-                            colorToneProgress = ClockMetadataModel.DEFAULT_COLOR_TONE_PROGRESS,
-                            seedColor = null,
-                        )
+    private suspend fun ColorOptionModel.toOptionItemViewModel(
+        context: Context
+    ): OptionItemViewModel<ColorOptionIconViewModel> {
+        val optionItemPayload =
+            when (colorOption) {
+                is ColorSeedOption -> {
+                    val colors = colorOption.previewInfo.resolveColors(context.resources)
+                    ColorOptionIconViewModel(colors[0], colors[1], colors[2], colors[3])
+                }
+                is ColorBundle -> {
+                    val primaryColor =
+                        colorOption.previewInfo.resolvePrimaryColor(context.resources)
+                    val secondaryColor =
+                        colorOption.previewInfo.resolveSecondaryColor(context.resources)
+                    ColorOptionIconViewModel(
+                        primaryColor,
+                        secondaryColor,
+                        primaryColor,
+                        secondaryColor
+                    )
+                }
+                else -> null
+            }
+        val isSelectedFlow = selectedColorId.map { it == null }.stateIn(viewModelScope)
+        return OptionItemViewModel<ColorOptionIconViewModel>(
+            key = MutableStateFlow(key) as StateFlow<String>,
+            payload = optionItemPayload,
+            text = Text.Loaded(context.getString(R.string.default_theme_title)),
+            isTextUserVisible = true,
+            isSelected = isSelectedFlow,
+            onClicked =
+                isSelectedFlow.map { isSelected ->
+                    if (isSelected) {
+                        null
+                    } else {
+                        {
+                            clockPickerInteractor.setClockColor(
+                                selectedColorId = null,
+                                colorToneProgress = ClockMetadataModel.DEFAULT_COLOR_TONE_PROGRESS,
+                                seedColor = null,
+                            )
+                        }
                     }
                 },
         )
diff --git a/src/com/android/customization/picker/color/ui/viewmodel/ColorPickerViewModel.kt b/src/com/android/customization/picker/color/ui/viewmodel/ColorPickerViewModel.kt
index 81a5810..73c1ac6 100644
--- a/src/com/android/customization/picker/color/ui/viewmodel/ColorPickerViewModel.kt
+++ b/src/com/android/customization/picker/color/ui/viewmodel/ColorPickerViewModel.kt
@@ -129,6 +129,7 @@
                                                     .getContentDescription(context)
                                                     .toString()
                                             ),
+                                        isTextUserVisible = false,
                                         isSelected = isSelectedFlow,
                                         onClicked =
                                             isSelectedFlow.map { isSelected ->
@@ -183,6 +184,7 @@
                                                     .getContentDescription(context)
                                                     .toString()
                                             ),
+                                        isTextUserVisible = false,
                                         isSelected = isSelectedFlow,
                                         onClicked =
                                             isSelectedFlow.map { isSelected ->
diff --git a/tests/src/com/android/customization/picker/clock/ui/viewmodel/ClockSettingsViewModelTest.kt b/tests/src/com/android/customization/picker/clock/ui/viewmodel/ClockSettingsViewModelTest.kt
index d53288d..a959a46 100644
--- a/tests/src/com/android/customization/picker/clock/ui/viewmodel/ClockSettingsViewModelTest.kt
+++ b/tests/src/com/android/customization/picker/clock/ui/viewmodel/ClockSettingsViewModelTest.kt
@@ -88,15 +88,20 @@
         val observedSeedColor = collectLastValue(underTest.seedColor)
         // Advance COLOR_OPTIONS_EVENT_UPDATE_DELAY_MILLIS since there is a delay from colorOptions
         advanceTimeBy(ClockSettingsViewModel.COLOR_OPTIONS_EVENT_UPDATE_DELAY_MILLIS)
-        assertThat(observedClockColorOptions()!![0].isSelected).isTrue()
-        assertThat(observedClockColorOptions()!![0].onClick).isNull()
+        val option0IsSelected = collectLastValue(observedClockColorOptions()!![0].isSelected)
+        val option0OnClicked = collectLastValue(observedClockColorOptions()!![0].onClicked)
+        assertThat(option0IsSelected()).isTrue()
+        assertThat(option0OnClicked()).isNull()
         assertThat(observedSelectedColorOptionPosition()).isEqualTo(0)
 
-        observedClockColorOptions()!![1].onClick?.invoke()
+        val option1OnClickedBefore = collectLastValue(observedClockColorOptions()!![1].onClicked)
+        option1OnClickedBefore()?.invoke()
         // Advance COLOR_OPTIONS_EVENT_UPDATE_DELAY_MILLIS since there is a delay from colorOptions
         advanceTimeBy(ClockSettingsViewModel.COLOR_OPTIONS_EVENT_UPDATE_DELAY_MILLIS)
-        assertThat(observedClockColorOptions()!![1].isSelected).isTrue()
-        assertThat(observedClockColorOptions()!![1].onClick).isNull()
+        val option1IsSelected = collectLastValue(observedClockColorOptions()!![1].isSelected)
+        val option1OnClickedAfter = collectLastValue(observedClockColorOptions()!![1].onClicked)
+        assertThat(option1IsSelected()).isTrue()
+        assertThat(option1OnClickedAfter()).isNull()
         assertThat(observedSelectedColorOptionPosition()).isEqualTo(1)
         assertThat(observedSliderProgress())
             .isEqualTo(ClockMetadataModel.DEFAULT_COLOR_TONE_PROGRESS)
@@ -120,10 +125,12 @@
         val observedSeedColor = collectLastValue(underTest.seedColor)
         // Advance COLOR_OPTIONS_EVENT_UPDATE_DELAY_MILLIS since there is a delay from colorOptions
         advanceTimeBy(ClockSettingsViewModel.COLOR_OPTIONS_EVENT_UPDATE_DELAY_MILLIS)
-        assertThat(observedClockColorOptions()!![0].isSelected).isTrue()
+        val option0IsSelected = collectLastValue(observedClockColorOptions()!![0].isSelected)
+        assertThat(option0IsSelected()).isTrue()
         assertThat(observedIsSliderEnabled()).isFalse()
 
-        observedClockColorOptions()!![1].onClick?.invoke()
+        val option1OnClicked = collectLastValue(observedClockColorOptions()!![1].onClicked)
+        option1OnClicked()?.invoke()
 
         // Advance COLOR_OPTIONS_EVENT_UPDATE_DELAY_MILLIS since there is a delay from colorOptions
         advanceTimeBy(ClockSettingsViewModel.COLOR_OPTIONS_EVENT_UPDATE_DELAY_MILLIS)