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

Commit 7ef3905b authored by Lucas Silva's avatar Lucas Silva
Browse files

Implement dream overlay motion when entering hub

This adds the x translation to the overlay as the hub is swiped in over
the dream.

Bug: 325102385
Test: atest SystemUiRoboTest:DreamingToGlanceableHubTransitionViewModelTest
Flag: ACONFIG com.android.systemui.communal_hub STAGING
Change-Id: Ifa2afa172afd2c7d645bc3bf82c96cb93deae6d9
parent ce17e6a1
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -8,7 +8,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.complication.ComplicationHostViewController
import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel
import com.android.systemui.dreams.ui.viewmodel.DreamOverlayViewModel
import com.android.systemui.log.core.FakeLogBuffer
import com.android.systemui.statusbar.BlurUtils
import com.android.systemui.statusbar.policy.ConfigurationController
@@ -47,7 +47,7 @@ class DreamOverlayAnimationsControllerTest : SysuiTestCase() {
    @Mock private lateinit var statusBarViewController: DreamOverlayStatusBarViewController
    @Mock private lateinit var stateController: DreamOverlayStateController
    @Mock private lateinit var configController: ConfigurationController
    @Mock private lateinit var transitionViewModel: DreamingToLockscreenTransitionViewModel
    @Mock private lateinit var transitionViewModel: DreamOverlayViewModel
    private val logBuffer = FakeLogBuffer.Factory.create()
    private lateinit var controller: DreamOverlayAnimationsController

+97 −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.keyguard.ui.viewmodel

import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectValues
import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.kosmos.testScope
import com.android.systemui.testKosmos
import com.google.common.collect.Range
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith

@SmallTest
@RunWith(AndroidJUnit4::class)
class DreamingToGlanceableHubTransitionViewModelTest : SysuiTestCase() {
    val kosmos = testKosmos()
    val testScope = kosmos.testScope

    val underTest by lazy { kosmos.dreamingToGlanceableHubTransitionViewModel }

    @Test
    fun dreamOverlayAlpha() =
        testScope.runTest {
            val values by collectValues(underTest.dreamOverlayAlpha)
            assertThat(values).isEmpty()

            kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps(
                listOf(
                    // Should start running here...
                    step(0f, TransitionState.STARTED),
                    step(0f),
                    step(0.1f),
                    step(0.5f),
                    // Up to here...
                    step(1f),
                ),
                testScope,
            )

            assertThat(values).hasSize(4)
            values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) }
        }

    @Test
    fun dreamOverlayTranslationX() =
        testScope.runTest {
            val values by collectValues(underTest.dreamOverlayTranslationX(100))
            assertThat(values).isEmpty()

            kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps(
                listOf(
                    step(0f, TransitionState.STARTED),
                    step(0.3f),
                    step(0.6f),
                ),
                testScope,
            )

            assertThat(values).hasSize(3)
            values.forEach { assertThat(it).isIn(Range.closed(-100f, 0f)) }
        }

    private fun step(
        value: Float,
        state: TransitionState = TransitionState.RUNNING
    ): TransitionStep {
        return TransitionStep(
            from = KeyguardState.DREAMING,
            to = KeyguardState.GLANCEABLE_HUB,
            value = value,
            transitionState = state,
            ownerName = "DreamingToGlanceableHubTransitionViewModelTest"
        )
    }
}
+1 −0
Original line number Diff line number Diff line
@@ -1857,6 +1857,7 @@
    <dimen name="dream_overlay_y_offset">80dp</dimen>
    <dimen name="dream_overlay_entry_y_offset">40dp</dimen>
    <dimen name="dream_overlay_exit_y_offset">40dp</dimen>
    <dimen name="dream_overlay_exit_x_offset">824dp</dimen>

    <dimen name="status_view_margin_horizontal">0dp</dimen>

+37 −59
Original line number Diff line number Diff line
@@ -33,20 +33,16 @@ import com.android.systemui.complication.ComplicationLayoutParams.POSITION_BOTTO
import com.android.systemui.complication.ComplicationLayoutParams.POSITION_TOP
import com.android.systemui.complication.ComplicationLayoutParams.Position
import com.android.systemui.dreams.dagger.DreamOverlayModule
import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel
import com.android.systemui.dreams.ui.viewmodel.DreamOverlayViewModel
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.core.Logger
import com.android.systemui.log.dagger.DreamLog
import com.android.systemui.res.R
import com.android.systemui.statusbar.BlurUtils
import com.android.systemui.statusbar.CrossFadeHelper
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener
import javax.inject.Inject
import javax.inject.Named
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.launch

/** Controller for dream overlay animations. */
@@ -58,7 +54,7 @@ constructor(
    private val mStatusBarViewController: DreamOverlayStatusBarViewController,
    private val mOverlayStateController: DreamOverlayStateController,
    @Named(DreamOverlayModule.DREAM_BLUR_RADIUS) private val mDreamBlurRadius: Int,
    private val transitionViewModel: DreamingToLockscreenTransitionViewModel,
    private val dreamOverlayViewModel: DreamOverlayViewModel,
    private val configController: ConfigurationController,
    @Named(DreamOverlayModule.DREAM_IN_BLUR_ANIMATION_DURATION)
    private val mDreamInBlurAnimDurationMs: Long,
@@ -91,37 +87,27 @@ constructor(
        this.view = view

        view.repeatWhenAttached {
            val configurationBasedDimensions = MutableStateFlow(loadFromResources(view))
            val configCallback =
                object : ConfigurationListener {
                    override fun onDensityOrFontScaleChanged() {
                        configurationBasedDimensions.value = loadFromResources(view)
            repeatOnLifecycle(Lifecycle.State.CREATED) {
                launch {
                    dreamOverlayViewModel.dreamOverlayTranslationY.collect { px ->
                        ComplicationLayoutParams.iteratePositions(
                            { position: Int -> setElementsTranslationYAtPosition(px, position) },
                            POSITION_TOP or POSITION_BOTTOM
                        )
                    }
                }

            configController.addCallback(configCallback)

            try {
                repeatOnLifecycle(Lifecycle.State.CREATED) {
                    /* Translation animations, when moving from DREAMING->LOCKSCREEN state */
                launch {
                        configurationBasedDimensions
                            .flatMapLatest {
                                transitionViewModel.dreamOverlayTranslationY(it.translationYPx)
                            }
                            .collect { px ->
                    dreamOverlayViewModel.dreamOverlayTranslationX.collect { px ->
                        ComplicationLayoutParams.iteratePositions(
                                    { position: Int ->
                                        setElementsTranslationYAtPosition(px, position)
                                    },
                            { position: Int -> setElementsTranslationXAtPosition(px, position) },
                            POSITION_TOP or POSITION_BOTTOM
                        )
                    }
                }

                    /* Alpha animations, when moving from DREAMING->LOCKSCREEN state */
                launch {
                        transitionViewModel.dreamOverlayAlpha.collect { alpha ->
                    dreamOverlayViewModel.dreamOverlayAlpha.collect { alpha ->
                        ComplicationLayoutParams.iteratePositions(
                            { position: Int ->
                                setElementsAlphaAtPosition(
@@ -136,15 +122,11 @@ constructor(
                }

                launch {
                        transitionViewModel.transitionEnded.collect { _ ->
                    dreamOverlayViewModel.transitionEnded.collect { _ ->
                        mOverlayStateController.setExitAnimationsRunning(false)
                    }
                }
            }
            } finally {
                // Ensure the callback is removed when cancellation happens
                configController.removeCallback(configCallback)
            }
        }
    }

@@ -373,14 +355,10 @@ constructor(
        }
    }

    private fun loadFromResources(view: View): ConfigurationBasedDimensions {
        return ConfigurationBasedDimensions(
            translationYPx =
                view.resources.getDimensionPixelSize(R.dimen.dream_overlay_exit_y_offset),
        )
    /** Sets x translation of complications at the specified position. */
    private fun setElementsTranslationXAtPosition(translationX: Float, position: Int) {
        mComplicationHostViewController.getViewsAtPosition(position).forEach { v ->
            v.translationX = translationX
        }
    }

    private data class ConfigurationBasedDimensions(
        val translationYPx: Int,
    )
}
+61 −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.dreams.ui.viewmodel

import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.ui.viewmodel.DreamingToGlanceableHubTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel
import com.android.systemui.res.R
import javax.inject.Inject
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.merge

@OptIn(ExperimentalCoroutinesApi::class)
@SysUISingleton
class DreamOverlayViewModel
@Inject
constructor(
    configurationInteractor: ConfigurationInteractor,
    private val toGlanceableHubTransitionViewModel: DreamingToGlanceableHubTransitionViewModel,
    private val toLockscreenTransitionViewModel: DreamingToLockscreenTransitionViewModel,
) {

    val dreamOverlayTranslationX: Flow<Float> =
        configurationInteractor
            .dimensionPixelSize(R.dimen.dream_overlay_exit_x_offset)
            .flatMapLatest { px: Int ->
                toGlanceableHubTransitionViewModel.dreamOverlayTranslationX(px)
            }

    val dreamOverlayTranslationY: Flow<Float> =
        configurationInteractor
            .dimensionPixelSize(R.dimen.dream_overlay_exit_y_offset)
            .flatMapLatest { px: Int ->
                toLockscreenTransitionViewModel.dreamOverlayTranslationY(px)
            }

    val dreamOverlayAlpha: Flow<Float> =
        merge(
            toLockscreenTransitionViewModel.dreamOverlayAlpha,
            toGlanceableHubTransitionViewModel.dreamOverlayAlpha,
        )

    val transitionEnded = toLockscreenTransitionViewModel.transitionEnded
}
Loading