Multi-shade foundation - UI layer viewmodel (3/5).
Early foundation for the multi-shade framework. View models in the UI layer for the multi-shade foundation. Includes two view-models: 1. A top level MultiShadeViewModel to coordinate between the individual shades and the scrim 2. A ShadeViewModel to describe UI state for a single shade Bug: 272130181 Test: includes integration tests for code in this layer together with the domain and data layers. Tested end-to-end in the last CL in this chain. Change-Id: I85ff1c24a780ffae66a7e352cf75acdbdbc749ba
This commit is contained in:
committed by
Ale Nijamkin
parent
664a12de42
commit
7aa59a35ac
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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.multishade.ui.viewmodel
|
||||
|
||||
import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor
|
||||
import com.android.systemui.multishade.shared.model.ProxiedInputModel
|
||||
import com.android.systemui.multishade.shared.model.ShadeConfig
|
||||
import com.android.systemui.multishade.shared.model.ShadeId
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
/** Models UI state for UI that supports multi (or single) shade. */
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class MultiShadeViewModel(
|
||||
viewModelScope: CoroutineScope,
|
||||
private val interactor: MultiShadeInteractor,
|
||||
) {
|
||||
/** Models UI state for the single shade. */
|
||||
val singleShade =
|
||||
ShadeViewModel(
|
||||
viewModelScope,
|
||||
ShadeId.SINGLE,
|
||||
interactor,
|
||||
)
|
||||
|
||||
/** Models UI state for the shade on the left-hand side. */
|
||||
val leftShade =
|
||||
ShadeViewModel(
|
||||
viewModelScope,
|
||||
ShadeId.LEFT,
|
||||
interactor,
|
||||
)
|
||||
|
||||
/** Models UI state for the shade on the right-hand side. */
|
||||
val rightShade =
|
||||
ShadeViewModel(
|
||||
viewModelScope,
|
||||
ShadeId.RIGHT,
|
||||
interactor,
|
||||
)
|
||||
|
||||
/** The amount of alpha that the scrim should have. This is a value between `0` and `1`. */
|
||||
val scrimAlpha: StateFlow<Float> =
|
||||
combine(
|
||||
interactor.maxShadeExpansion,
|
||||
interactor.shadeConfig
|
||||
.map { it as? ShadeConfig.DualShadeConfig }
|
||||
.map { dualShadeConfigOrNull -> dualShadeConfigOrNull?.scrimAlpha ?: 0f },
|
||||
::Pair,
|
||||
)
|
||||
.map { (anyShadeExpansion, scrimAlpha) ->
|
||||
(anyShadeExpansion * scrimAlpha).coerceIn(0f, 1f)
|
||||
}
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(),
|
||||
initialValue = 0f,
|
||||
)
|
||||
|
||||
/** Whether the scrim should accept touch events. */
|
||||
val isScrimEnabled: StateFlow<Boolean> =
|
||||
interactor.shadeConfig
|
||||
.flatMapLatest { shadeConfig ->
|
||||
when (shadeConfig) {
|
||||
// In the dual shade configuration, the scrim is enabled when the expansion is
|
||||
// greater than zero on any one of the shades.
|
||||
is ShadeConfig.DualShadeConfig ->
|
||||
interactor.maxShadeExpansion
|
||||
.map { expansion -> expansion > 0 }
|
||||
.distinctUntilChanged()
|
||||
// No scrim in the single shade configuration.
|
||||
is ShadeConfig.SingleShadeConfig -> flowOf(false)
|
||||
}
|
||||
}
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(),
|
||||
initialValue = false,
|
||||
)
|
||||
|
||||
/** Notifies that the scrim has been touched. */
|
||||
fun onScrimTouched(proxiedInput: ProxiedInputModel) {
|
||||
if (!isScrimEnabled.value) {
|
||||
return
|
||||
}
|
||||
|
||||
interactor.sendProxiedInput(proxiedInput)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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.multishade.ui.viewmodel
|
||||
|
||||
import androidx.annotation.FloatRange
|
||||
import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor
|
||||
import com.android.systemui.multishade.shared.model.ProxiedInputModel
|
||||
import com.android.systemui.multishade.shared.model.ShadeConfig
|
||||
import com.android.systemui.multishade.shared.model.ShadeId
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
/** Models UI state for a single shade. */
|
||||
class ShadeViewModel(
|
||||
viewModelScope: CoroutineScope,
|
||||
private val shadeId: ShadeId,
|
||||
private val interactor: MultiShadeInteractor,
|
||||
) {
|
||||
/** Whether the shade is visible. */
|
||||
val isVisible: StateFlow<Boolean> =
|
||||
interactor
|
||||
.isVisible(shadeId)
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(),
|
||||
initialValue = false,
|
||||
)
|
||||
|
||||
/** Whether swiping on the shade UI is currently enabled. */
|
||||
val isSwipingEnabled: StateFlow<Boolean> =
|
||||
interactor
|
||||
.isNonProxiedInputAllowed(shadeId)
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(),
|
||||
initialValue = false,
|
||||
)
|
||||
|
||||
/** Whether the shade must be collapsed immediately. */
|
||||
val isForceCollapsed: Flow<Boolean> =
|
||||
interactor.isForceCollapsed(shadeId).distinctUntilChanged()
|
||||
|
||||
/** The width of the shade. */
|
||||
val width: StateFlow<Size> =
|
||||
interactor.shadeConfig
|
||||
.map { shadeWidth(it) }
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(),
|
||||
initialValue = shadeWidth(interactor.shadeConfig.value),
|
||||
)
|
||||
|
||||
/**
|
||||
* The amount that the user must swipe up when the shade is fully expanded to automatically
|
||||
* collapse once the user lets go of the shade. If the user swipes less than this amount, the
|
||||
* shade will automatically revert back to fully expanded once the user stops swiping.
|
||||
*/
|
||||
val swipeCollapseThreshold: StateFlow<Float> =
|
||||
interactor.shadeConfig
|
||||
.map { it.swipeCollapseThreshold }
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(),
|
||||
initialValue = interactor.shadeConfig.value.swipeCollapseThreshold,
|
||||
)
|
||||
|
||||
/**
|
||||
* The amount that the user must swipe down when the shade is fully collapsed to automatically
|
||||
* expand once the user lets go of the shade. If the user swipes less than this amount, the
|
||||
* shade will automatically revert back to fully collapsed once the user stops swiping.
|
||||
*/
|
||||
val swipeExpandThreshold: StateFlow<Float> =
|
||||
interactor.shadeConfig
|
||||
.map { it.swipeExpandThreshold }
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(),
|
||||
initialValue = interactor.shadeConfig.value.swipeExpandThreshold,
|
||||
)
|
||||
|
||||
/**
|
||||
* Proxied input affecting the shade. This is input coming from sources outside of system UI
|
||||
* (for example, swiping down on the Launcher or from the status bar) or outside the UI of any
|
||||
* shade (for example, the scrim that's shown behind the shades).
|
||||
*/
|
||||
val proxiedInput: Flow<ProxiedInputModel?> =
|
||||
interactor
|
||||
.proxiedInput(shadeId)
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(),
|
||||
initialValue = null,
|
||||
)
|
||||
|
||||
/** Notifies that the expansion amount for the shade has changed. */
|
||||
fun onExpansionChanged(
|
||||
expansion: Float,
|
||||
) {
|
||||
interactor.setExpansion(shadeId, expansion.coerceIn(0f, 1f))
|
||||
}
|
||||
|
||||
/** Notifies that a drag gesture has started. */
|
||||
fun onDragStarted() {
|
||||
interactor.onUserInteractionStarted(shadeId)
|
||||
}
|
||||
|
||||
/** Notifies that a drag gesture has ended. */
|
||||
fun onDragEnded() {
|
||||
interactor.onUserInteractionEnded(shadeId = shadeId)
|
||||
}
|
||||
|
||||
private fun shadeWidth(shadeConfig: ShadeConfig): Size {
|
||||
return when (shadeId) {
|
||||
ShadeId.LEFT ->
|
||||
Size.Pixels((shadeConfig as? ShadeConfig.DualShadeConfig)?.leftShadeWidthPx ?: 0)
|
||||
ShadeId.RIGHT ->
|
||||
Size.Pixels((shadeConfig as? ShadeConfig.DualShadeConfig)?.rightShadeWidthPx ?: 0)
|
||||
ShadeId.SINGLE -> Size.Fraction(1f)
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Size {
|
||||
data class Fraction(
|
||||
@FloatRange(from = 0.0, to = 1.0) val fraction: Float,
|
||||
) : Size()
|
||||
data class Pixels(
|
||||
val pixels: Int,
|
||||
) : Size()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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.multishade.ui.viewmodel
|
||||
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.coroutines.collectLastValue
|
||||
import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy
|
||||
import com.android.systemui.multishade.domain.interactor.MultiShadeInteractorTest
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.JUnit4
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@SmallTest
|
||||
@RunWith(JUnit4::class)
|
||||
class MultiShadeViewModelTest : SysuiTestCase() {
|
||||
|
||||
private lateinit var testScope: TestScope
|
||||
private lateinit var inputProxy: MultiShadeInputProxy
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
testScope = TestScope()
|
||||
inputProxy = MultiShadeInputProxy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun scrim_whenDualShadeCollapsed() =
|
||||
testScope.runTest {
|
||||
val alpha = 0.5f
|
||||
overrideResource(R.dimen.dual_shade_scrim_alpha, alpha)
|
||||
overrideResource(R.bool.dual_shade_enabled, true)
|
||||
|
||||
val underTest = create()
|
||||
val scrimAlpha: Float? by collectLastValue(underTest.scrimAlpha)
|
||||
val isScrimEnabled: Boolean? by collectLastValue(underTest.isScrimEnabled)
|
||||
|
||||
assertThat(scrimAlpha).isZero()
|
||||
assertThat(isScrimEnabled).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun scrim_whenDualShadeExpanded() =
|
||||
testScope.runTest {
|
||||
val alpha = 0.5f
|
||||
overrideResource(R.dimen.dual_shade_scrim_alpha, alpha)
|
||||
overrideResource(R.bool.dual_shade_enabled, true)
|
||||
val underTest = create()
|
||||
val scrimAlpha: Float? by collectLastValue(underTest.scrimAlpha)
|
||||
val isScrimEnabled: Boolean? by collectLastValue(underTest.isScrimEnabled)
|
||||
assertThat(scrimAlpha).isZero()
|
||||
assertThat(isScrimEnabled).isFalse()
|
||||
|
||||
underTest.leftShade.onExpansionChanged(0.5f)
|
||||
assertThat(scrimAlpha).isEqualTo(alpha * 0.5f)
|
||||
assertThat(isScrimEnabled).isTrue()
|
||||
|
||||
underTest.rightShade.onExpansionChanged(1f)
|
||||
assertThat(scrimAlpha).isEqualTo(alpha * 1f)
|
||||
assertThat(isScrimEnabled).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun scrim_whenSingleShadeCollapsed() =
|
||||
testScope.runTest {
|
||||
val alpha = 0.5f
|
||||
overrideResource(R.dimen.dual_shade_scrim_alpha, alpha)
|
||||
overrideResource(R.bool.dual_shade_enabled, false)
|
||||
|
||||
val underTest = create()
|
||||
val scrimAlpha: Float? by collectLastValue(underTest.scrimAlpha)
|
||||
val isScrimEnabled: Boolean? by collectLastValue(underTest.isScrimEnabled)
|
||||
|
||||
assertThat(scrimAlpha).isZero()
|
||||
assertThat(isScrimEnabled).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun scrim_whenSingleShadeExpanded() =
|
||||
testScope.runTest {
|
||||
val alpha = 0.5f
|
||||
overrideResource(R.dimen.dual_shade_scrim_alpha, alpha)
|
||||
overrideResource(R.bool.dual_shade_enabled, false)
|
||||
val underTest = create()
|
||||
val scrimAlpha: Float? by collectLastValue(underTest.scrimAlpha)
|
||||
val isScrimEnabled: Boolean? by collectLastValue(underTest.isScrimEnabled)
|
||||
|
||||
underTest.singleShade.onExpansionChanged(0.95f)
|
||||
|
||||
assertThat(scrimAlpha).isZero()
|
||||
assertThat(isScrimEnabled).isFalse()
|
||||
}
|
||||
|
||||
private fun create(): MultiShadeViewModel {
|
||||
return MultiShadeViewModel(
|
||||
viewModelScope = testScope.backgroundScope,
|
||||
interactor =
|
||||
MultiShadeInteractorTest.create(
|
||||
testScope = testScope,
|
||||
context = context,
|
||||
inputProxy = inputProxy,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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.multishade.ui.viewmodel
|
||||
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.coroutines.collectLastValue
|
||||
import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy
|
||||
import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor
|
||||
import com.android.systemui.multishade.domain.interactor.MultiShadeInteractorTest
|
||||
import com.android.systemui.multishade.shared.model.ProxiedInputModel
|
||||
import com.android.systemui.multishade.shared.model.ShadeId
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.google.common.truth.Truth.assertWithMessage
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.JUnit4
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@SmallTest
|
||||
@RunWith(JUnit4::class)
|
||||
class ShadeViewModelTest : SysuiTestCase() {
|
||||
|
||||
private lateinit var testScope: TestScope
|
||||
private lateinit var inputProxy: MultiShadeInputProxy
|
||||
private var interactor: MultiShadeInteractor? = null
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
testScope = TestScope()
|
||||
inputProxy = MultiShadeInputProxy()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isVisible_dualShadeConfig() =
|
||||
testScope.runTest {
|
||||
overrideResource(R.bool.dual_shade_enabled, true)
|
||||
val isLeftShadeVisible: Boolean? by collectLastValue(create(ShadeId.LEFT).isVisible)
|
||||
val isRightShadeVisible: Boolean? by collectLastValue(create(ShadeId.RIGHT).isVisible)
|
||||
val isSingleShadeVisible: Boolean? by collectLastValue(create(ShadeId.SINGLE).isVisible)
|
||||
|
||||
assertThat(isLeftShadeVisible).isTrue()
|
||||
assertThat(isRightShadeVisible).isTrue()
|
||||
assertThat(isSingleShadeVisible).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isVisible_singleShadeConfig() =
|
||||
testScope.runTest {
|
||||
overrideResource(R.bool.dual_shade_enabled, false)
|
||||
val isLeftShadeVisible: Boolean? by collectLastValue(create(ShadeId.LEFT).isVisible)
|
||||
val isRightShadeVisible: Boolean? by collectLastValue(create(ShadeId.RIGHT).isVisible)
|
||||
val isSingleShadeVisible: Boolean? by collectLastValue(create(ShadeId.SINGLE).isVisible)
|
||||
|
||||
assertThat(isLeftShadeVisible).isFalse()
|
||||
assertThat(isRightShadeVisible).isFalse()
|
||||
assertThat(isSingleShadeVisible).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isSwipingEnabled() =
|
||||
testScope.runTest {
|
||||
val underTest = create(ShadeId.LEFT)
|
||||
val isSwipingEnabled: Boolean? by collectLastValue(underTest.isSwipingEnabled)
|
||||
assertWithMessage("isSwipingEnabled should start as true!")
|
||||
.that(isSwipingEnabled)
|
||||
.isTrue()
|
||||
|
||||
// Need to collect proxied input so the flows become hot as the gesture cancelation code
|
||||
// logic sits in side the proxiedInput flow for each shade.
|
||||
collectLastValue(underTest.proxiedInput)
|
||||
collectLastValue(create(ShadeId.RIGHT).proxiedInput)
|
||||
|
||||
// Starting a proxied interaction on the LEFT shade disallows non-proxied interaction on
|
||||
// the
|
||||
// same shade.
|
||||
inputProxy.onProxiedInput(
|
||||
ProxiedInputModel.OnDrag(xFraction = 0f, yDragAmountPx = 123f)
|
||||
)
|
||||
assertThat(isSwipingEnabled).isFalse()
|
||||
|
||||
// Registering the end of the proxied interaction re-allows it.
|
||||
inputProxy.onProxiedInput(ProxiedInputModel.OnDragEnd)
|
||||
assertThat(isSwipingEnabled).isTrue()
|
||||
|
||||
// Starting a proxied interaction on the RIGHT shade force-collapses the LEFT shade,
|
||||
// disallowing non-proxied input on the LEFT shade.
|
||||
inputProxy.onProxiedInput(
|
||||
ProxiedInputModel.OnDrag(xFraction = 1f, yDragAmountPx = 123f)
|
||||
)
|
||||
assertThat(isSwipingEnabled).isFalse()
|
||||
|
||||
// Registering the end of the interaction on the RIGHT shade re-allows it.
|
||||
inputProxy.onProxiedInput(ProxiedInputModel.OnDragEnd)
|
||||
assertThat(isSwipingEnabled).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isForceCollapsed_whenOtherShadeInteractionUnderway() =
|
||||
testScope.runTest {
|
||||
val leftShade = create(ShadeId.LEFT)
|
||||
val rightShade = create(ShadeId.RIGHT)
|
||||
val isLeftShadeForceCollapsed: Boolean? by collectLastValue(leftShade.isForceCollapsed)
|
||||
val isRightShadeForceCollapsed: Boolean? by
|
||||
collectLastValue(rightShade.isForceCollapsed)
|
||||
val isSingleShadeForceCollapsed: Boolean? by
|
||||
collectLastValue(create(ShadeId.SINGLE).isForceCollapsed)
|
||||
|
||||
assertWithMessage("isForceCollapsed should start as false!")
|
||||
.that(isLeftShadeForceCollapsed)
|
||||
.isFalse()
|
||||
assertWithMessage("isForceCollapsed should start as false!")
|
||||
.that(isRightShadeForceCollapsed)
|
||||
.isFalse()
|
||||
assertWithMessage("isForceCollapsed should start as false!")
|
||||
.that(isSingleShadeForceCollapsed)
|
||||
.isFalse()
|
||||
|
||||
// Registering the start of an interaction on the RIGHT shade force-collapses the LEFT
|
||||
// shade.
|
||||
rightShade.onDragStarted()
|
||||
assertThat(isLeftShadeForceCollapsed).isTrue()
|
||||
assertThat(isRightShadeForceCollapsed).isFalse()
|
||||
assertThat(isSingleShadeForceCollapsed).isFalse()
|
||||
|
||||
// Registering the end of the interaction on the RIGHT shade re-allows it.
|
||||
rightShade.onDragEnded()
|
||||
assertThat(isLeftShadeForceCollapsed).isFalse()
|
||||
assertThat(isRightShadeForceCollapsed).isFalse()
|
||||
assertThat(isSingleShadeForceCollapsed).isFalse()
|
||||
|
||||
// Registering the start of an interaction on the LEFT shade force-collapses the RIGHT
|
||||
// shade.
|
||||
leftShade.onDragStarted()
|
||||
assertThat(isLeftShadeForceCollapsed).isFalse()
|
||||
assertThat(isRightShadeForceCollapsed).isTrue()
|
||||
assertThat(isSingleShadeForceCollapsed).isFalse()
|
||||
|
||||
// Registering the end of the interaction on the LEFT shade re-allows it.
|
||||
leftShade.onDragEnded()
|
||||
assertThat(isLeftShadeForceCollapsed).isFalse()
|
||||
assertThat(isRightShadeForceCollapsed).isFalse()
|
||||
assertThat(isSingleShadeForceCollapsed).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onTapOutside_collapsesAll() =
|
||||
testScope.runTest {
|
||||
val isLeftShadeForceCollapsed: Boolean? by
|
||||
collectLastValue(create(ShadeId.LEFT).isForceCollapsed)
|
||||
val isRightShadeForceCollapsed: Boolean? by
|
||||
collectLastValue(create(ShadeId.RIGHT).isForceCollapsed)
|
||||
val isSingleShadeForceCollapsed: Boolean? by
|
||||
collectLastValue(create(ShadeId.SINGLE).isForceCollapsed)
|
||||
|
||||
assertWithMessage("isForceCollapsed should start as false!")
|
||||
.that(isLeftShadeForceCollapsed)
|
||||
.isFalse()
|
||||
assertWithMessage("isForceCollapsed should start as false!")
|
||||
.that(isRightShadeForceCollapsed)
|
||||
.isFalse()
|
||||
assertWithMessage("isForceCollapsed should start as false!")
|
||||
.that(isSingleShadeForceCollapsed)
|
||||
.isFalse()
|
||||
|
||||
inputProxy.onProxiedInput(ProxiedInputModel.OnTap)
|
||||
assertThat(isLeftShadeForceCollapsed).isTrue()
|
||||
assertThat(isRightShadeForceCollapsed).isTrue()
|
||||
assertThat(isSingleShadeForceCollapsed).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun proxiedInput_ignoredWhileNonProxiedGestureUnderway() =
|
||||
testScope.runTest {
|
||||
val underTest = create(ShadeId.RIGHT)
|
||||
val proxiedInput: ProxiedInputModel? by collectLastValue(underTest.proxiedInput)
|
||||
underTest.onDragStarted()
|
||||
|
||||
inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0.9f, 100f))
|
||||
assertThat(proxiedInput).isNull()
|
||||
|
||||
inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0.8f, 110f))
|
||||
assertThat(proxiedInput).isNull()
|
||||
|
||||
underTest.onDragEnded()
|
||||
|
||||
inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0.9f, 100f))
|
||||
assertThat(proxiedInput).isNotNull()
|
||||
}
|
||||
|
||||
private fun create(
|
||||
shadeId: ShadeId,
|
||||
): ShadeViewModel {
|
||||
return ShadeViewModel(
|
||||
viewModelScope = testScope.backgroundScope,
|
||||
shadeId = shadeId,
|
||||
interactor = interactor
|
||||
?: MultiShadeInteractorTest.create(
|
||||
testScope = testScope,
|
||||
context = context,
|
||||
inputProxy = inputProxy,
|
||||
)
|
||||
.also { interactor = it },
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user