Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 7320f0f6 authored by Alejandro Nijamkin's avatar Alejandro Nijamkin
Browse files

[flexiglass] Unfold transition support in the shade scene.

When a foldable is fully unfolded and then the user begins to fold it
up, there's a subtle animation that happens to elements across the
screen. Left-hand side elements move to the right and right-hand side
elements move to the left, seeming to gently float towards the fold
hinge.

This CL adds that for Flexiglass, only for the (split) shade scene and
only for the NSSL, which didn't have that until now.

Test: updated unit and integration tests for the new code that exposes the
unfoldTranslationX
Test: manually verified that gently folding up the device correctly
slides the elements of the split shade into the center (both left-hand
side elements and the NSSL on the right)
Flag: ACONFIG com.android.systemui.scene_container DEVELOPMENT
Bug: 330483283

Change-Id: I10a9f941110e484dc5a81322558611b422a1a9d8
parent b4ce9161
Loading
Loading
Loading
Loading
+0 −77
Original line number Diff line number Diff line
/*
 * 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.fold.ui.composable

import androidx.annotation.FloatRange
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
import com.android.compose.modifiers.padding
import kotlin.math.roundToInt

/**
 * Applies a translation that feeds off of the unfold transition that's active while the device is
 * being folded or unfolded, effectively shifting the element towards the fold hinge.
 *
 * @param startSide `true` if the affected element is on the start side (left-hand side in
 *   left-to-right layouts), `false` otherwise.
 * @param fullTranslation The maximum translation to apply when the element is the most shifted. The
 *   modifier will never apply more than this much translation on the element.
 * @param unfoldProgress A provider for the amount of progress of the unfold transition. This should
 *   be sourced from the `UnfoldTransitionInteractor`, ideally through a view-model.
 */
@Composable
fun Modifier.unfoldTranslation(
    startSide: Boolean,
    fullTranslation: Dp,
    @FloatRange(from = 0.0, to = 1.0) unfoldProgress: () -> Float,
): Modifier {
    val translateToTheRight = startSide && LocalLayoutDirection.current == LayoutDirection.Ltr
    return this.graphicsLayer {
        translationX =
            fullTranslation.toPx() *
                if (translateToTheRight) {
                    1 - unfoldProgress()
                } else {
                    unfoldProgress() - 1
                }
    }
}

/**
 * Applies horizontal padding that feeds off of the unfold transition that's active while the device
 * is being folded or unfolded, effectively "squishing" the element on both sides.
 *
 * This is horizontal padding so it's applied on both the start and end sides of the element.
 *
 * @param fullPadding The maximum padding to apply when the element is the most padded. The modifier
 *   will never apply more than this much horizontal padding on the element.
 * @param unfoldProgress A provider for the amount of progress of the unfold transition. This should
 *   be sourced from the `UnfoldTransitionInteractor`, ideally through a view-model.
 */
@Composable
fun Modifier.unfoldHorizontalPadding(
    fullPadding: Dp,
    @FloatRange(from = 0.0, to = 1.0) unfoldProgress: () -> Float,
): Modifier {
    return this.padding(
        horizontal = { (fullPadding.toPx() * (1 - unfoldProgress())).roundToInt() },
    )
}
+18 −22
Original line number Diff line number Diff line
@@ -62,11 +62,10 @@ import com.android.compose.animation.scene.TransitionState
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
import com.android.compose.animation.scene.animateSceneFloatAsState
import com.android.compose.modifiers.padding
import com.android.compose.modifiers.thenIf
import com.android.systemui.battery.BatteryMeterViewController
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.fold.ui.composable.unfoldHorizontalPadding
import com.android.systemui.fold.ui.composable.unfoldTranslation
import com.android.systemui.media.controls.ui.composable.MediaCarousel
import com.android.systemui.media.controls.ui.controller.MediaCarouselController
import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager
@@ -291,7 +290,18 @@ private fun SceneScope.SplitShade(
        remember(lifecycleOwner, viewModel) { viewModel.getFooterActionsViewModel(lifecycleOwner) }
    val tileSquishiness by
        animateSceneFloatAsState(value = 1f, key = QuickSettings.SharedValues.TilesSquishiness)
    val unfoldTransitionProgress by viewModel.unfoldTransitionProgress.collectAsState()
    val unfoldTranslationXForStartSide by
        viewModel
            .unfoldTranslationX(
                isOnStartSide = true,
            )
            .collectAsState(0f)
    val unfoldTranslationXForEndSide by
        viewModel
            .unfoldTranslationX(
                isOnStartSide = false,
            )
            .collectAsState(0f)

    val navBarBottomHeight = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
    val density = LocalDensity.current
@@ -340,21 +350,16 @@ private fun SceneScope.SplitShade(
                modifier =
                    Modifier.padding(horizontal = Shade.Dimensions.HorizontalPadding)
                        .then(brightnessMirrorShowingModifier)
                        .unfoldHorizontalPadding(
                            fullPadding = dimensionResource(R.dimen.notification_side_paddings),
                        ) {
                            unfoldTransitionProgress
                        }
                        .padding(
                            horizontal = { unfoldTranslationXForStartSide.roundToInt() },
                        )
            )

            Row(modifier = Modifier.fillMaxWidth().weight(1f)) {
                Box(
                    modifier =
                        Modifier.weight(1f).unfoldTranslation(
                            startSide = true,
                            fullTranslation = dimensionResource(R.dimen.notification_side_paddings),
                        ) {
                            unfoldTransitionProgress
                        Modifier.weight(1f).graphicsLayer {
                            translationX = unfoldTranslationXForStartSide
                        },
                ) {
                    BrightnessMirror(
@@ -424,15 +429,6 @@ private fun SceneScope.SplitShade(
                            .fillMaxHeight()
                            .padding(bottom = navBarBottomHeight)
                            .then(brightnessMirrorShowingModifier)
                            .unfoldTranslation(
                                startSide = false,
                                fullTranslation =
                                    dimensionResource(
                                        R.dimen.notification_side_paddings,
                                    ),
                            ) {
                                unfoldTransitionProgress
                            },
                )
            }
        }
+42 −6
Original line number Diff line number Diff line
@@ -24,6 +24,7 @@ import com.android.compose.animation.scene.SwipeDirection
import com.android.systemui.SysuiTestCase
import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
@@ -60,7 +61,9 @@ import com.android.systemui.unfold.fakeUnfoldTransitionProgressProvider
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import java.util.Locale
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -304,22 +307,55 @@ class ShadeSceneViewModelTest : SysuiTestCase() {
    @Test
    fun unfoldTransitionProgress() =
        testScope.runTest {
            val unfoldProvider = kosmos.fakeUnfoldTransitionProgressProvider
            val progress by collectLastValue(underTest.unfoldTransitionProgress)
            val maxTranslation = prepareConfiguration()
            val translations by
                collectLastValue(
                    combine(
                        underTest.unfoldTranslationX(isOnStartSide = true),
                        underTest.unfoldTranslationX(isOnStartSide = false),
                    ) { start, end ->
                        Translations(
                            start = start,
                            end = end,
                        )
                    }
                )

            val unfoldProvider = kosmos.fakeUnfoldTransitionProgressProvider
            unfoldProvider.onTransitionStarted()
            assertThat(progress).isEqualTo(1f)
            assertThat(translations?.start).isEqualTo(0f)
            assertThat(translations?.end).isEqualTo(-0f)

            repeat(10) { repetition ->
                val transitionProgress = 0.1f * (repetition + 1)
                unfoldProvider.onTransitionProgress(transitionProgress)
                assertThat(progress).isEqualTo(transitionProgress)
                assertThat(translations?.start).isEqualTo((1 - transitionProgress) * maxTranslation)
                assertThat(translations?.end).isEqualTo(-(1 - transitionProgress) * maxTranslation)
            }

            unfoldProvider.onTransitionFinishing()
            assertThat(progress).isEqualTo(1f)
            assertThat(translations?.start).isEqualTo(0f)
            assertThat(translations?.end).isEqualTo(-0f)

            unfoldProvider.onTransitionFinished()
            assertThat(progress).isEqualTo(1f)
            assertThat(translations?.start).isEqualTo(0f)
            assertThat(translations?.end).isEqualTo(-0f)
        }

    private fun prepareConfiguration(): Int {
        val configuration = context.resources.configuration
        configuration.setLayoutDirection(Locale.US)
        kosmos.fakeConfigurationRepository.onConfigurationChange(configuration)
        val maxTranslation = 10
        kosmos.fakeConfigurationRepository.setDimensionPixelSize(
            R.dimen.notification_side_paddings,
            maxTranslation
        )
        return maxTranslation
    }

    private data class Translations(
        val start: Float,
        val end: Float,
    )
}
+89 −7
Original line number Diff line number Diff line
@@ -18,13 +18,17 @@ package com.android.systemui.unfold.domain.interactor
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
import com.android.systemui.res.R
import com.android.systemui.testKosmos
import com.android.systemui.unfold.fakeUnfoldTransitionProgressProvider
import com.google.common.truth.Truth.assertThat
import java.util.Locale
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Test
@@ -79,24 +83,102 @@ class UnfoldTransitionInteractorTest : SysuiTestCase() {
        }

    @Test
    fun unfoldProgress() =
    fun unfoldTranslationX_leftToRight() =
        testScope.runTest {
            val progress by collectLastValue(underTest.unfoldProgress)
            val maxTranslation = prepareConfiguration(isLeftToRight = true)
            val translations by
                collectLastValue(
                    combine(
                        underTest.unfoldTranslationX(isOnStartSide = true),
                        underTest.unfoldTranslationX(isOnStartSide = false),
                    ) { start, end ->
                        Translations(
                            start = start,
                            end = end,
                        )
                    }
                )
            runCurrent()

            unfoldTransitionProgressProvider.onTransitionStarted()
            assertThat(progress).isEqualTo(1f)
            assertThat(translations?.start).isEqualTo(0f)
            assertThat(translations?.end).isEqualTo(-0f)

            repeat(10) { repetition ->
                val transitionProgress = 0.1f * (repetition + 1)
                val transitionProgress = 1 - 0.1f * (repetition + 1)
                unfoldTransitionProgressProvider.onTransitionProgress(transitionProgress)
                assertThat(progress).isEqualTo(transitionProgress)
                assertThat(translations?.start).isEqualTo((1 - transitionProgress) * maxTranslation)
                assertThat(translations?.end).isEqualTo(-(1 - transitionProgress) * maxTranslation)
            }

            unfoldTransitionProgressProvider.onTransitionFinishing()
            assertThat(progress).isEqualTo(1f)
            assertThat(translations?.start).isEqualTo(maxTranslation)
            assertThat(translations?.end).isEqualTo(-maxTranslation)

            unfoldTransitionProgressProvider.onTransitionFinished()
            assertThat(progress).isEqualTo(1f)
            assertThat(translations?.start).isEqualTo(0f)
            assertThat(translations?.end).isEqualTo(-0f)
        }

    @Test
    fun unfoldTranslationX_rightToLeft() =
        testScope.runTest {
            val maxTranslation = prepareConfiguration(isLeftToRight = false)
            val translations by
                collectLastValue(
                    combine(
                        underTest.unfoldTranslationX(isOnStartSide = true),
                        underTest.unfoldTranslationX(isOnStartSide = false),
                    ) { start, end ->
                        Translations(
                            start = start,
                            end = end,
                        )
                    }
                )
            runCurrent()

            unfoldTransitionProgressProvider.onTransitionStarted()
            assertThat(translations?.start).isEqualTo(-0f)
            assertThat(translations?.end).isEqualTo(0f)

            repeat(10) { repetition ->
                val transitionProgress = 1 - 0.1f * (repetition + 1)
                unfoldTransitionProgressProvider.onTransitionProgress(transitionProgress)
                assertThat(translations?.start)
                    .isEqualTo(-(1 - transitionProgress) * maxTranslation)
                assertThat(translations?.end).isEqualTo((1 - transitionProgress) * maxTranslation)
            }

            unfoldTransitionProgressProvider.onTransitionFinishing()
            assertThat(translations?.start).isEqualTo(-maxTranslation)
            assertThat(translations?.end).isEqualTo(maxTranslation)

            unfoldTransitionProgressProvider.onTransitionFinished()
            assertThat(translations?.start).isEqualTo(-0f)
            assertThat(translations?.end).isEqualTo(0f)
        }

    private fun prepareConfiguration(
        isLeftToRight: Boolean,
    ): Int {
        val configuration = context.resources.configuration
        if (isLeftToRight) {
            configuration.setLayoutDirection(Locale.US)
        } else {
            configuration.setLayoutDirection(Locale("he", "il"))
        }
        kosmos.fakeConfigurationRepository.onConfigurationChange(configuration)
        val maxTranslation = 10
        kosmos.fakeConfigurationRepository.setDimensionPixelSize(
            R.dimen.notification_side_paddings,
            maxTranslation
        )
        return maxTranslation
    }

    private data class Translations(
        val start: Float,
        val end: Float,
    )
}
+7 −0
Original line number Diff line number Diff line
@@ -56,6 +56,13 @@ class ConfigurationInteractor @Inject constructor(private val repository: Config
    val naturalMaxBounds: Flow<Rect> =
        repository.configurationValues.map { it.naturalScreenBounds }.distinctUntilChanged()

    /**
     * The layout direction. Will be either `View#LAYOUT_DIRECTION_LTR` or
     * `View#LAYOUT_DIRECTION_RTL`.
     */
    val layoutDirection: Flow<Int> =
        repository.configurationValues.map { it.layoutDirection }.distinctUntilChanged()

    /** Given [resourceId], emit the dimension pixel size on config change */
    fun dimensionPixelSize(resourceId: Int): Flow<Int> {
        return onAnyConfigurationChange.mapLatest { repository.getDimensionPixelSize(resourceId) }
Loading