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

Commit b4ce9161 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.

Test: added unit and integration tests for the new code that exposes the
unfoldProgress
Test: manually verified that gently folding up the device correctly
slides the elements of the split shade into the center (true for the
left-hand QS side and status bar, but needs more work for the
notifications side, even though the code applies the translation it's
not happening yet)
Flag: ACONFIG com.android.systemui.scene_container DEVELOPMENT
Bug: 330483283

Change-Id: I91db7922e824ee106faa96709b069f5b862aa7de
parent 49447103
Loading
Loading
Loading
Loading
+77 −0
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() },
    )
}
+27 −2
Original line number Diff line number Diff line
@@ -65,6 +65,8 @@ import com.android.compose.animation.scene.animateSceneFloatAsState
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
@@ -289,6 +291,7 @@ 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 navBarBottomHeight = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
    val density = LocalDensity.current
@@ -337,10 +340,23 @@ private fun SceneScope.SplitShade(
                modifier =
                    Modifier.padding(horizontal = Shade.Dimensions.HorizontalPadding)
                        .then(brightnessMirrorShowingModifier)
                        .unfoldHorizontalPadding(
                            fullPadding = dimensionResource(R.dimen.notification_side_paddings),
                        ) {
                            unfoldTransitionProgress
                        }
            )

            Row(modifier = Modifier.fillMaxWidth().weight(1f)) {
                Box(modifier = Modifier.weight(1f)) {
                Box(
                    modifier =
                        Modifier.weight(1f).unfoldTranslation(
                            startSide = true,
                            fullTranslation = dimensionResource(R.dimen.notification_side_paddings),
                        ) {
                            unfoldTransitionProgress
                        },
                ) {
                    BrightnessMirror(
                        viewModel = viewModel.brightnessMirrorViewModel,
                        qsSceneAdapter = viewModel.qsSceneAdapter,
@@ -407,7 +423,16 @@ private fun SceneScope.SplitShade(
                        Modifier.weight(1f)
                            .fillMaxHeight()
                            .padding(bottom = navBarBottomHeight)
                            .then(brightnessMirrorShowingModifier),
                            .then(brightnessMirrorShowingModifier)
                            .unfoldTranslation(
                                startSide = false,
                                fullTranslation =
                                    dimensionResource(
                                        R.dimen.notification_side_paddings,
                                    ),
                            ) {
                                unfoldTransitionProgress
                            },
                )
            }
        }
+2 −0
Original line number Diff line number Diff line
@@ -93,6 +93,7 @@ import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnec
import com.android.systemui.statusbar.policy.domain.interactor.deviceProvisioningInteractor
import com.android.systemui.telephony.data.repository.fakeTelephonyRepository
import com.android.systemui.testKosmos
import com.android.systemui.unfold.domain.interactor.unfoldTransitionInteractor
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
@@ -266,6 +267,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() {
                footerActionsController = kosmos.footerActionsController,
                footerActionsViewModelFactory = kosmos.footerActionsViewModelFactory,
                sceneInteractor = sceneInteractor,
                unfoldTransitionInteractor = kosmos.unfoldTransitionInteractor,
            )

        val displayTracker = FakeDisplayTracker(context)
+25 −0
Original line number Diff line number Diff line
@@ -55,6 +55,8 @@ import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsVi
import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
import com.android.systemui.testKosmos
import com.android.systemui.unfold.domain.interactor.unfoldTransitionInteractor
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
@@ -134,6 +136,7 @@ class ShadeSceneViewModelTest : SysuiTestCase() {
                footerActionsViewModelFactory = kosmos.footerActionsViewModelFactory,
                footerActionsController = kosmos.footerActionsController,
                sceneInteractor = kosmos.sceneInteractor,
                unfoldTransitionInteractor = kosmos.unfoldTransitionInteractor,
            )
    }

@@ -297,4 +300,26 @@ class ShadeSceneViewModelTest : SysuiTestCase() {
            shadeRepository.setShadeMode(ShadeMode.Split)
            assertThat(shadeMode).isEqualTo(ShadeMode.Split)
        }

    @Test
    fun unfoldTransitionProgress() =
        testScope.runTest {
            val unfoldProvider = kosmos.fakeUnfoldTransitionProgressProvider
            val progress by collectLastValue(underTest.unfoldTransitionProgress)

            unfoldProvider.onTransitionStarted()
            assertThat(progress).isEqualTo(1f)

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

            unfoldProvider.onTransitionFinishing()
            assertThat(progress).isEqualTo(1f)

            unfoldProvider.onTransitionFinished()
            assertThat(progress).isEqualTo(1f)
        }
}
+33 −22
Original line number Diff line number Diff line
@@ -15,42 +15,31 @@
 */
package com.android.systemui.unfold.domain.interactor

import android.testing.AndroidTestingRunner
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.unfold.TestUnfoldTransitionProvider
import com.android.systemui.unfold.data.repository.UnfoldTransitionRepositoryImpl
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
import com.android.systemui.testKosmos
import com.android.systemui.unfold.fakeUnfoldTransitionProgressProvider
import com.google.common.truth.Truth.assertThat
import java.util.Optional
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.async
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.MockitoAnnotations

@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidTestingRunner::class)
open class UnfoldTransitionInteractorTest : SysuiTestCase() {
@RunWith(AndroidJUnit4::class)
class UnfoldTransitionInteractorTest : SysuiTestCase() {

    private val testScope = TestScope()
    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope
    private val unfoldTransitionProgressProvider = kosmos.fakeUnfoldTransitionProgressProvider

    private val unfoldTransitionProgressProvider = TestUnfoldTransitionProvider()
    private val unfoldTransitionRepository =
        UnfoldTransitionRepositoryImpl(Optional.of(unfoldTransitionProgressProvider))

    private lateinit var underTest: UnfoldTransitionInteractor

    @Before
    fun setUp() {
        MockitoAnnotations.initMocks(this)

        underTest = UnfoldTransitionInteractorImpl(unfoldTransitionRepository)
    }
    private val underTest: UnfoldTransitionInteractor = kosmos.unfoldTransitionInteractor

    @Test
    fun waitForTransitionFinish_noEvents_doesNotComplete() =
@@ -88,4 +77,26 @@ open class UnfoldTransitionInteractorTest : SysuiTestCase() {
            assertThat(deferred.isCompleted).isFalse()
            deferred.cancel()
        }

    @Test
    fun unfoldProgress() =
        testScope.runTest {
            val progress by collectLastValue(underTest.unfoldProgress)
            runCurrent()

            unfoldTransitionProgressProvider.onTransitionStarted()
            assertThat(progress).isEqualTo(1f)

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

            unfoldTransitionProgressProvider.onTransitionFinishing()
            assertThat(progress).isEqualTo(1f)

            unfoldTransitionProgressProvider.onTransitionFinished()
            assertThat(progress).isEqualTo(1f)
        }
}
Loading