summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Aaron Liu <aaronjli@google.com> 2024-03-23 17:03:02 +0000
committer Android (Google) Code Review <android-gerrit@google.com> 2024-03-23 17:03:02 +0000
commit6b4be2fa055578f03361b70c4db67a7556a72b43 (patch)
treeae92571a45c47502439c4c4d869caaaee0c13b90
parentc5be730a2de6377b8248ad279a65e217629b1962 (diff)
parent4f47e3bbbcd1cdcf5448292323b758bdc8698d6a (diff)
Merge "Add stepping animation to clock." into main
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ClockTransition.kt3
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt38
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt63
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt73
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt10
-rw-r--r--packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt11
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt11
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelWithKosmosTest.kt51
8 files changed, 225 insertions, 35 deletions
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ClockTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ClockTransition.kt
index a12f0990b581..acd9e3dc83cb 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ClockTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ClockTransition.kt
@@ -16,6 +16,7 @@
package com.android.systemui.keyguard.ui.composable.blueprint
+import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.SceneKey
@@ -39,7 +40,7 @@ object ClockTransition {
transitioningToSmallClock()
}
from(ClockScenes.splitShadeLargeClockScene, to = ClockScenes.largeClockScene) {
- spec = tween(1000)
+ spec = tween(1000, easing = LinearEasing)
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt
index 2781f39fc479..1c938a6c19a5 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt
@@ -16,6 +16,7 @@
package com.android.systemui.keyguard.ui.composable.section
+import android.content.res.Resources
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
@@ -23,6 +24,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
@@ -36,6 +38,8 @@ import com.android.systemui.customization.R as customizationR
import com.android.systemui.customization.R
import com.android.systemui.keyguard.ui.composable.blueprint.ClockElementKeys.largeClockElementKey
import com.android.systemui.keyguard.ui.composable.blueprint.ClockElementKeys.smallClockElementKey
+import com.android.systemui.keyguard.ui.composable.blueprint.ClockScenes.largeClockScene
+import com.android.systemui.keyguard.ui.composable.blueprint.ClockScenes.splitShadeLargeClockScene
import com.android.systemui.keyguard.ui.composable.modifier.burnInAware
import com.android.systemui.keyguard.ui.composable.modifier.onTopPlacementChanged
import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel
@@ -95,6 +99,36 @@ constructor(
if (currentClock?.largeClock?.view == null) {
return
}
+
+ // Centering animation for clocks that have custom position animations.
+ LaunchedEffect(layoutState.currentTransition?.progress) {
+ val transition = layoutState.currentTransition ?: return@LaunchedEffect
+ if (currentClock?.largeClock?.config?.hasCustomPositionUpdatedAnimation != true) {
+ return@LaunchedEffect
+ }
+
+ // If we are not doing the centering animation, do not animate.
+ val progress =
+ if (transition.isTransitioningBetween(largeClockScene, splitShadeLargeClockScene)) {
+ transition.progress
+ } else {
+ 1f
+ }
+
+ val distance =
+ if (transition.toScene == splitShadeLargeClockScene) {
+ -getClockCenteringDistance()
+ } else {
+ getClockCenteringDistance()
+ }
+ .toFloat()
+ val largeClock = checkNotNull(currentClock).largeClock
+ largeClock.animations.onPositionUpdated(
+ distance = distance,
+ fraction = progress,
+ )
+ }
+
MovableElement(key = largeClockElementKey, modifier = modifier) {
content {
AndroidView(
@@ -120,4 +154,8 @@ constructor(
(clockView.parent as? ViewGroup)?.removeView(clockView)
addView(clockView)
}
+
+ fun getClockCenteringDistance(): Float {
+ return Resources.getSystem().displayMetrics.widthPixels / 4f
+ }
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt
index d72d5cad31b4..b4472fc15ac4 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt
@@ -16,12 +16,16 @@
package com.android.systemui.keyguard.ui.composable.section
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
@@ -31,11 +35,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import com.android.compose.animation.scene.SceneTransitionLayout
+import com.android.compose.modifiers.thenIf
import com.android.systemui.Flags
import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
-import com.android.systemui.keyguard.ui.composable.blueprint.ClockScenes
import com.android.systemui.keyguard.ui.composable.blueprint.ClockScenes.largeClockScene
import com.android.systemui.keyguard.ui.composable.blueprint.ClockScenes.smallClockScene
import com.android.systemui.keyguard.ui.composable.blueprint.ClockScenes.splitShadeLargeClockScene
@@ -63,6 +68,9 @@ constructor(
) {
val isLargeClockVisible by clockViewModel.isLargeClockVisible.collectAsState()
val currentClockLayout by clockViewModel.currentClockLayout.collectAsState()
+ val hasCustomPositionUpdatedAnimation by
+ clockViewModel.hasCustomPositionUpdatedAnimation.collectAsState()
+
val currentScene =
when (currentClockLayout) {
KeyguardClockViewModel.ClockLayout.SPLIT_SHADE_LARGE_CLOCK ->
@@ -94,12 +102,10 @@ constructor(
transitions = ClockTransition.defaultClockTransitions,
enableInterruptions = false,
) {
- scene(ClockScenes.splitShadeLargeClockScene) {
- Row(
- modifier = Modifier.fillMaxSize(),
- ) {
+ scene(splitShadeLargeClockScene) {
+ Box(modifier = Modifier.fillMaxSize()) {
Column(
- modifier = Modifier.fillMaxHeight().weight(weight = 1f),
+ modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
with(smartSpaceSection) {
@@ -108,8 +114,34 @@ constructor(
onTopChanged = burnIn.onSmartspaceTopChanged,
)
}
- with(clockSection) { LargeClock(modifier = Modifier.fillMaxWidth()) }
+
+ with(clockSection) {
+ LargeClock(
+ modifier =
+ Modifier.fillMaxSize().thenIf(
+ !hasCustomPositionUpdatedAnimation
+ ) {
+ // If we do not have a custom position animation, we want
+ // the clock to be on one half of the screen.
+ Modifier.offset {
+ IntOffset(
+ x =
+ -clockSection
+ .getClockCenteringDistance()
+ .toInt(),
+ y = 0,
+ )
+ }
+ }
+ )
+ }
}
+ }
+
+ Row(
+ modifier = Modifier.fillMaxSize(),
+ ) {
+ Spacer(modifier = Modifier.weight(weight = 1f))
with(notificationSection) {
Notifications(
modifier =
@@ -121,7 +153,7 @@ constructor(
}
}
- scene(ClockScenes.splitShadeSmallClockScene) {
+ scene(splitShadeSmallClockScene) {
Row(
modifier = Modifier.fillMaxSize(),
) {
@@ -133,7 +165,7 @@ constructor(
SmallClock(
burnInParams = burnIn.parameters,
onTopChanged = burnIn.onSmallClockTopChanged,
- modifier = Modifier.fillMaxWidth()
+ modifier = Modifier.wrapContentSize()
)
}
with(smartSpaceSection) {
@@ -155,13 +187,13 @@ constructor(
}
}
- scene(ClockScenes.smallClockScene) {
+ scene(smallClockScene) {
Column {
with(clockSection) {
SmallClock(
burnInParams = burnIn.parameters,
onTopChanged = burnIn.onSmallClockTopChanged,
- modifier = Modifier.fillMaxWidth()
+ modifier = Modifier.wrapContentSize()
)
}
with(smartSpaceSection) {
@@ -172,15 +204,12 @@ constructor(
}
with(mediaCarouselSection) { MediaCarousel() }
with(notificationSection) {
- Notifications(
- modifier =
- androidx.compose.ui.Modifier.fillMaxWidth().weight(weight = 1f)
- )
+ Notifications(modifier = Modifier.fillMaxWidth().weight(weight = 1f))
}
}
}
- scene(ClockScenes.largeClockScene) {
+ scene(largeClockScene) {
Column {
with(smartSpaceSection) {
SmartSpace(
@@ -188,7 +217,7 @@ constructor(
onTopChanged = burnIn.onSmartspaceTopChanged,
)
}
- with(clockSection) { LargeClock(modifier = Modifier.fillMaxWidth()) }
+ with(clockSection) { LargeClock(modifier = Modifier.fillMaxSize()) }
}
}
}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
index cea49e1b535e..11c946261816 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
@@ -517,24 +517,12 @@ class AnimatableClockView @JvmOverloads constructor(
val currentMoveAmount = left - clockStartLeft
val digitOffsetDirection = if (isLayoutRtl) -1 else 1
for (i in 0 until NUM_DIGITS) {
- // The delay for the digit, in terms of fraction (i.e. the digit should not move
- // during 0.0 - 0.1).
- val digitInitialDelay =
- if (isMovingToCenter) {
- moveToCenterDelays[i] * MOVE_DIGIT_STEP
- } else {
- moveToSideDelays[i] * MOVE_DIGIT_STEP
- }
val digitFraction =
- MOVE_INTERPOLATOR.getInterpolation(
- constrainedMap(
- 0.0f,
- 1.0f,
- digitInitialDelay,
- digitInitialDelay + AVAILABLE_ANIMATION_TIME,
- moveFraction
- )
- )
+ getDigitFraction(
+ digit = i,
+ isMovingToCenter = isMovingToCenter,
+ fraction = moveFraction,
+ )
val moveAmountForDigit = currentMoveAmount * digitFraction
val moveAmountDeltaForDigit = moveAmountForDigit - currentMoveAmount
glyphOffsets[i] = digitOffsetDirection * moveAmountDeltaForDigit
@@ -542,6 +530,57 @@ class AnimatableClockView @JvmOverloads constructor(
invalidate()
}
+ /**
+ * Offsets the glyphs of the clock for the step clock animation.
+ *
+ * The animation makes the glyphs of the clock move at different speeds, when the clock is
+ * moving horizontally. This method uses direction, distance, and fraction to determine offset.
+ *
+ * @param distance is the total distance in pixels to offset the glyphs when animation
+ * completes. Negative distance means we are animating the position towards the center.
+ * @param fraction fraction of the clock movement. 0 means it is at the beginning, and 1
+ * means it finished moving.
+ */
+ fun offsetGlyphsForStepClockAnimation(
+ distance: Float,
+ fraction: Float,
+ ) {
+ for (i in 0 until NUM_DIGITS) {
+ val dir = if (isLayoutRtl) -1 else 1
+ val digitFraction =
+ getDigitFraction(digit = i, isMovingToCenter = distance > 0, fraction = fraction)
+ val moveAmountForDigit = dir * distance * digitFraction
+ glyphOffsets[i] = moveAmountForDigit
+
+ if (distance > 0) {
+ // If distance > 0 then we are moving from the left towards the center.
+ // We need ensure that the glyphs are offset to the initial position.
+ glyphOffsets -= dir * distance
+ }
+ }
+ invalidate()
+ }
+
+ private fun getDigitFraction(digit: Int, isMovingToCenter: Boolean, fraction: Float): Float {
+ // The delay for the digit, in terms of fraction (i.e. the digit should not move
+ // during 0.0 - 0.1).
+ val digitInitialDelay =
+ if (isMovingToCenter) {
+ moveToCenterDelays[digit] * MOVE_DIGIT_STEP
+ } else {
+ moveToSideDelays[digit] * MOVE_DIGIT_STEP
+ }
+ return MOVE_INTERPOLATOR.getInterpolation(
+ constrainedMap(
+ 0.0f,
+ 1.0f,
+ digitInitialDelay,
+ digitInitialDelay + AVAILABLE_ANIMATION_TIME,
+ fraction,
+ )
+ )
+ }
+
// DateFormat.getBestDateTimePattern is extremely expensive, and refresh is called often.
// This is an optimization to ensure we only recompute the patterns when the inputs change.
private object Patterns {
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt
index 54c7a0823963..b39201427b46 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt
@@ -232,6 +232,10 @@ class DefaultClockController(
fun offsetGlyphsForStepClockAnimation(fromLeft: Int, direction: Int, fraction: Float) {
view.offsetGlyphsForStepClockAnimation(fromLeft, direction, fraction)
}
+
+ fun offsetGlyphsForStepClockAnimation(distance: Float, fraction: Float) {
+ view.offsetGlyphsForStepClockAnimation(distance, fraction)
+ }
}
inner class DefaultClockEvents : ClockEvents {
@@ -316,6 +320,8 @@ class DefaultClockController(
}
override fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) {}
+
+ override fun onPositionUpdated(distance: Float, fraction: Float) {}
}
inner class LargeClockAnimations(
@@ -326,6 +332,10 @@ class DefaultClockController(
override fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) {
largeClock.offsetGlyphsForStepClockAnimation(fromLeft, direction, fraction)
}
+
+ override fun onPositionUpdated(distance: Float, fraction: Float) {
+ largeClock.offsetGlyphsForStepClockAnimation(distance, fraction)
+ }
}
class AnimationState(
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt
index fd7a7f34d258..8e2bd9b2562b 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt
@@ -188,10 +188,21 @@ interface ClockAnimations {
* negative means left.
* @param fraction fraction of the clock movement. 0 means it is at the beginning, and 1 means
* it finished moving.
+ * @deprecated use {@link #onPositionUpdated(float, float)} instead.
*/
fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float)
/**
+ * Runs when the clock's position changed during the move animation.
+ *
+ * @param distance is the total distance in pixels to offset the glyphs when animation
+ * completes. Negative distance means we are animating the position towards the center.
+ * @param fraction fraction of the clock movement. 0 means it is at the beginning, and 1 means
+ * it finished moving.
+ */
+ fun onPositionUpdated(distance: Float, fraction: Float)
+
+ /**
* Runs when swiping clock picker, swipingFraction: 1.0 -> clock is scaled up in the preview,
* 0.0 -> clock is scaled down in the shade; previewRatio is previewSize / screenSize
*/
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
index 1c1c33ab7e7e..3d649512f342 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
@@ -130,6 +130,17 @@ constructor(
initialValue = ClockLayout.SMALL_CLOCK
)
+ val hasCustomPositionUpdatedAnimation: StateFlow<Boolean> =
+ combine(currentClock, isLargeClockVisible) { currentClock, isLargeClockVisible ->
+ isLargeClockVisible &&
+ currentClock?.largeClock?.config?.hasCustomPositionUpdatedAnimation == true
+ }
+ .stateIn(
+ scope = applicationScope,
+ started = SharingStarted.WhileSubscribed(),
+ initialValue = false
+ )
+
/** Calculates the top margin for the small clock. */
fun getSmallClockTopMargin(context: Context): Int {
var topMargin: Int
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelWithKosmosTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelWithKosmosTest.kt
index e53cd11ebe48..d12980a74a18 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelWithKosmosTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelWithKosmosTest.kt
@@ -20,17 +20,23 @@ import androidx.test.filters.SmallTest
import com.android.keyguard.KeyguardClockSwitch
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyguard.data.repository.fakeKeyguardClockRepository
import com.android.systemui.keyguard.data.repository.keyguardClockRepository
import com.android.systemui.keyguard.data.repository.keyguardRepository
import com.android.systemui.kosmos.testScope
+import com.android.systemui.plugins.clocks.ClockController
+import com.android.systemui.plugins.clocks.ClockFaceConfig
+import com.android.systemui.plugins.clocks.ClockFaceController
import com.android.systemui.shade.data.repository.shadeRepository
import com.android.systemui.shade.shared.model.ShadeMode
import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlin.test.Test
import kotlinx.coroutines.test.runTest
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
+import org.mockito.Mockito.mock
@SmallTest
@RunWith(JUnit4::class)
@@ -98,4 +104,49 @@ class KeyguardClockViewModelWithKosmosTest : SysuiTestCase() {
val currentClockLayout by collectLastValue(underTest.currentClockLayout)
assertThat(currentClockLayout).isEqualTo(KeyguardClockViewModel.ClockLayout.LARGE_CLOCK)
}
+
+ @Test
+ fun hasCustomPositionUpdatedAnimation_withConfigTrue_isTrue() =
+ testScope.runTest {
+ with(kosmos) {
+ keyguardClockRepository.setClockSize(KeyguardClockSwitch.LARGE)
+ fakeKeyguardClockRepository.setCurrentClock(
+ buildClockController(hasCustomPositionUpdatedAnimation = true)
+ )
+ }
+
+ val hasCustomPositionUpdatedAnimation by
+ collectLastValue(underTest.hasCustomPositionUpdatedAnimation)
+ assertThat(hasCustomPositionUpdatedAnimation).isEqualTo(true)
+ }
+
+ @Test
+ fun hasCustomPositionUpdatedAnimation_withConfigFalse_isFalse() =
+ testScope.runTest {
+ with(kosmos) {
+ keyguardClockRepository.setClockSize(KeyguardClockSwitch.LARGE)
+ fakeKeyguardClockRepository.setCurrentClock(
+ buildClockController(hasCustomPositionUpdatedAnimation = false)
+ )
+ }
+
+ val hasCustomPositionUpdatedAnimation by
+ collectLastValue(underTest.hasCustomPositionUpdatedAnimation)
+ assertThat(hasCustomPositionUpdatedAnimation).isEqualTo(false)
+ }
+
+ private fun buildClockController(
+ hasCustomPositionUpdatedAnimation: Boolean = false
+ ): ClockController {
+ val clockController = mock(ClockController::class.java)
+ val largeClock = mock(ClockFaceController::class.java)
+ val config = mock(ClockFaceConfig::class.java)
+
+ whenever(clockController.largeClock).thenReturn(largeClock)
+ whenever(largeClock.config).thenReturn(config)
+ whenever(config.hasCustomPositionUpdatedAnimation)
+ .thenReturn(hasCustomPositionUpdatedAnimation)
+
+ return clockController
+ }
}