diff options
| author | 2024-03-23 17:03:02 +0000 | |
|---|---|---|
| committer | 2024-03-23 17:03:02 +0000 | |
| commit | 6b4be2fa055578f03361b70c4db67a7556a72b43 (patch) | |
| tree | ae92571a45c47502439c4c4d869caaaee0c13b90 | |
| parent | c5be730a2de6377b8248ad279a65e217629b1962 (diff) | |
| parent | 4f47e3bbbcd1cdcf5448292323b758bdc8698d6a (diff) | |
Merge "Add stepping animation to clock." into main
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 + } } |