Merge "Priority ordering for keyguard ConstraintSet transitions" into main
diff --git a/packages/SystemUI/customization/src/com/android/systemui/util/ThreadAssert.kt b/packages/SystemUI/customization/src/com/android/systemui/util/ThreadAssert.kt
new file mode 100644
index 0000000..ccbf4ef
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/util/ThreadAssert.kt
@@ -0,0 +1,23 @@
+/*
+ * 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.systemui.util
+
+/** Injectable helper providing thread assertions. */
+class ThreadAssert() {
+    fun isMainThread() = Assert.isMainThread()
+    fun isNotMainThread() = Assert.isNotMainThread()
+}
diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml
index 2ab0813..71ae0d7 100644
--- a/packages/SystemUI/res/values/ids.xml
+++ b/packages/SystemUI/res/values/ids.xml
@@ -228,6 +228,7 @@
     <item type="id" name="ambient_indication_container" />
     <item type="id" name="status_view_media_container" />
     <item type="id" name="smart_space_barrier_bottom" />
+    <item type="id" name="small_clock_guideline_top" />
     <item type="id" name="weather_clock_date_and_icons_barrier_bottom" />
 
     <!-- Privacy dialog -->
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
index abe49ee..86b99ec 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
@@ -100,6 +100,7 @@
     private val keyguardClockViewModel: KeyguardClockViewModel,
     private val lockscreenContentViewModel: LockscreenContentViewModel,
     private val lockscreenSceneBlueprintsLazy: Lazy<Set<LockscreenSceneBlueprint>>,
+    private val keyguardBlueprintViewBinder: KeyguardBlueprintViewBinder,
 ) : CoreStartable {
 
     private var rootViewHandle: DisposableHandle? = null
@@ -143,7 +144,7 @@
                 cs.connect(composeView.id, BOTTOM, PARENT_ID, BOTTOM)
                 keyguardRootView.addView(composeView)
             } else {
-                KeyguardBlueprintViewBinder.bind(
+                keyguardBlueprintViewBinder.bind(
                     keyguardRootView,
                     keyguardBlueprintViewModel,
                     keyguardClockViewModel,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
index 0b227fa..968c3e3 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
@@ -73,6 +73,7 @@
 import com.android.systemui.statusbar.policy.UserSwitcherController;
 import com.android.systemui.user.domain.interactor.SelectedUserInteractor;
 import com.android.systemui.util.DeviceConfigProxy;
+import com.android.systemui.util.ThreadAssert;
 import com.android.systemui.util.kotlin.JavaAdapter;
 import com.android.systemui.util.settings.SecureSettings;
 import com.android.systemui.util.settings.SystemSettings;
@@ -223,6 +224,13 @@
         return new KeyguardQuickAffordancesMetricsLoggerImpl();
     }
 
+    /** */
+    @Provides
+    @SysUISingleton
+    static ThreadAssert providesThreadAssert() {
+        return new ThreadAssert();
+    }
+
     /** Binds {@link KeyguardUpdateMonitor} as a {@link CoreStartable}. */
     @Binds
     @IntoMap
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepository.kt
index 9381830..0659c7c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepository.kt
@@ -17,14 +17,16 @@
 
 package com.android.systemui.keyguard.data.repository
 
+import android.os.Handler
 import android.util.Log
 import com.android.systemui.common.ui.data.repository.ConfigurationRepository
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.keyguard.shared.model.KeyguardBlueprint
 import com.android.systemui.keyguard.ui.view.layout.blueprints.DefaultKeyguardBlueprint.Companion.DEFAULT
 import com.android.systemui.keyguard.ui.view.layout.blueprints.KeyguardBlueprintModule
-import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransitionType
-import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransitionType.DefaultTransition
+import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Config
+import com.android.systemui.util.ThreadAssert
 import java.io.PrintWriter
 import java.util.TreeMap
 import javax.inject.Inject
@@ -49,16 +51,17 @@
 constructor(
     configurationRepository: ConfigurationRepository,
     blueprints: Set<@JvmSuppressWildcards KeyguardBlueprint>,
+    @Main val handler: Handler,
+    val assert: ThreadAssert,
 ) {
     // This is TreeMap so that we can order the blueprints and assign numerical values to the
     // blueprints in the adb tool.
     private val blueprintIdMap: TreeMap<String, KeyguardBlueprint> =
         TreeMap<String, KeyguardBlueprint>().apply { putAll(blueprints.associateBy { it.id }) }
     val blueprint: MutableStateFlow<KeyguardBlueprint> = MutableStateFlow(blueprintIdMap[DEFAULT]!!)
-    val refreshBluePrint: MutableSharedFlow<Unit> = MutableSharedFlow(extraBufferCapacity = 1)
-    val refreshBlueprintTransition: MutableSharedFlow<IntraBlueprintTransitionType> =
-        MutableSharedFlow(extraBufferCapacity = 1)
+    val refreshTransition = MutableSharedFlow<Config>(extraBufferCapacity = 1)
     val configurationChange: Flow<Unit> = configurationRepository.onAnyConfigurationChange
+    private var targetTransitionConfig: Config? = null
 
     /**
      * Emits the blueprint value to the collectors.
@@ -105,14 +108,32 @@
         blueprint?.let { this.blueprint.value = it }
     }
 
-    /** Re-emits the last emitted blueprint value if possible. */
-    fun refreshBlueprint() {
-        refreshBlueprintWithTransition(DefaultTransition)
-    }
+    /**
+     * Re-emits the last emitted blueprint value if possible. This is delayed until next frame to
+     * dedupe requests and determine the correct transition to execute.
+     */
+    fun refreshBlueprint(config: Config = Config.DEFAULT) {
+        fun scheduleCallback() {
+            // We use a handler here instead of a CoroutineDipsatcher because the one provided by
+            // @Main CoroutineDispatcher is currently Dispatchers.Main.immediate, which doesn't
+            // delay the callback, and instead runs it imemdiately.
+            handler.post {
+                assert.isMainThread()
+                targetTransitionConfig?.let {
+                    val success = refreshTransition.tryEmit(it)
+                    if (!success) {
+                        Log.e(TAG, "refreshBlueprint: Failed to emit blueprint refresh: $it")
+                    }
+                }
+                targetTransitionConfig = null
+            }
+        }
 
-    fun refreshBlueprintWithTransition(type: IntraBlueprintTransitionType = DefaultTransition) {
-        refreshBluePrint.tryEmit(Unit)
-        refreshBlueprintTransition.tryEmit(type)
+        assert.isMainThread()
+        if ((targetTransitionConfig?.type?.priority ?: Int.MIN_VALUE) < config.type.priority) {
+            if (targetTransitionConfig == null) scheduleCallback()
+            targetTransitionConfig = config
+        }
     }
 
     /** Prints all available blueprints to the PrintWriter. */
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt
index 566e006..56d64a2 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt
@@ -24,13 +24,12 @@
 import com.android.systemui.keyguard.shared.model.KeyguardBlueprint
 import com.android.systemui.keyguard.ui.view.layout.blueprints.DefaultKeyguardBlueprint
 import com.android.systemui.keyguard.ui.view.layout.blueprints.SplitShadeKeyguardBlueprint
-import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransitionType
-import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransitionType.DefaultTransition
+import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Config
+import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Type
 import com.android.systemui.statusbar.policy.SplitShadeStateController
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.launch
 
@@ -44,20 +43,14 @@
     private val splitShadeStateController: SplitShadeStateController,
 ) {
 
-    /**
-     * The current blueprint for the lockscreen.
-     *
-     * This flow can also emit the same blueprint value if refreshBlueprint is emitted.
-     */
+    /** The current blueprint for the lockscreen. */
     val blueprint: Flow<KeyguardBlueprint> = keyguardBlueprintRepository.blueprint
 
-    val blueprintWithTransition =
-        combine(
-            keyguardBlueprintRepository.refreshBluePrint,
-            keyguardBlueprintRepository.refreshBlueprintTransition
-        ) { _, source ->
-            source
-        }
+    /**
+     * Triggered when the blueprint isn't changed, but the ConstraintSet should be rebuilt and
+     * optionally a transition should be fired to move to the rebuilt ConstraintSet.
+     */
+    val refreshTransition = keyguardBlueprintRepository.refreshTransition
 
     init {
         applicationScope.launch {
@@ -105,14 +98,11 @@
         return keyguardBlueprintRepository.applyBlueprint(blueprintId)
     }
 
-    /** Re-emits the blueprint value to the collectors. */
-    fun refreshBlueprint() {
-        keyguardBlueprintRepository.refreshBlueprint()
-    }
+    /** Emits a value to refresh the blueprint with the appropriate transition. */
+    fun refreshBlueprint(type: Type = Type.NoTransition) = refreshBlueprint(Config(type))
 
-    fun refreshBlueprintWithTransition(type: IntraBlueprintTransitionType = DefaultTransition) {
-        keyguardBlueprintRepository.refreshBlueprintWithTransition(type)
-    }
+    /** Emits a value to refresh the blueprint with the appropriate transition. */
+    fun refreshBlueprint(config: Config) = keyguardBlueprintRepository.refreshBlueprint(config)
 
     fun getCurrentBlueprint(): KeyguardBlueprint {
         return keyguardBlueprintRepository.blueprint.value
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt
index 404046b..6e70368 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt
@@ -17,7 +17,9 @@
 
 package com.android.systemui.keyguard.ui.binder
 
+import android.os.Handler
 import android.os.Trace
+import android.transition.Transition
 import android.transition.TransitionManager
 import android.util.Log
 import androidx.constraintlayout.widget.ConstraintLayout
@@ -25,98 +27,168 @@
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.systemui.Flags.keyguardBottomAreaRefactor
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.BaseBlueprintTransition
 import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition
-import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransitionType
+import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Config
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardBlueprintViewModel
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
 import com.android.systemui.lifecycle.repeatWhenAttached
+import javax.inject.Inject
+import kotlin.math.max
 import kotlinx.coroutines.launch
 
-class KeyguardBlueprintViewBinder {
-    companion object {
-        private const val TAG = "KeyguardBlueprintViewBinder"
+private const val TAG = "KeyguardBlueprintViewBinder"
+private const val DEBUG = true
 
-        fun bind(
-            constraintLayout: ConstraintLayout,
-            viewModel: KeyguardBlueprintViewModel,
-            clockViewModel: KeyguardClockViewModel
-        ) {
-            constraintLayout.repeatWhenAttached {
-                repeatOnLifecycle(Lifecycle.State.CREATED) {
-                    launch {
-                        viewModel.blueprint.collect { blueprint ->
-                            val prevBluePrint = viewModel.currentBluePrint
-                            Trace.beginSection("KeyguardBlueprint#applyBlueprint")
-                            Log.d(TAG, "applying blueprint: $blueprint")
-                            TransitionManager.endTransitions(constraintLayout)
+@SysUISingleton
+class KeyguardBlueprintViewBinder
+@Inject
+constructor(
+    @Main private val handler: Handler,
+) {
+    private var runningPriority = -1
+    private val runningTransitions = mutableSetOf<Transition>()
+    private val isTransitionRunning: Boolean
+        get() = runningTransitions.size > 0
+    private val transitionListener =
+        object : Transition.TransitionListener {
+            override fun onTransitionCancel(transition: Transition) {
+                if (DEBUG) Log.e(TAG, "onTransitionCancel: ${transition::class.simpleName}")
+                runningTransitions.remove(transition)
+            }
 
-                            val cs =
-                                ConstraintSet().apply {
-                                    clone(constraintLayout)
-                                    val emptyLayout = ConstraintSet.Layout()
-                                    knownIds.forEach {
-                                        getConstraint(it).layout.copyFrom(emptyLayout)
-                                    }
-                                    blueprint.applyConstraints(this)
-                                }
+            override fun onTransitionEnd(transition: Transition) {
+                if (DEBUG) Log.e(TAG, "onTransitionEnd: ${transition::class.simpleName}")
+                runningTransitions.remove(transition)
+            }
 
-                            // Apply transition.
+            override fun onTransitionPause(transition: Transition) {
+                if (DEBUG) Log.i(TAG, "onTransitionPause: ${transition::class.simpleName}")
+                runningTransitions.remove(transition)
+            }
+
+            override fun onTransitionResume(transition: Transition) {
+                if (DEBUG) Log.i(TAG, "onTransitionResume: ${transition::class.simpleName}")
+                runningTransitions.add(transition)
+            }
+
+            override fun onTransitionStart(transition: Transition) {
+                if (DEBUG) Log.i(TAG, "onTransitionStart: ${transition::class.simpleName}")
+                runningTransitions.add(transition)
+            }
+        }
+
+    fun bind(
+        constraintLayout: ConstraintLayout,
+        viewModel: KeyguardBlueprintViewModel,
+        clockViewModel: KeyguardClockViewModel,
+    ) {
+        constraintLayout.repeatWhenAttached {
+            repeatOnLifecycle(Lifecycle.State.CREATED) {
+                launch {
+                    viewModel.blueprint.collect { blueprint ->
+                        Trace.beginSection("KeyguardBlueprintViewBinder#applyBlueprint")
+                        val prevBluePrint = viewModel.currentBluePrint
+
+                        val cs =
+                            ConstraintSet().apply {
+                                clone(constraintLayout)
+                                val emptyLayout = ConstraintSet.Layout()
+                                knownIds.forEach { getConstraint(it).layout.copyFrom(emptyLayout) }
+                                blueprint.applyConstraints(this)
+                            }
+
+                        var transition =
                             if (
                                 !keyguardBottomAreaRefactor() &&
                                     prevBluePrint != null &&
                                     prevBluePrint != blueprint
                             ) {
-                                TransitionManager.beginDelayedTransition(
-                                    constraintLayout,
-                                    BaseBlueprintTransition(clockViewModel)
-                                        .addTransition(
-                                            IntraBlueprintTransition(
-                                                IntraBlueprintTransitionType.NoTransition,
-                                                clockViewModel
-                                            )
-                                        )
-                                )
-                            } else {
-                                TransitionManager.beginDelayedTransition(
-                                    constraintLayout,
-                                    IntraBlueprintTransition(
-                                        IntraBlueprintTransitionType.NoTransition,
-                                        clockViewModel
+                                BaseBlueprintTransition(clockViewModel)
+                                    .addTransition(
+                                        IntraBlueprintTransition(Config.DEFAULT, clockViewModel)
                                     )
-                                )
+                            } else {
+                                IntraBlueprintTransition(Config.DEFAULT, clockViewModel)
                             }
 
-                            // Add and remove views of sections that are not contained by the
-                            // other.
+                        runTransition(constraintLayout, transition, Config.DEFAULT) {
+                            // Add and remove views of sections that are not contained by the other.
                             blueprint.replaceViews(prevBluePrint, constraintLayout)
                             cs.applyTo(constraintLayout)
-
-                            viewModel.currentBluePrint = blueprint
-                            Trace.endSection()
                         }
+
+                        viewModel.currentBluePrint = blueprint
+                        Trace.endSection()
                     }
+                }
 
-                    launch {
-                        viewModel.blueprintWithTransition.collect { source ->
-                            TransitionManager.endTransitions(constraintLayout)
+                launch {
+                    viewModel.refreshTransition.collect { transition ->
+                        Trace.beginSection("KeyguardBlueprintViewBinder#refreshTransition")
+                        val cs =
+                            ConstraintSet().apply {
+                                clone(constraintLayout)
+                                viewModel.currentBluePrint?.applyConstraints(this)
+                            }
 
-                            val cs =
-                                ConstraintSet().apply {
-                                    clone(constraintLayout)
-                                    viewModel.currentBluePrint?.applyConstraints(this)
-                                }
-
-                            TransitionManager.beginDelayedTransition(
-                                constraintLayout,
-                                IntraBlueprintTransition(source, clockViewModel)
-                            )
+                        runTransition(
+                            constraintLayout,
+                            IntraBlueprintTransition(transition, clockViewModel),
+                            transition,
+                        ) {
                             cs.applyTo(constraintLayout)
-                            Trace.endSection()
                         }
+                        Trace.endSection()
                     }
                 }
             }
         }
     }
+
+    private fun runTransition(
+        constraintLayout: ConstraintLayout,
+        transition: Transition,
+        config: Config,
+        apply: () -> Unit,
+    ) {
+        val currentPriority = if (isTransitionRunning) runningPriority else -1
+        if (config.checkPriority && config.type.priority < currentPriority) {
+            if (DEBUG) {
+                Log.w(
+                    TAG,
+                    "runTransition: skipping ${transition::class.simpleName}: " +
+                        "currentPriority=$currentPriority; config=$config"
+                )
+            }
+            apply()
+            return
+        }
+
+        if (DEBUG) {
+            Log.i(
+                TAG,
+                "runTransition: running ${transition::class.simpleName}: " +
+                    "currentPriority=$currentPriority; config=$config"
+            )
+        }
+
+        // beginDelayedTransition makes a copy, so we temporarially add the uncopied transition to
+        // the running set until the copy is started by the handler.
+        runningTransitions.add(transition)
+        transition.addListener(transitionListener)
+        runningPriority = max(currentPriority, config.type.priority)
+
+        handler.post {
+            if (config.terminatePrevious) {
+                TransitionManager.endTransitions(constraintLayout)
+            }
+
+            TransitionManager.beginDelayedTransition(constraintLayout, transition)
+            runningTransitions.remove(transition)
+            apply()
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt
index 62a6e8b..01596ed 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt
@@ -30,7 +30,7 @@
 import com.android.systemui.Flags.migrateClocksToBlueprint
 import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
-import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransitionType
+import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Type
 import com.android.systemui.keyguard.ui.view.layout.sections.ClockSection
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
 import com.android.systemui.lifecycle.repeatWhenAttached
@@ -39,6 +39,8 @@
 import kotlinx.coroutines.launch
 
 object KeyguardClockViewBinder {
+    private val TAG = KeyguardClockViewBinder::class.simpleName!!
+
     @JvmStatic
     fun bind(
         clockSection: ClockSection,
@@ -68,9 +70,7 @@
                     if (!migrateClocksToBlueprint()) return@launch
                     viewModel.clockSize.collect {
                         updateBurnInLayer(keyguardRootView, viewModel)
-                        blueprintInteractor.refreshBlueprintWithTransition(
-                            IntraBlueprintTransitionType.ClockSize
-                        )
+                        blueprintInteractor.refreshBlueprint(Type.ClockSize)
                     }
                 }
                 launch {
@@ -83,13 +83,9 @@
                                 it.largeClock.config.hasCustomPositionUpdatedAnimation &&
                                     it.config.id == DEFAULT_CLOCK_ID
                             ) {
-                                blueprintInteractor.refreshBlueprintWithTransition(
-                                    IntraBlueprintTransitionType.DefaultClockStepping
-                                )
+                                blueprintInteractor.refreshBlueprint(Type.DefaultClockStepping)
                             } else {
-                                blueprintInteractor.refreshBlueprintWithTransition(
-                                    IntraBlueprintTransitionType.DefaultTransition
-                                )
+                                blueprintInteractor.refreshBlueprint(Type.DefaultTransition)
                             }
                         }
                     }
@@ -102,9 +98,7 @@
                             if (
                                 viewModel.useLargeClock && it.config.id == "DIGITAL_CLOCK_WEATHER"
                             ) {
-                                blueprintInteractor.refreshBlueprintWithTransition(
-                                    IntraBlueprintTransitionType.DefaultTransition
-                                )
+                                blueprintInteractor.refreshBlueprint(Type.DefaultTransition)
                             }
                         }
                     }
@@ -112,6 +106,7 @@
             }
         }
     }
+
     @VisibleForTesting
     fun updateBurnInLayer(
         keyguardRootView: ConstraintLayout,
@@ -171,6 +166,7 @@
             }
         }
     }
+
     fun applyConstraints(
         clockSection: ClockSection,
         rootView: ConstraintLayout,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt
index 08a2b9c..b77f0c5 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt
@@ -23,6 +23,8 @@
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.systemui.Flags.migrateClocksToBlueprint
 import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
+import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Config
+import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Type
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel
 import com.android.systemui.lifecycle.repeatWhenAttached
@@ -49,7 +51,13 @@
                             clockViewModel,
                             smartspaceViewModel
                         )
-                        blueprintInteractor.refreshBlueprintWithTransition()
+                        blueprintInteractor.refreshBlueprint(
+                            Config(
+                                Type.SmartspaceVisibility,
+                                checkPriority = false,
+                                terminatePrevious = false,
+                            )
+                        )
                     }
                 }
 
@@ -57,7 +65,13 @@
                     if (!migrateClocksToBlueprint()) return@launch
                     smartspaceViewModel.bcSmartspaceVisibility.collect {
                         updateBCSmartspaceInBurnInLayer(keyguardRootView, clockViewModel)
-                        blueprintInteractor.refreshBlueprintWithTransition()
+                        blueprintInteractor.refreshBlueprint(
+                            Config(
+                                Type.SmartspaceVisibility,
+                                checkPriority = false,
+                                terminatePrevious = false,
+                            )
+                        )
                     }
                 }
             }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt
index 524aa1a..a7075d9 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt
@@ -21,25 +21,42 @@
 import com.android.systemui.keyguard.ui.view.layout.sections.transitions.DefaultClockSteppingTransition
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
 
-enum class IntraBlueprintTransitionType {
-    ClockSize,
-    ClockCenter,
-    DefaultClockStepping,
-    DefaultTransition,
-    AodNotifIconsTransition,
-    // When transition between blueprint, we don't need any duration or interpolator but we need
-    // all elements go to correct state
-    NoTransition,
-}
-
 class IntraBlueprintTransition(
-    type: IntraBlueprintTransitionType,
-    clockViewModel: KeyguardClockViewModel
+    config: IntraBlueprintTransition.Config,
+    clockViewModel: KeyguardClockViewModel,
 ) : TransitionSet() {
+
+    enum class Type(
+        val priority: Int,
+    ) {
+        ClockSize(100),
+        ClockCenter(99),
+        DefaultClockStepping(98),
+        AodNotifIconsTransition(97),
+        SmartspaceVisibility(2),
+        DefaultTransition(1),
+        // When transition between blueprint, we don't need any duration or interpolator but we need
+        // all elements go to correct state
+        NoTransition(0),
+    }
+
+    data class Config(
+        val type: Type,
+        val checkPriority: Boolean = true,
+        val terminatePrevious: Boolean = true,
+    ) {
+        companion object {
+            val DEFAULT = Config(Type.NoTransition)
+        }
+    }
+
     init {
         ordering = ORDERING_TOGETHER
-        if (type == IntraBlueprintTransitionType.DefaultClockStepping)
-            addTransition(clockViewModel.clock?.let { DefaultClockSteppingTransition(it) })
-        addTransition(ClockSizeTransition(type, clockViewModel))
+        when (config.type) {
+            Type.NoTransition -> {}
+            Type.DefaultClockStepping ->
+                addTransition(clockViewModel.clock?.let { DefaultClockSteppingTransition(it) })
+            else -> addTransition(ClockSizeTransition(config, clockViewModel))
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
index 631b342..54a7ca4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
@@ -24,7 +24,7 @@
 import androidx.constraintlayout.widget.ConstraintSet
 import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
 import androidx.constraintlayout.widget.ConstraintSet.END
-import androidx.constraintlayout.widget.ConstraintSet.INVISIBLE
+import androidx.constraintlayout.widget.ConstraintSet.GONE
 import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
 import androidx.constraintlayout.widget.ConstraintSet.START
 import androidx.constraintlayout.widget.ConstraintSet.TOP
@@ -52,11 +52,6 @@
     visibility: Int,
 ) = views.forEach { view -> this.setVisibility(view.id, visibility) }
 
-internal fun ConstraintSet.setAlpha(
-    views: Iterable<View>,
-    alpha: Float,
-) = views.forEach { view -> this.setAlpha(view.id, alpha) }
-
 open class ClockSection
 @Inject
 constructor(
@@ -105,7 +100,7 @@
         // Add constraint between elements in clock and clock container
         return constraintSet.apply {
             setVisibility(getTargetClockFace(clock).views, VISIBLE)
-            setVisibility(getNonTargetClockFace(clock).views, INVISIBLE)
+            setVisibility(getNonTargetClockFace(clock).views, GONE)
             if (!keyguardClockViewModel.useLargeClock) {
                 connect(sharedR.id.bc_smartspace_view, TOP, sharedR.id.date_smartspace_view, BOTTOM)
             }
@@ -150,6 +145,7 @@
             }
         }
     }
+
     open fun applyDefaultConstraints(constraints: ConstraintSet) {
         val guideline =
             if (keyguardClockViewModel.clockShouldBeCentered.value) PARENT_ID
@@ -168,8 +164,8 @@
             largeClockTopMargin += getDimen(ENHANCED_SMARTSPACE_HEIGHT)
 
             connect(R.id.lockscreen_clock_view_large, TOP, PARENT_ID, TOP, largeClockTopMargin)
-            constrainHeight(R.id.lockscreen_clock_view_large, WRAP_CONTENT)
             constrainWidth(R.id.lockscreen_clock_view_large, WRAP_CONTENT)
+            constrainHeight(R.id.lockscreen_clock_view_large, WRAP_CONTENT)
             constrainWidth(R.id.lockscreen_clock_view, WRAP_CONTENT)
             constrainHeight(
                 R.id.lockscreen_clock_view,
@@ -190,11 +186,10 @@
                     context.resources.getDimensionPixelSize(R.dimen.keyguard_clock_top_margin) +
                         Utils.getStatusBarHeaderHeightKeyguard(context)
                 }
-            if (keyguardClockViewModel.useLargeClock) {
-                smallClockTopMargin -=
-                    context.resources.getDimensionPixelSize(customizationR.dimen.small_clock_height)
-            }
-            connect(R.id.lockscreen_clock_view, TOP, PARENT_ID, TOP, smallClockTopMargin)
+
+            create(R.id.small_clock_guideline_top, ConstraintSet.HORIZONTAL_GUIDELINE)
+            setGuidelineBegin(R.id.small_clock_guideline_top, smallClockTopMargin)
+            connect(R.id.lockscreen_clock_view, TOP, R.id.small_clock_guideline_top, BOTTOM)
         }
 
         constrainWeatherClockDateIconsBarrier(constraints)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt
index 2f99719..8255bcc 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt
@@ -53,14 +53,14 @@
     private var dateView: View? = null
 
     private var smartspaceVisibilityListener: OnGlobalLayoutListener? = null
+    private var pastVisibility: Int = -1
 
     override fun addViews(constraintLayout: ConstraintLayout) {
-        if (!migrateClocksToBlueprint()) {
-            return
-        }
+        if (!migrateClocksToBlueprint()) return
         smartspaceView = smartspaceController.buildAndConnectView(constraintLayout)
         weatherView = smartspaceController.buildAndConnectWeatherView(constraintLayout)
         dateView = smartspaceController.buildAndConnectDateView(constraintLayout)
+        pastVisibility = smartspaceView?.visibility ?: View.GONE
         if (keyguardSmartspaceViewModel.isSmartspaceEnabled) {
             constraintLayout.addView(smartspaceView)
             if (keyguardSmartspaceViewModel.isDateWeatherDecoupled) {
@@ -69,26 +69,20 @@
             }
         }
         keyguardUnlockAnimationController.lockscreenSmartspace = smartspaceView
-        smartspaceVisibilityListener =
-            object : OnGlobalLayoutListener {
-                var pastVisibility = GONE
-                override fun onGlobalLayout() {
-                    smartspaceView?.let {
-                        val newVisibility = it.visibility
-                        if (pastVisibility != newVisibility) {
-                            keyguardSmartspaceInteractor.setBcSmartspaceVisibility(newVisibility)
-                            pastVisibility = newVisibility
-                        }
-                    }
+        smartspaceVisibilityListener = OnGlobalLayoutListener {
+            smartspaceView?.let {
+                val newVisibility = it.visibility
+                if (pastVisibility != newVisibility) {
+                    keyguardSmartspaceInteractor.setBcSmartspaceVisibility(newVisibility)
+                    pastVisibility = newVisibility
                 }
             }
+        }
         smartspaceView?.viewTreeObserver?.addOnGlobalLayoutListener(smartspaceVisibilityListener)
     }
 
     override fun bindData(constraintLayout: ConstraintLayout) {
-        if (!migrateClocksToBlueprint()) {
-            return
-        }
+        if (!migrateClocksToBlueprint()) return
         KeyguardSmartspaceViewBinder.bind(
             constraintLayout,
             keyguardClockViewModel,
@@ -98,9 +92,7 @@
     }
 
     override fun applyConstraints(constraintSet: ConstraintSet) {
-        if (!migrateClocksToBlueprint()) {
-            return
-        }
+        if (!migrateClocksToBlueprint()) return
         val horizontalPaddingStart =
             context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_start) +
                 context.resources.getDimensionPixelSize(R.dimen.status_view_margin_horizontal)
@@ -196,9 +188,7 @@
     }
 
     override fun removeViews(constraintLayout: ConstraintLayout) {
-        if (!migrateClocksToBlueprint()) {
-            return
-        }
+        if (!migrateClocksToBlueprint()) return
         listOf(smartspaceView, dateView, weatherView).forEach {
             it?.let {
                 if (it.parent == constraintLayout) {
@@ -211,6 +201,9 @@
     }
 
     private fun updateVisibility(constraintSet: ConstraintSet) {
+        // This may update the visibility of the smartspace views
+        smartspaceController.requestSmartspaceUpdate()
+
         constraintSet.apply {
             setVisibility(
                 sharedR.id.weather_smartspace_view,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt
index 99565b1..64cbb32 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt
@@ -17,175 +17,308 @@
 package com.android.systemui.keyguard.ui.view.layout.sections.transitions
 
 import android.animation.Animator
-import android.animation.ObjectAnimator
-import android.transition.ChangeBounds
+import android.animation.AnimatorListenerAdapter
+import android.animation.ValueAnimator
+import android.graphics.Rect
+import android.transition.Transition
 import android.transition.TransitionSet
 import android.transition.TransitionValues
-import android.transition.Visibility
+import android.util.Log
 import android.view.View
 import android.view.ViewGroup
+import android.view.ViewTreeObserver.OnPreDrawListener
 import com.android.app.animation.Interpolators
-import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransitionType
+import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition
+import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Type
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
 import com.android.systemui.res.R
 import com.android.systemui.shared.R as sharedR
+import kotlin.math.abs
 
-const val CLOCK_OUT_MILLIS = 133L
-const val CLOCK_IN_MILLIS = 167L
-val CLOCK_IN_INTERPOLATOR = Interpolators.LINEAR_OUT_SLOW_IN
-const val CLOCK_IN_START_DELAY_MILLIS = 133L
-val CLOCK_OUT_INTERPOLATOR = Interpolators.LINEAR
+internal fun View.setRect(rect: Rect) =
+    this.setLeftTopRightBottom(rect.left, rect.top, rect.right, rect.bottom)
 
 class ClockSizeTransition(
-    val type: IntraBlueprintTransitionType,
-    clockViewModel: KeyguardClockViewModel
+    config: IntraBlueprintTransition.Config,
+    clockViewModel: KeyguardClockViewModel,
 ) : TransitionSet() {
     init {
         ordering = ORDERING_TOGETHER
-        addTransition(ClockOutTransition(clockViewModel, type))
-        addTransition(ClockInTransition(clockViewModel, type))
-        addTransition(SmartspaceChangeBounds(clockViewModel, type))
-        addTransition(ClockInChangeBounds(clockViewModel, type))
-        addTransition(ClockOutChangeBounds(clockViewModel, type))
+        if (config.type != Type.SmartspaceVisibility) {
+            addTransition(ClockFaceOutTransition(config, clockViewModel))
+            addTransition(ClockFaceInTransition(config, clockViewModel))
+        }
+        addTransition(SmartspaceMoveTransition(config, clockViewModel))
     }
 
-    class ClockInTransition(viewModel: KeyguardClockViewModel, type: IntraBlueprintTransitionType) :
-        Visibility() {
-        init {
-            mode = MODE_IN
-            if (type != IntraBlueprintTransitionType.NoTransition) {
-                duration = CLOCK_IN_MILLIS
-                startDelay = CLOCK_IN_START_DELAY_MILLIS
-                interpolator = Interpolators.LINEAR_OUT_SLOW_IN
-            } else {
-                duration = 0
-                startDelay = 0
-            }
+    open class VisibilityBoundsTransition() : Transition() {
+        var captureSmartspace: Boolean = false
 
-            addTarget(sharedR.id.bc_smartspace_view)
-            addTarget(sharedR.id.date_smartspace_view)
-            addTarget(sharedR.id.weather_smartspace_view)
-            if (viewModel.useLargeClock) {
-                viewModel.clock?.let { it.largeClock.layout.views.forEach { addTarget(it) } }
-            } else {
-                addTarget(R.id.lockscreen_clock_view)
-            }
+        override fun captureEndValues(transition: TransitionValues) = captureValues(transition)
+        override fun captureStartValues(transition: TransitionValues) = captureValues(transition)
+        override fun getTransitionProperties(): Array<String> = TRANSITION_PROPERTIES
+        open fun mutateBounds(
+            view: View,
+            fromVis: Int,
+            toVis: Int,
+            fromBounds: Rect,
+            toBounds: Rect,
+            fromSSBounds: Rect?,
+            toSSBounds: Rect?
+        ) {}
+
+        private fun captureValues(transition: TransitionValues) {
+            val view = transition.view
+            transition.values[PROP_VISIBILITY] = view.visibility
+            transition.values[PROP_ALPHA] = view.alpha
+            transition.values[PROP_BOUNDS] = Rect(view.left, view.top, view.right, view.bottom)
+
+            if (!captureSmartspace) return
+            val ss = (view.parent as View).findViewById<View>(sharedR.id.bc_smartspace_view)
+            if (ss == null) return
+            transition.values[SMARTSPACE_BOUNDS] = Rect(ss.left, ss.top, ss.right, ss.bottom)
         }
 
-        override fun onAppear(
-            sceneRoot: ViewGroup?,
-            view: View,
+        override fun createAnimator(
+            sceenRoot: ViewGroup,
             startValues: TransitionValues?,
             endValues: TransitionValues?
-        ): Animator {
-            return ObjectAnimator.ofFloat(view, "alpha", 1f).also {
-                it.duration = duration
-                it.startDelay = startDelay
-                it.interpolator = interpolator
-                it.addUpdateListener { view.alpha = it.animatedValue as Float }
-                it.start()
-            }
-        }
-    }
+        ): Animator? {
+            if (startValues == null || endValues == null) return null
 
-    class ClockOutTransition(
-        viewModel: KeyguardClockViewModel,
-        type: IntraBlueprintTransitionType
-    ) : Visibility() {
-        init {
-            mode = MODE_OUT
-            if (type != IntraBlueprintTransitionType.NoTransition) {
-                duration = CLOCK_OUT_MILLIS
-                interpolator = CLOCK_OUT_INTERPOLATOR
-            } else {
-                duration = 0
+            val fromView = startValues.view
+            var fromVis = startValues.values[PROP_VISIBILITY] as Int
+            var fromIsVis = fromVis == View.VISIBLE
+            var fromAlpha = startValues.values[PROP_ALPHA] as Float
+            val fromBounds = startValues.values[PROP_BOUNDS] as Rect
+            val fromSSBounds =
+                if (captureSmartspace) startValues.values[SMARTSPACE_BOUNDS] as Rect else null
+
+            val toView = endValues.view
+            val toVis = endValues.values[PROP_VISIBILITY] as Int
+            val toBounds = endValues.values[PROP_BOUNDS] as Rect
+            val toSSBounds =
+                if (captureSmartspace) endValues.values[SMARTSPACE_BOUNDS] as Rect else null
+            val toIsVis = toVis == View.VISIBLE
+            val toAlpha = if (toIsVis) 1f else 0f
+
+            // Align starting visibility and alpha
+            if (!fromIsVis) fromAlpha = 0f
+            else if (fromAlpha <= 0f) {
+                fromIsVis = false
+                fromVis = View.INVISIBLE
             }
 
-            addTarget(sharedR.id.bc_smartspace_view)
-            addTarget(sharedR.id.date_smartspace_view)
-            addTarget(sharedR.id.weather_smartspace_view)
-            if (viewModel.useLargeClock) {
-                addTarget(R.id.lockscreen_clock_view)
-            } else {
-                viewModel.clock?.let { it.largeClock.layout.views.forEach { addTarget(it) } }
-            }
-        }
-
-        override fun onDisappear(
-            sceneRoot: ViewGroup?,
-            view: View,
-            startValues: TransitionValues?,
-            endValues: TransitionValues?
-        ): Animator {
-            return ObjectAnimator.ofFloat(view, "alpha", 0f).also {
-                it.duration = duration
-                it.interpolator = interpolator
-                it.addUpdateListener { view.alpha = it.animatedValue as Float }
-                it.start()
-            }
-        }
-    }
-
-    class ClockInChangeBounds(
-        viewModel: KeyguardClockViewModel,
-        type: IntraBlueprintTransitionType
-    ) : ChangeBounds() {
-        init {
-            if (type != IntraBlueprintTransitionType.NoTransition) {
-                duration = CLOCK_IN_MILLIS
-                startDelay = CLOCK_IN_START_DELAY_MILLIS
-                interpolator = CLOCK_IN_INTERPOLATOR
-            } else {
-                duration = 0
-                startDelay = 0
+            mutateBounds(toView, fromVis, toVis, fromBounds, toBounds, fromSSBounds, toSSBounds)
+            if (fromIsVis == toIsVis && fromBounds.equals(toBounds)) {
+                if (DEBUG) {
+                    Log.w(
+                        TAG,
+                        "Skipping no-op transition: $toView; " +
+                            "vis: $fromVis -> $toVis; " +
+                            "alpha: $fromAlpha -> $toAlpha; " +
+                            "bounds: $fromBounds -> $toBounds; "
+                    )
+                }
+                return null
             }
 
-            if (viewModel.useLargeClock) {
-                viewModel.clock?.let { it.largeClock.layout.views.forEach { addTarget(it) } }
-            } else {
-                addTarget(R.id.lockscreen_clock_view)
-            }
-        }
-    }
+            val sendToBack = fromIsVis && !toIsVis
+            fun lerp(start: Int, end: Int, fract: Float): Int =
+                (start * (1f - fract) + end * fract).toInt()
+            fun computeBounds(fract: Float): Rect =
+                Rect(
+                    lerp(fromBounds.left, toBounds.left, fract),
+                    lerp(fromBounds.top, toBounds.top, fract),
+                    lerp(fromBounds.right, toBounds.right, fract),
+                    lerp(fromBounds.bottom, toBounds.bottom, fract)
+                )
 
-    class ClockOutChangeBounds(
-        viewModel: KeyguardClockViewModel,
-        type: IntraBlueprintTransitionType
-    ) : ChangeBounds() {
-        init {
-            if (type != IntraBlueprintTransitionType.NoTransition) {
-                duration = CLOCK_OUT_MILLIS
-                interpolator = CLOCK_OUT_INTERPOLATOR
-            } else {
-                duration = 0
+            fun assignAnimValues(src: String, alpha: Float, fract: Float, vis: Int? = null) {
+                val bounds = computeBounds(fract)
+                if (DEBUG) Log.i(TAG, "$src: $toView; alpha=$alpha; vis=$vis; bounds=$bounds;")
+                toView.setVisibility(vis ?: View.VISIBLE)
+                toView.setAlpha(alpha)
+                toView.setRect(bounds)
             }
-            if (viewModel.useLargeClock) {
-                addTarget(R.id.lockscreen_clock_view)
-            } else {
-                viewModel.clock?.let { it.largeClock.layout.views.forEach { addTarget(it) } }
-            }
-        }
-    }
 
-    class SmartspaceChangeBounds(
-        viewModel: KeyguardClockViewModel,
-        val type: IntraBlueprintTransitionType = IntraBlueprintTransitionType.DefaultTransition
-    ) : ChangeBounds() {
-        init {
-            if (type != IntraBlueprintTransitionType.NoTransition) {
-                duration =
-                    if (viewModel.useLargeClock) {
-                        STATUS_AREA_MOVE_UP_MILLIS
-                    } else {
-                        STATUS_AREA_MOVE_DOWN_MILLIS
+            if (DEBUG) {
+                Log.i(
+                    TAG,
+                    "transitioning: $toView; " +
+                        "vis: $fromVis -> $toVis; " +
+                        "alpha: $fromAlpha -> $toAlpha; " +
+                        "bounds: $fromBounds -> $toBounds; "
+                )
+            }
+
+            return ValueAnimator.ofFloat(fromAlpha, toAlpha).also { anim ->
+                // We enforce the animation parameters on the target view every frame using a
+                // predraw listener. This is suboptimal but prevents issues with layout passes
+                // overwriting the animation for individual frames.
+                val predrawCallback = OnPreDrawListener {
+                    assignAnimValues("predraw", anim.animatedValue as Float, anim.animatedFraction)
+                    return@OnPreDrawListener true
+                }
+
+                anim.duration = duration
+                anim.startDelay = startDelay
+                anim.interpolator = interpolator
+                anim.addListener(
+                    object : AnimatorListenerAdapter() {
+                        override fun onAnimationStart(anim: Animator) {
+                            assignAnimValues("start", fromAlpha, 0f)
+                        }
+
+                        override fun onAnimationEnd(anim: Animator) {
+                            assignAnimValues("end", toAlpha, 1f, toVis)
+                            if (sendToBack) toView.translationZ = 0f
+                            toView.viewTreeObserver.removeOnPreDrawListener(predrawCallback)
+                        }
                     }
-                interpolator = Interpolators.EMPHASIZED
-            } else {
-                duration = 0
+                )
+                toView.viewTreeObserver.addOnPreDrawListener(predrawCallback)
             }
+        }
+
+        companion object {
+            private const val PROP_VISIBILITY = "ClockSizeTransition:Visibility"
+            private const val PROP_ALPHA = "ClockSizeTransition:Alpha"
+            private const val PROP_BOUNDS = "ClockSizeTransition:Bounds"
+            private const val SMARTSPACE_BOUNDS = "ClockSizeTransition:SSBounds"
+            private val TRANSITION_PROPERTIES =
+                arrayOf(PROP_VISIBILITY, PROP_ALPHA, PROP_BOUNDS, SMARTSPACE_BOUNDS)
+
+            private val DEBUG = true
+            private val TAG = ClockFaceInTransition::class.simpleName!!
+        }
+    }
+
+    class ClockFaceInTransition(
+        config: IntraBlueprintTransition.Config,
+        val viewModel: KeyguardClockViewModel,
+    ) : VisibilityBoundsTransition() {
+        init {
+            duration = CLOCK_IN_MILLIS
+            startDelay = CLOCK_IN_START_DELAY_MILLIS
+            interpolator = CLOCK_IN_INTERPOLATOR
+            captureSmartspace = !viewModel.useLargeClock
+
+            if (viewModel.useLargeClock) {
+                viewModel.clock?.let { it.largeClock.layout.views.forEach { addTarget(it) } }
+            } else {
+                addTarget(R.id.lockscreen_clock_view)
+            }
+        }
+
+        override fun mutateBounds(
+            view: View,
+            fromVis: Int,
+            toVis: Int,
+            fromBounds: Rect,
+            toBounds: Rect,
+            fromSSBounds: Rect?,
+            toSSBounds: Rect?
+        ) {
+            fromBounds.left = toBounds.left
+            fromBounds.right = toBounds.right
+            if (viewModel.useLargeClock) {
+                // Large clock shouldn't move
+                fromBounds.top = toBounds.top
+                fromBounds.bottom = toBounds.bottom
+            } else if (toSSBounds != null && fromSSBounds != null) {
+                // Instead of moving the small clock the full distance, we compute the distance
+                // smartspace will move. We then scale this to match the duration of this animation
+                // so that the small clock moves at the same speed as smartspace.
+                val ssTranslation =
+                    abs((toSSBounds.top - fromSSBounds.top) * SMALL_CLOCK_IN_MOVE_SCALE).toInt()
+                fromBounds.top = toBounds.top - ssTranslation
+                fromBounds.bottom = toBounds.bottom - ssTranslation
+            } else {
+                Log.e(TAG, "mutateBounds: smallClock received no smartspace bounds")
+            }
+        }
+
+        companion object {
+            const val CLOCK_IN_MILLIS = 167L
+            const val CLOCK_IN_START_DELAY_MILLIS = 133L
+            val CLOCK_IN_INTERPOLATOR = Interpolators.LINEAR_OUT_SLOW_IN
+            const val SMALL_CLOCK_IN_MOVE_SCALE =
+                CLOCK_IN_MILLIS / SmartspaceMoveTransition.STATUS_AREA_MOVE_DOWN_MILLIS.toFloat()
+            private val TAG = ClockFaceInTransition::class.simpleName!!
+        }
+    }
+
+    class ClockFaceOutTransition(
+        config: IntraBlueprintTransition.Config,
+        val viewModel: KeyguardClockViewModel,
+    ) : VisibilityBoundsTransition() {
+        init {
+            duration = CLOCK_OUT_MILLIS
+            interpolator = CLOCK_OUT_INTERPOLATOR
+            captureSmartspace = viewModel.useLargeClock
+
+            if (viewModel.useLargeClock) {
+                addTarget(R.id.lockscreen_clock_view)
+            } else {
+                viewModel.clock?.let { it.largeClock.layout.views.forEach { addTarget(it) } }
+            }
+        }
+
+        override fun mutateBounds(
+            view: View,
+            fromVis: Int,
+            toVis: Int,
+            fromBounds: Rect,
+            toBounds: Rect,
+            fromSSBounds: Rect?,
+            toSSBounds: Rect?
+        ) {
+            toBounds.left = fromBounds.left
+            toBounds.right = fromBounds.right
+            if (!viewModel.useLargeClock) {
+                // Large clock shouldn't move
+                toBounds.top = fromBounds.top
+                toBounds.bottom = fromBounds.bottom
+            } else if (toSSBounds != null && fromSSBounds != null) {
+                // Instead of moving the small clock the full distance, we compute the distance
+                // smartspace will move. We then scale this to match the duration of this animation
+                // so that the small clock moves at the same speed as smartspace.
+                val ssTranslation =
+                    abs((toSSBounds.top - fromSSBounds.top) * SMALL_CLOCK_OUT_MOVE_SCALE).toInt()
+                toBounds.top = fromBounds.top - ssTranslation
+                toBounds.bottom = fromBounds.bottom - ssTranslation
+            } else {
+                Log.w(TAG, "mutateBounds: smallClock received no smartspace bounds")
+            }
+        }
+
+        companion object {
+            const val CLOCK_OUT_MILLIS = 133L
+            val CLOCK_OUT_INTERPOLATOR = Interpolators.LINEAR
+            const val SMALL_CLOCK_OUT_MOVE_SCALE =
+                CLOCK_OUT_MILLIS / SmartspaceMoveTransition.STATUS_AREA_MOVE_UP_MILLIS.toFloat()
+            private val TAG = ClockFaceOutTransition::class.simpleName!!
+        }
+    }
+
+    // TODO: Might need a mechanism to update this one while in-progress
+    class SmartspaceMoveTransition(
+        val config: IntraBlueprintTransition.Config,
+        viewModel: KeyguardClockViewModel,
+    ) : VisibilityBoundsTransition() {
+        init {
+            duration =
+                if (viewModel.useLargeClock) STATUS_AREA_MOVE_UP_MILLIS
+                else STATUS_AREA_MOVE_DOWN_MILLIS
+            interpolator = Interpolators.EMPHASIZED
             addTarget(sharedR.id.date_smartspace_view)
             addTarget(sharedR.id.weather_smartspace_view)
             addTarget(sharedR.id.bc_smartspace_view)
+
+            // Notifications normally and media on split shade needs to be moved
+            addTarget(R.id.aod_notification_icon_container)
+            addTarget(R.id.status_view_media_container)
         }
 
         companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/DefaultClockSteppingTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/DefaultClockSteppingTransition.kt
index c35dad7..60ab40c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/DefaultClockSteppingTransition.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/DefaultClockSteppingTransition.kt
@@ -24,12 +24,15 @@
 import com.android.app.animation.Interpolators
 import com.android.systemui.plugins.clocks.ClockController
 
-class DefaultClockSteppingTransition(private val clock: ClockController) : Transition() {
+class DefaultClockSteppingTransition(
+    private val clock: ClockController,
+) : Transition() {
     init {
         interpolator = Interpolators.LINEAR
         duration = KEYGUARD_STATUS_VIEW_CUSTOM_CLOCK_MOVE_DURATION_MS
         addTarget(clock.largeClock.view)
     }
+
     private fun captureValues(transitionValues: TransitionValues) {
         transitionValues.values[PROP_BOUNDS_LEFT] = transitionValues.view.left
         val locationInWindowTmp = IntArray(2)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt
index d22856b..edd3318 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBlueprintViewModel.kt
@@ -23,8 +23,10 @@
 
 class KeyguardBlueprintViewModel
 @Inject
-constructor(keyguardBlueprintInteractor: KeyguardBlueprintInteractor) {
+constructor(
+    keyguardBlueprintInteractor: KeyguardBlueprintInteractor,
+) {
     var currentBluePrint: KeyguardBlueprint? = null
     val blueprint = keyguardBlueprintInteractor.blueprint
-    val blueprintWithTransition = keyguardBlueprintInteractor.blueprintWithTransition
+    val refreshTransition = keyguardBlueprintInteractor.refreshTransition
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryTest.kt
index f2bd817..37836a5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryTest.kt
@@ -19,12 +19,18 @@
 
 package com.android.systemui.keyguard.data.repository
 
+import android.os.fakeExecutorHandler
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.ui.data.repository.ConfigurationRepository
+import com.android.systemui.concurrency.fakeExecutor
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.keyguard.ui.view.layout.blueprints.DefaultKeyguardBlueprint
 import com.android.systemui.keyguard.ui.view.layout.blueprints.DefaultKeyguardBlueprint.Companion.DEFAULT
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.android.systemui.util.ThreadAssert
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -45,17 +51,23 @@
     private lateinit var underTest: KeyguardBlueprintRepository
     @Mock lateinit var configurationRepository: ConfigurationRepository
     @Mock lateinit var defaultLockscreenBlueprint: DefaultKeyguardBlueprint
+    @Mock lateinit var threadAssert: ThreadAssert
     private val testScope = TestScope(StandardTestDispatcher())
+    private val kosmos: Kosmos = testKosmos()
 
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
-        whenever(defaultLockscreenBlueprint.id).thenReturn(DEFAULT)
-        underTest =
-            KeyguardBlueprintRepository(
-                configurationRepository,
-                setOf(defaultLockscreenBlueprint),
-            )
+        with(kosmos) {
+            whenever(defaultLockscreenBlueprint.id).thenReturn(DEFAULT)
+            underTest =
+                KeyguardBlueprintRepository(
+                    configurationRepository,
+                    setOf(defaultLockscreenBlueprint),
+                    fakeExecutorHandler,
+                    threadAssert,
+                )
+        }
     }
 
     @Test
@@ -88,13 +100,17 @@
 
     @Test
     fun testTransitionToSameBlueprint_refreshesBlueprint() =
-        testScope.runTest {
-            val refreshBlueprint by collectLastValue(underTest.refreshBluePrint)
-            runCurrent()
+        with(kosmos) {
+            testScope.runTest {
+                val transition by collectLastValue(underTest.refreshTransition)
+                fakeExecutor.runAllReady()
+                runCurrent()
 
-            underTest.applyBlueprint(defaultLockscreenBlueprint)
-            runCurrent()
+                underTest.applyBlueprint(defaultLockscreenBlueprint)
+                fakeExecutor.runAllReady()
+                runCurrent()
 
-            assertThat(refreshBlueprint).isNotNull()
+                assertThat(transition).isNotNull()
+            }
         }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt
index 8b16da2..9df00d3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorTest.kt
@@ -23,7 +23,9 @@
 import com.android.systemui.keyguard.data.repository.KeyguardBlueprintRepository
 import com.android.systemui.keyguard.ui.view.layout.blueprints.DefaultKeyguardBlueprint
 import com.android.systemui.keyguard.ui.view.layout.blueprints.SplitShadeKeyguardBlueprint
-import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransitionType
+import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition
+import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Config
+import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Type
 import com.android.systemui.statusbar.policy.SplitShadeStateController
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.whenever
@@ -47,8 +49,7 @@
     private lateinit var underTest: KeyguardBlueprintInteractor
     private lateinit var testScope: TestScope
 
-    val refreshBluePrint: MutableSharedFlow<Unit> = MutableSharedFlow(extraBufferCapacity = 1)
-    val refreshBlueprintTransition: MutableSharedFlow<IntraBlueprintTransitionType> =
+    val refreshTransition: MutableSharedFlow<IntraBlueprintTransition.Config> =
         MutableSharedFlow(extraBufferCapacity = 1)
 
     @Mock private lateinit var splitShadeStateController: SplitShadeStateController
@@ -59,9 +60,7 @@
         MockitoAnnotations.initMocks(this)
         testScope = TestScope(StandardTestDispatcher())
         whenever(keyguardBlueprintRepository.configurationChange).thenReturn(configurationFlow)
-        whenever(keyguardBlueprintRepository.refreshBluePrint).thenReturn(refreshBluePrint)
-        whenever(keyguardBlueprintRepository.refreshBlueprintTransition)
-            .thenReturn(refreshBlueprintTransition)
+        whenever(keyguardBlueprintRepository.refreshTransition).thenReturn(refreshTransition)
 
         underTest =
             KeyguardBlueprintInteractor(
@@ -116,8 +115,8 @@
 
     @Test
     fun testRefreshBlueprintWithTransition() {
-        underTest.refreshBlueprintWithTransition(IntraBlueprintTransitionType.DefaultTransition)
+        underTest.refreshBlueprint(Type.DefaultTransition)
         verify(keyguardBlueprintRepository)
-            .refreshBlueprintWithTransition(IntraBlueprintTransitionType.DefaultTransition)
+            .refreshBlueprint(Config(Type.DefaultTransition, true, true))
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt
index 2da74b0..08d44c1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt
@@ -138,10 +138,10 @@
         underTest.applyDefaultConstraints(cs)
 
         val expectedLargeClockTopMargin = LARGE_CLOCK_TOP
-        assetLargeClockTop(cs, expectedLargeClockTopMargin)
+        assertLargeClockTop(cs, expectedLargeClockTopMargin)
 
-        val expectedSmallClockTopMargin = SMALL_CLOCK_TOP_SPLIT_SHADE - CLOCK_FADE_TRANSLATION_Y
-        assetSmallClockTop(cs, expectedSmallClockTopMargin)
+        val expectedSmallClockTopMargin = SMALL_CLOCK_TOP_SPLIT_SHADE
+        assertSmallClockTop(cs, expectedSmallClockTopMargin)
     }
 
     @Test
@@ -152,10 +152,10 @@
         underTest.applyDefaultConstraints(cs)
 
         val expectedLargeClockTopMargin = LARGE_CLOCK_TOP
-        assetLargeClockTop(cs, expectedLargeClockTopMargin)
+        assertLargeClockTop(cs, expectedLargeClockTopMargin)
 
-        val expectedSmallClockTopMargin = SMALL_CLOCK_TOP_NON_SPLIT_SHADE - CLOCK_FADE_TRANSLATION_Y
-        assetSmallClockTop(cs, expectedSmallClockTopMargin)
+        val expectedSmallClockTopMargin = SMALL_CLOCK_TOP_NON_SPLIT_SHADE
+        assertSmallClockTop(cs, expectedSmallClockTopMargin)
     }
 
     @Test
@@ -166,10 +166,10 @@
         underTest.applyDefaultConstraints(cs)
 
         val expectedLargeClockTopMargin = LARGE_CLOCK_TOP
-        assetLargeClockTop(cs, expectedLargeClockTopMargin)
+        assertLargeClockTop(cs, expectedLargeClockTopMargin)
 
         val expectedSmallClockTopMargin = SMALL_CLOCK_TOP_SPLIT_SHADE
-        assetSmallClockTop(cs, expectedSmallClockTopMargin)
+        assertSmallClockTop(cs, expectedSmallClockTopMargin)
     }
 
     @Test
@@ -179,10 +179,10 @@
         val cs = ConstraintSet()
         underTest.applyDefaultConstraints(cs)
         val expectedLargeClockTopMargin = LARGE_CLOCK_TOP
-        assetLargeClockTop(cs, expectedLargeClockTopMargin)
+        assertLargeClockTop(cs, expectedLargeClockTopMargin)
 
         val expectedSmallClockTopMargin = SMALL_CLOCK_TOP_NON_SPLIT_SHADE
-        assetSmallClockTop(cs, expectedSmallClockTopMargin)
+        assertSmallClockTop(cs, expectedSmallClockTopMargin)
     }
 
     @Test
@@ -228,16 +228,22 @@
             .thenReturn(isInSplitShade)
     }
 
-    private fun assetLargeClockTop(cs: ConstraintSet, expectedLargeClockTopMargin: Int) {
+    private fun assertLargeClockTop(cs: ConstraintSet, expectedLargeClockTopMargin: Int) {
         val largeClockConstraint = cs.getConstraint(R.id.lockscreen_clock_view_large)
         assertThat(largeClockConstraint.layout.topToTop).isEqualTo(ConstraintSet.PARENT_ID)
         assertThat(largeClockConstraint.layout.topMargin).isEqualTo(expectedLargeClockTopMargin)
     }
 
-    private fun assetSmallClockTop(cs: ConstraintSet, expectedSmallClockTopMargin: Int) {
+    private fun assertSmallClockTop(cs: ConstraintSet, expectedSmallClockTopMargin: Int) {
+        val smallClockGuidelineConstraint = cs.getConstraint(R.id.small_clock_guideline_top)
+        assertThat(smallClockGuidelineConstraint.layout.topToTop).isEqualTo(-1)
+        assertThat(smallClockGuidelineConstraint.layout.guideBegin)
+            .isEqualTo(expectedSmallClockTopMargin)
+
         val smallClockConstraint = cs.getConstraint(R.id.lockscreen_clock_view)
-        assertThat(smallClockConstraint.layout.topToTop).isEqualTo(ConstraintSet.PARENT_ID)
-        assertThat(smallClockConstraint.layout.topMargin).isEqualTo(expectedSmallClockTopMargin)
+        assertThat(smallClockConstraint.layout.topToBottom)
+            .isEqualTo(R.id.small_clock_guideline_top)
+        assertThat(smallClockConstraint.layout.topMargin).isEqualTo(0)
     }
 
     companion object {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryKosmos.kt
index 19cd950..8452963 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryKosmos.kt
@@ -16,17 +16,22 @@
 
 package com.android.systemui.keyguard.data.repository
 
+import android.os.fakeExecutorHandler
 import com.android.systemui.common.ui.data.repository.configurationRepository
 import com.android.systemui.keyguard.shared.model.KeyguardBlueprint
 import com.android.systemui.keyguard.shared.model.KeyguardSection
 import com.android.systemui.keyguard.ui.view.layout.blueprints.DefaultKeyguardBlueprint.Companion.DEFAULT
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.ThreadAssert
+import com.android.systemui.util.mockito.mock
 
 val Kosmos.keyguardBlueprintRepository by
     Kosmos.Fixture {
         KeyguardBlueprintRepository(
             configurationRepository = configurationRepository,
             blueprints = setOf(defaultBlueprint),
+            handler = fakeExecutorHandler,
+            assert = mock<ThreadAssert>(),
         )
     }