Introduce small clock carousel

The carousel will show small clocks when clock size set to Small
Otherwise, we show large clocks

Test: Manually tested that small clocks show on the carousel. See bug
Bug: 274927017
Change-Id: I4b859151ab370a8a4b81c859f3b020e1e8e33fbb
diff --git a/src/com/android/customization/picker/clock/ui/binder/ClockCarouselViewBinder.kt b/src/com/android/customization/picker/clock/ui/binder/ClockCarouselViewBinder.kt
index 5323cf4..98c84f2 100644
--- a/src/com/android/customization/picker/clock/ui/binder/ClockCarouselViewBinder.kt
+++ b/src/com/android/customization/picker/clock/ui/binder/ClockCarouselViewBinder.kt
@@ -27,6 +27,7 @@
 import com.android.customization.picker.clock.ui.view.ClockViewFactory
 import com.android.customization.picker.clock.ui.viewmodel.ClockCarouselViewModel
 import com.android.wallpaper.R
+import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.launch
 
 object ClockCarouselViewBinder {
@@ -40,6 +41,7 @@
         lifecycleOwner: LifecycleOwner,
         hideSmartspace: (Boolean) -> Unit,
     ) {
+        carouselView.setClockViewFactory(clockViewFactory)
         val singleClockHostView =
             singleClockView.requireViewById<FrameLayout>(R.id.single_clock_host_view)
         lifecycleOwner.lifecycleScope.launch {
@@ -47,12 +49,11 @@
                 launch { viewModel.isCarouselVisible.collect { carouselView.isVisible = it } }
 
                 launch {
-                    viewModel.allClockIds.collect { allClockIds ->
+                    combine(viewModel.selectedClockSize, viewModel.allClockIds, ::Pair).collect {
+                        (size, allClockIds) ->
                         carouselView.setUpClockCarouselView(
+                            clockSize = size,
                             clockIds = allClockIds,
-                            onGetClockController = { clockId ->
-                                clockViewFactory.getController(clockId)
-                            },
                             onClockSelected = { clockId ->
                                 viewModel.setSelectedClock(clockId)
                                 val hasCustomWeatherDataDisplay =
@@ -61,7 +62,6 @@
                                         .largeClock
                                         .config
                                         .hasCustomWeatherDataDisplay
-
                                 hideSmartspace(hasCustomWeatherDataDisplay)
                             },
                         )
diff --git a/src/com/android/customization/picker/clock/ui/view/ClockCarouselView.kt b/src/com/android/customization/picker/clock/ui/view/ClockCarouselView.kt
index 46a2801..04ad81f 100644
--- a/src/com/android/customization/picker/clock/ui/view/ClockCarouselView.kt
+++ b/src/com/android/customization/picker/clock/ui/view/ClockCarouselView.kt
@@ -23,6 +23,10 @@
 import android.widget.FrameLayout
 import androidx.constraintlayout.helper.widget.Carousel
 import androidx.constraintlayout.motion.widget.MotionLayout
+import androidx.core.view.doOnPreDraw
+import androidx.core.view.get
+import androidx.core.view.isNotEmpty
+import com.android.customization.picker.clock.shared.ClockSize
 import com.android.systemui.plugins.ClockController
 import com.android.wallpaper.R
 import java.lang.Float.max
@@ -39,12 +43,15 @@
     val carousel: Carousel
     private val motionLayout: MotionLayout
     private lateinit var adapter: ClockCarouselAdapter
-    private var scalingUpClockController: ClockController? = null
-    private var scalingDownClockController: ClockController? = null
-    private var scalingUpClockView: View? = null
-    private var scalingDownClockView: View? = null
-    private var showingCardView: View? = null
-    private var hidingCardView: View? = null
+    private lateinit var clockViewFactory: ClockViewFactory
+    private var toCenterClockController: ClockController? = null
+    private var offCenterClockController: ClockController? = null
+    private var toCenterClockView: View? = null
+    private var offCenterClockView: View? = null
+    private var toCenterClockHostView: ClockHostView? = null
+    private var offCenterClockHostView: ClockHostView? = null
+    private var toCenterCardView: View? = null
+    private var offCenterCardView: View? = null
 
     init {
         val clockCarousel = LayoutInflater.from(context).inflate(R.layout.clock_carousel, this)
@@ -52,12 +59,19 @@
         motionLayout = clockCarousel.requireViewById(R.id.motion_container)
     }
 
+    /**
+     * Make sure to set [clockViewFactory] before calling any functions from [ClockCarouselView].
+     */
+    fun setClockViewFactory(factory: ClockViewFactory) {
+        clockViewFactory = factory
+    }
+
     fun setUpClockCarouselView(
+        clockSize: ClockSize,
         clockIds: List<String>,
-        onGetClockController: (clockId: String) -> ClockController,
         onClockSelected: (clockId: String) -> Unit,
     ) {
-        adapter = ClockCarouselAdapter(clockIds, onGetClockController, onClockSelected)
+        adapter = ClockCarouselAdapter(clockSize, clockIds, clockViewFactory, onClockSelected)
         carousel.setAdapter(adapter)
         carousel.refresh()
         motionLayout.setTransitionListener(
@@ -68,70 +82,146 @@
                     startId: Int,
                     endId: Int
                 ) {
-                    val scalingDownClockId = adapter.clockIds[carousel.currentIndex]
-                    val scalingUpIdx =
-                        if (endId == R.id.next) (carousel.currentIndex + 1) % adapter.count()
-                        else (carousel.currentIndex - 1 + adapter.count()) % adapter.count()
-                    val scalingUpClockId = adapter.clockIds[scalingUpIdx]
-                    scalingDownClockController = adapter.onGetClockController(scalingDownClockId)
-                    scalingUpClockController = adapter.onGetClockController(scalingUpClockId)
-                    scalingDownClockView = motionLayout?.findViewById(R.id.clock_scale_view_2)
-                    scalingUpClockView =
-                        motionLayout?.findViewById(
-                            if (endId == R.id.next) R.id.clock_scale_view_3
-                            else R.id.clock_scale_view_1
-                        )
-                    showingCardView = motionLayout?.findViewById(R.id.item_card_2)
-                    hidingCardView =
-                        motionLayout?.findViewById(
-                            if (endId == R.id.next) R.id.item_card_3 else R.id.item_card_1
-                        )
-                    setCardAnimationState(true)
+                    if (motionLayout == null) {
+                        return
+                    }
+                    when (clockSize) {
+                        ClockSize.DYNAMIC -> prepareDynamicClockView(motionLayout, endId)
+                        ClockSize.SMALL -> prepareSmallClockView(motionLayout, endId)
+                    }
+                    prepareCardView(motionLayout, endId)
+                    setCarouselItemAnimationState(true)
                 }
 
                 override fun onTransitionChange(
                     motionLayout: MotionLayout?,
                     startId: Int,
                     endId: Int,
-                    progress: Float
+                    progress: Float,
                 ) {
-                    scalingDownClockController
-                        ?.largeClock
-                        ?.animations
-                        ?.onPickerCarouselSwiping(1 - progress)
-                    scalingUpClockController
-                        ?.largeClock
-                        ?.animations
-                        ?.onPickerCarouselSwiping(progress)
-                    val scalingUpScale = getScalingUpScale(progress)
-                    val scalingDownScale = getScalingDownScale(progress)
-                    scalingUpClockView?.scaleX = scalingUpScale
-                    scalingUpClockView?.scaleY = scalingUpScale
-                    scalingDownClockView?.scaleX = scalingDownScale
-                    scalingDownClockView?.scaleY = scalingDownScale
-                    showingCardView?.alpha = getShowingAlpha(progress)
-                    hidingCardView?.alpha = getHidingAlpha(progress)
+                    when (clockSize) {
+                        ClockSize.DYNAMIC -> onDynamicClockViewTransition(progress)
+                        ClockSize.SMALL -> onSmallClockViewTransition(progress)
+                    }
+                    onCardViewTransition(progress)
                 }
 
                 override fun onTransitionCompleted(motionLayout: MotionLayout?, currentId: Int) {
-                    setCardAnimationState(currentId == R.id.start)
+                    setCarouselItemAnimationState(currentId == R.id.start)
                 }
 
-                private fun setCardAnimationState(isStart: Boolean) {
-                    scalingDownClockView?.scaleX = if (isStart) 1f else CLOCK_CAROUSEL_VIEW_SCALE
-                    scalingDownClockView?.scaleY = if (isStart) 1f else CLOCK_CAROUSEL_VIEW_SCALE
-                    scalingUpClockView?.scaleX = if (isStart) CLOCK_CAROUSEL_VIEW_SCALE else 1f
-                    scalingUpClockView?.scaleY = if (isStart) CLOCK_CAROUSEL_VIEW_SCALE else 1f
-                    scalingDownClockController
+                private fun prepareDynamicClockView(motionLayout: MotionLayout, endId: Int) {
+                    val scalingDownClockId = adapter.clockIds[carousel.currentIndex]
+                    val scalingUpIdx =
+                        if (endId == R.id.next) (carousel.currentIndex + 1) % adapter.count()
+                        else (carousel.currentIndex - 1 + adapter.count()) % adapter.count()
+                    val scalingUpClockId = adapter.clockIds[scalingUpIdx]
+                    offCenterClockController = clockViewFactory.getController(scalingDownClockId)
+                    toCenterClockController = clockViewFactory.getController(scalingUpClockId)
+                    offCenterClockView = motionLayout.findViewById(R.id.clock_scale_view_2)
+                    toCenterClockView =
+                        motionLayout.findViewById(
+                            if (endId == R.id.next) R.id.clock_scale_view_3
+                            else R.id.clock_scale_view_1
+                        )
+                }
+
+                private fun prepareSmallClockView(motionLayout: MotionLayout, endId: Int) {
+                    offCenterClockHostView = motionLayout.findViewById(R.id.clock_host_view_2)
+                    toCenterClockHostView =
+                        motionLayout.findViewById(
+                            if (endId == R.id.next) R.id.clock_host_view_3
+                            else R.id.clock_host_view_1
+                        )
+                }
+
+                private fun prepareCardView(motionLayout: MotionLayout, endId: Int) {
+                    offCenterCardView = motionLayout.findViewById(R.id.item_card_2)
+                    toCenterCardView =
+                        motionLayout.findViewById(
+                            if (endId == R.id.next) R.id.item_card_3 else R.id.item_card_1
+                        )
+                }
+
+                private fun onCardViewTransition(progress: Float) {
+                    offCenterCardView?.alpha = getShowingAlpha(progress)
+                    toCenterCardView?.alpha = getHidingAlpha(progress)
+                }
+
+                private fun onDynamicClockViewTransition(progress: Float) {
+                    offCenterClockController
                         ?.largeClock
                         ?.animations
-                        ?.onPickerCarouselSwiping(if (isStart) 1f else 0f)
-                    scalingUpClockController
+                        ?.onPickerCarouselSwiping(1 - progress)
+                    toCenterClockController
                         ?.largeClock
                         ?.animations
-                        ?.onPickerCarouselSwiping(if (isStart) 0f else 1f)
-                    showingCardView?.alpha = if (isStart) 0f else 1f
-                    hidingCardView?.alpha = if (isStart) 1f else 0f
+                        ?.onPickerCarouselSwiping(progress)
+                    val scalingDownScale = getScalingDownScale(progress)
+                    val scalingUpScale = getScalingUpScale(progress)
+                    offCenterClockView?.scaleX = scalingDownScale
+                    offCenterClockView?.scaleY = scalingDownScale
+                    toCenterClockView?.scaleX = scalingUpScale
+                    toCenterClockView?.scaleY = scalingUpScale
+                }
+
+                private fun onSmallClockViewTransition(progress: Float) {
+                    val offCenterClockHostView = offCenterClockHostView ?: return
+                    val toCenterClockHostView = toCenterClockHostView ?: return
+                    val offCenterClockFrame =
+                        if (offCenterClockHostView.isNotEmpty()) {
+                            offCenterClockHostView[0]
+                        } else {
+                            null
+                        }
+                            ?: return
+                    val toCenterClockFrame =
+                        if (toCenterClockHostView.isNotEmpty()) {
+                            toCenterClockHostView[0]
+                        } else {
+                            null
+                        }
+                            ?: return
+                    offCenterClockHostView.doOnPreDraw {
+                        it.pivotX = progress * it.width / 2
+                        it.pivotY = progress * it.height / 2
+                    }
+                    toCenterClockHostView.doOnPreDraw {
+                        it.pivotX = (1 - progress) * it.width / 2
+                        it.pivotY = (1 - progress) * it.height / 2
+                    }
+                    offCenterClockFrame.translationX =
+                        getTranslationDistance(
+                            offCenterClockHostView.width,
+                            offCenterClockFrame.width,
+                            offCenterClockFrame.left,
+                        ) * progress
+                    offCenterClockFrame.translationY =
+                        getTranslationDistance(
+                            offCenterClockHostView.height,
+                            offCenterClockFrame.height,
+                            offCenterClockFrame.top,
+                        ) * progress
+                    toCenterClockFrame.translationX =
+                        getTranslationDistance(
+                            toCenterClockHostView.width,
+                            toCenterClockFrame.width,
+                            toCenterClockFrame.left,
+                        ) * (1 - progress)
+                    toCenterClockFrame.translationY =
+                        getTranslationDistance(
+                            toCenterClockHostView.height,
+                            toCenterClockFrame.height,
+                            toCenterClockFrame.top,
+                        ) * (1 - progress)
+                }
+
+                private fun setCarouselItemAnimationState(isStart: Boolean) {
+                    when (clockSize) {
+                        ClockSize.DYNAMIC -> onDynamicClockViewTransition(if (isStart) 0f else 1f)
+                        ClockSize.SMALL -> onSmallClockViewTransition(if (isStart) 0f else 1f)
+                    }
+                    onCardViewTransition(if (isStart) 0f else 1f)
                 }
 
                 override fun onTransitionTrigger(
@@ -154,9 +244,10 @@
         }
     }
 
-    class ClockCarouselAdapter(
+    private class ClockCarouselAdapter(
+        val clockSize: ClockSize,
         val clockIds: List<String>,
-        val onGetClockController: (clockId: String) -> ClockController,
+        private val clockViewFactory: ClockViewFactory,
         private val onClockSelected: (clockId: String) -> Unit
     ) : Carousel.Adapter {
 
@@ -175,29 +266,89 @@
             val clockHostView =
                 getClockHostViewId(viewRoot.id)?.let { viewRoot.findViewById(it) as? ClockHostView }
                     ?: return
+            val clockId = clockIds[index]
+
+            // Add the clock view to the cloc host view
             clockHostView.removeAllViews()
-            val clockView = onGetClockController(clockIds[index]).largeClock.view
+            val clockView =
+                when (clockSize) {
+                    ClockSize.DYNAMIC -> clockViewFactory.getLargeView(clockId)
+                    ClockSize.SMALL -> clockViewFactory.getSmallView(clockId)
+                }
             // The clock view might still be attached to an existing parent. Detach before adding to
             // another parent.
             (clockView.parent as? ViewGroup)?.removeView(clockView)
             clockHostView.addView(clockView)
-            // initialize scaling state for all clocks
-            if (!isMiddleView(viewRoot.id)) {
-                cardView.alpha = 1f
-                clockScaleView.scaleX = CLOCK_CAROUSEL_VIEW_SCALE
-                clockScaleView.scaleY = CLOCK_CAROUSEL_VIEW_SCALE
-                onGetClockController(clockIds[index])
-                    .largeClock
-                    .animations
-                    .onPickerCarouselSwiping(0F)
-            } else {
-                cardView.alpha = 0f
+
+            val isMiddleView = isMiddleView(viewRoot.id)
+            when (clockSize) {
+                ClockSize.DYNAMIC ->
+                    initializeDynamicClockView(
+                        isMiddleView,
+                        clockScaleView,
+                        clockId,
+                    )
+                ClockSize.SMALL ->
+                    initializeSmallClockView(
+                        isMiddleView,
+                        clockHostView,
+                        clockView,
+                    )
+            }
+            cardView.alpha = if (isMiddleView) 0f else 1f
+        }
+
+        private fun initializeDynamicClockView(
+            isMiddleView: Boolean,
+            clockScaleView: View,
+            clockId: String,
+        ) {
+            if (isMiddleView) {
                 clockScaleView.scaleX = 1f
                 clockScaleView.scaleY = 1f
-                onGetClockController(clockIds[index])
+                clockViewFactory
+                    .getController(clockId)
                     .largeClock
                     .animations
                     .onPickerCarouselSwiping(1F)
+            } else {
+                clockScaleView.scaleX = CLOCK_CAROUSEL_VIEW_SCALE
+                clockScaleView.scaleY = CLOCK_CAROUSEL_VIEW_SCALE
+                clockViewFactory
+                    .getController(clockId)
+                    .largeClock
+                    .animations
+                    .onPickerCarouselSwiping(0F)
+            }
+        }
+
+        private fun initializeSmallClockView(
+            isMiddleView: Boolean,
+            clockHostView: ClockHostView,
+            clockView: View,
+        ) {
+            clockHostView.doOnPreDraw {
+                if (isMiddleView) {
+                    it.pivotX = 0F
+                    it.pivotY = 0F
+                    clockView.translationX = 0F
+                    clockView.translationY = 0F
+                } else {
+                    it.pivotX = it.width / 2F
+                    it.pivotY = it.height / 2F
+                    clockView.translationX =
+                        getTranslationDistance(
+                            clockHostView.width,
+                            clockView.width,
+                            clockView.left,
+                        )
+                    clockView.translationY =
+                        getTranslationDistance(
+                            clockHostView.height,
+                            clockView.height,
+                            clockView.top,
+                        )
+                }
             }
         }
 
@@ -258,5 +409,13 @@
         fun isMiddleView(rootViewId: Int): Boolean {
             return rootViewId == R.id.item_view_2
         }
+
+        private fun getTranslationDistance(
+            hostLength: Int,
+            frameLength: Int,
+            edgeDimen: Int,
+        ): Float {
+            return ((hostLength - frameLength) / 2 - edgeDimen).toFloat()
+        }
     }
 }
diff --git a/src/com/android/customization/picker/clock/ui/view/ClockViewFactory.kt b/src/com/android/customization/picker/clock/ui/view/ClockViewFactory.kt
index cfc05dc..1aa6206 100644
--- a/src/com/android/customization/picker/clock/ui/view/ClockViewFactory.kt
+++ b/src/com/android/customization/picker/clock/ui/view/ClockViewFactory.kt
@@ -70,21 +70,20 @@
                 FrameLayout.LayoutParams.WRAP_CONTENT,
                 resources.getDimensionPixelSize(R.dimen.small_clock_height)
             )
-        layoutParams.topMargin =
-            getStatusBarHeight(resources) +
-                resources.getDimensionPixelSize(R.dimen.small_clock_padding_top)
+        layoutParams.topMargin = getSmallClockTopMargin()
+        layoutParams.marginStart = getSmallClockStartPadding()
         smallClockFrame.layoutParams = layoutParams
-
-        smallClockFrame.setPaddingRelative(
-            resources.getDimensionPixelSize(R.dimen.clock_padding_start),
-            0,
-            0,
-            0
-        )
         smallClockFrame.clipChildren = false
         return smallClockFrame
     }
 
+    private fun getSmallClockTopMargin() =
+        getStatusBarHeight(appContext.resources) +
+            appContext.resources.getDimensionPixelSize(R.dimen.small_clock_padding_top)
+
+    private fun getSmallClockStartPadding() =
+        appContext.resources.getDimensionPixelSize(R.dimen.clock_padding_start)
+
     fun updateColorForAllClocks(@ColorInt seedColor: Int?) {
         clockControllers.values.forEach { it.events.onSeedColorChanged(seedColor = seedColor) }
     }
@@ -185,12 +184,9 @@
      * and position the clock view
      */
     private fun getSmallClockRegion(): Rect {
-        val topMargin =
-            getStatusBarHeight(resources) +
-                resources.getDimensionPixelSize(R.dimen.small_clock_padding_top)
-        val start = resources.getDimensionPixelSize(R.dimen.clock_padding_start)
+        val topMargin = getSmallClockTopMargin()
         val targetHeight = resources.getDimensionPixelSize(R.dimen.small_clock_height)
-        return Rect(start, topMargin, screenSize.x, topMargin + targetHeight)
+        return Rect(getSmallClockStartPadding(), topMargin, screenSize.x, topMargin + targetHeight)
     }
 
     companion object {
diff --git a/src/com/android/customization/picker/clock/ui/viewmodel/ClockCarouselViewModel.kt b/src/com/android/customization/picker/clock/ui/viewmodel/ClockCarouselViewModel.kt
index c11c333..781b48c 100644
--- a/src/com/android/customization/picker/clock/ui/viewmodel/ClockCarouselViewModel.kt
+++ b/src/com/android/customization/picker/clock/ui/viewmodel/ClockCarouselViewModel.kt
@@ -19,6 +19,7 @@
 import androidx.lifecycle.ViewModelProvider
 import androidx.lifecycle.viewModelScope
 import com.android.customization.picker.clock.domain.interactor.ClockPickerInteractor
+import com.android.customization.picker.clock.shared.ClockSize
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.Job
@@ -53,6 +54,8 @@
             }
             .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
 
+    val selectedClockSize: Flow<ClockSize> = interactor.selectedClockSize
+
     val seedColor: Flow<Int?> = interactor.seedColor
 
     val isCarouselVisible: Flow<Boolean> = allClockIds.map { it.size > 1 }.distinctUntilChanged()