Skip to content

Commit

Permalink
Fix wrong animation confirm setup (#933)
Browse files Browse the repository at this point in the history
**Background**

On setup screen of infrared remotes bottom sheet animation has bad
behaviour on state change

**Changes**

- Use transitions to implement target state and current state for bottom
sheet animation

**Test plan**

- Open setup screen
- Press button, waith for bottom sheet opened with beautiful animation
- Click yes or no and see another beautiful close animation
  • Loading branch information
makeevrserg authored Sep 3, 2024
1 parent df9ad41 commit 72dac16
Show file tree
Hide file tree
Showing 8 changed files with 108 additions and 47 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Attention: don't forget to add the flag for F-Droid before release
- [FIX] Fap manifest caching
- [FIX] Remote controls design issues
- [FIX] Fix flaky test
- [FIX] Bad bottom sheet animation on infrared setup screen
- [CI] Fix merge-queue files diff
- [CI] Add https://github.com/LionZXY/detekt-decompose-rule
- [CI] Enabling detekt module for android and kmp modules
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,17 @@ import kotlinx.coroutines.flow.StateFlow

interface DispatchSignalApi : InstanceKeeper.Instance {
val state: StateFlow<State>
val isEmulated: StateFlow<Boolean>

fun dismissBusyDialog()

/**
* Dispatch key from temporal file which contains only one key
*/
fun dispatch(config: EmulateConfig, identifier: IfrKeyIdentifier)
fun dispatch(
config: EmulateConfig,
identifier: IfrKeyIdentifier,
onDispatched: () -> Unit = {}
)

fun reset()

Expand All @@ -26,7 +29,8 @@ interface DispatchSignalApi : InstanceKeeper.Instance {
fun dispatch(
identifier: IfrKeyIdentifier,
remotes: List<InfraredRemote>,
ffPath: FlipperFilePath
ffPath: FlipperFilePath,
onDispatched: () -> Unit = {}
)

sealed interface State {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Scaffold
import androidx.compose.runtime.Composable
Expand All @@ -18,6 +19,7 @@ import com.flipperdevices.core.ui.dialog.composable.busy.ComposableFlipperBusy
import com.flipperdevices.core.ui.theme.LocalPalletV2
import com.flipperdevices.ifrmvp.core.ui.layout.shared.ErrorComposable
import com.flipperdevices.ifrmvp.core.ui.layout.shared.SharedTopBar
import com.flipperdevices.remotecontrols.impl.setup.composable.components.AnimatedConfirmContent
import com.flipperdevices.remotecontrols.impl.setup.composable.components.LoadedContent
import com.flipperdevices.remotecontrols.impl.setup.composable.components.SetupLoadingContent
import com.flipperdevices.remotecontrols.impl.setup.presentation.decompose.SetupComponent
Expand Down Expand Up @@ -45,6 +47,7 @@ fun SetupScreen(
val model by remember(setupComponent, coroutineScope) {
setupComponent.model(coroutineScope)
}.collectAsState()
val lastEmulatedSignal by setupComponent.lastEmulatedSignal.collectAsState()
LaunchedEffect(setupComponent.remoteFoundFlow) {
setupComponent.remoteFoundFlow.onEach {
setupComponent.onFileFound(it)
Expand Down Expand Up @@ -90,8 +93,6 @@ fun SetupScreen(
LoadedContent(
model = model,
modifier = Modifier.padding(scaffoldPaddings),
onPositiveClick = setupComponent::onSuccessClick,
onNegativeClick = setupComponent::onFailedClick,
onDispatchSignalClick = setupComponent::dispatchSignal
)
}
Expand All @@ -101,5 +102,13 @@ fun SetupScreen(
}
}
}
AnimatedConfirmContent(
lastEmulatedSignal = lastEmulatedSignal,
modifier = Modifier
.fillMaxSize()
.padding(scaffoldPaddings),
onNegativeClick = setupComponent::onFailedClick,
onSuccessClick = setupComponent::onSuccessClick,
)
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
package com.flipperdevices.remotecontrols.impl.setup.composable.components

import android.content.res.Configuration
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.EnterExitState
import androidx.compose.animation.core.updateTransition
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
Expand All @@ -9,6 +16,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
Expand All @@ -27,6 +35,7 @@ import com.flipperdevices.core.ui.ktx.elements.ComposableFlipperButton
import com.flipperdevices.core.ui.theme.FlipperThemeInternal
import com.flipperdevices.core.ui.theme.LocalPalletV2
import com.flipperdevices.core.ui.theme.LocalTypography
import com.flipperdevices.ifrmvp.backend.model.SignalResponse
import com.flipperdevices.remotecontrols.setup.impl.R as SetupR

@Composable
Expand Down Expand Up @@ -89,6 +98,49 @@ fun ConfirmContent(
)
}

@Composable
fun AnimatedConfirmContent(
lastEmulatedSignal: SignalResponse?,
onNegativeClick: () -> Unit,
onSuccessClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Box(
modifier = modifier,
contentAlignment = Alignment.BottomCenter
) {
val transition = updateTransition(
targetState = lastEmulatedSignal,
label = lastEmulatedSignal?.signalModel?.id?.toString()
)
transition.AnimatedVisibility(
visible = { localLastEmulatedSignal -> localLastEmulatedSignal != null },
enter = slideInVertically(initialOffsetY = { it / 2 }) + fadeIn(),
exit = slideOutVertically(targetOffsetY = { it / 2 }) + fadeOut(),
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomCenter)
.navigationBarsPadding(),
) {
val contentState = when (this.transition.targetState) {
EnterExitState.Visible -> transition.targetState
else -> transition.currentState
}
ConfirmContent(
text = when (contentState) {
null -> ""
else ->
contentState.message
.format(contentState.categoryName)
},
onNegativeClick = onNegativeClick,
onPositiveClick = onSuccessClick,
modifier = Modifier.align(Alignment.BottomCenter)
)
}
}
}

@Preview(
showSystemUi = true,
showBackground = true,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
package com.flipperdevices.remotecontrols.impl.setup.composable.components

import android.content.res.Configuration
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
Expand All @@ -23,8 +18,6 @@ import com.flipperdevices.remotecontrols.setup.impl.R as SetupR
@Composable
fun LoadedContent(
model: SetupComponent.Model.Loaded,
onPositiveClick: () -> Unit,
onNegativeClick: () -> Unit,
onDispatchSignalClick: () -> Unit,
modifier: Modifier = Modifier
) {
Expand All @@ -44,22 +37,6 @@ fun LoadedContent(
isSyncing = model.isSyncing,
isConnected = model.isConnected
)
AnimatedVisibility(
visible = model.isEmulated,
enter = slideInVertically(initialOffsetY = { it / 2 }),
exit = slideOutVertically(targetOffsetY = { it / 2 }),
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomCenter)
.navigationBarsPadding(),
) {
ConfirmContent(
text = signalResponse.message.format(signalResponse.categoryName),
onNegativeClick = onNegativeClick,
onPositiveClick = onPositiveClick,
modifier = Modifier.align(Alignment.BottomCenter)
)
}
}

else -> {
Expand All @@ -82,12 +59,9 @@ private fun LoadedContentPreview() {
LoadedContent(
model = SetupComponent.Model.Loaded(
response = SignalResponseModel(),
isEmulated = true,
emulatedKeyIdentifier = null,
connectionState = InfraredConnectionApi.InfraredEmulateState.ALL_GOOD
),
onPositiveClick = {},
onNegativeClick = {},
onDispatchSignalClick = {}
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.flipperdevices.remotecontrols.impl.setup.presentation.decompose

import com.arkivanov.decompose.ComponentContext
import com.flipperdevices.ifrmvp.backend.model.IfrFileModel
import com.flipperdevices.ifrmvp.backend.model.SignalResponse
import com.flipperdevices.ifrmvp.backend.model.SignalResponseModel
import com.flipperdevices.ifrmvp.model.IfrKeyIdentifier
import com.flipperdevices.infrared.api.InfraredConnectionApi.InfraredEmulateState
Expand All @@ -15,6 +16,7 @@ interface SetupComponent {
fun model(coroutineScope: CoroutineScope): StateFlow<Model>

val remoteFoundFlow: Flow<IfrFileModel>
val lastEmulatedSignal: StateFlow<SignalResponse?>
val param: SetupScreenDecomposeComponent.Param

fun onBackClick()
Expand All @@ -35,7 +37,6 @@ interface SetupComponent {
val response: SignalResponseModel,
val isFlipperBusy: Boolean = false,
val emulatedKeyIdentifier: IfrKeyIdentifier?,
val isEmulated: Boolean,
val connectionState: InfraredEmulateState
) : Model {
val isSyncing: Boolean = listOf(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import com.flipperdevices.bridge.dao.api.model.FlipperFilePath
import com.flipperdevices.bridge.dao.api.model.FlipperKeyType
import com.flipperdevices.core.di.AppGraph
import com.flipperdevices.core.ktx.jre.FlipperDispatchers
import com.flipperdevices.core.log.LogTagProvider
import com.flipperdevices.core.log.error
import com.flipperdevices.ifrmvp.backend.model.IfrFileModel
import com.flipperdevices.ifrmvp.backend.model.SignalResponse
import com.flipperdevices.ifrmvp.model.IfrKeyIdentifier
import com.flipperdevices.ifrmvp.model.buttondata.SingleKeyButtonData
import com.flipperdevices.keyemulate.model.EmulateConfig
Expand All @@ -23,7 +26,10 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.flowOn
Expand All @@ -44,7 +50,8 @@ class SetupComponentImpl @AssistedInject constructor(
createSaveTempSignalApi: Provider<SaveTempSignalApi>,
createDispatchSignalApi: Provider<DispatchSignalApi>,
createConnectionViewModel: Provider<ConnectionViewModel>
) : SetupComponent, ComponentContext by componentContext {
) : SetupComponent, ComponentContext by componentContext, LogTagProvider {
override val TAG: String = "SetupComponent"
private val saveSignalApi = instanceKeeper.getOrCreate(
key = "SetupComponent_saveSignalApi_${param.brandId}_${param.categoryId}",
factory = {
Expand Down Expand Up @@ -84,13 +91,15 @@ class SetupComponentImpl @AssistedInject constructor(
}
}
)
private val _lastEmulatedSignal = MutableStateFlow<SignalResponse?>(null)
override val lastEmulatedSignal: StateFlow<SignalResponse?> = _lastEmulatedSignal.asStateFlow()

private val modelFlow = combine(
createCurrentSignalViewModel.state,
saveSignalApi.state,
dispatchSignalApi.state,
dispatchSignalApi.isEmulated,
connectionViewModel.state,
transform = { signalState, saveState, dispatchState, isEmulated, connectionState ->
transform = { signalState, saveState, dispatchState, connectionState ->
val emulatingState = (dispatchState as? DispatchSignalApi.State.Emulating)
when (signalState) {
CurrentSignalViewModel.State.Error -> SetupComponent.Model.Error
Expand All @@ -103,7 +112,6 @@ class SetupComponentImpl @AssistedInject constructor(
response = signalState.response,
isFlipperBusy = dispatchState is DispatchSignalApi.State.FlipperIsBusy,
emulatedKeyIdentifier = emulatingState?.ifrKeyIdentifier,
isEmulated = isEmulated,
connectionState = connectionState
)
}
Expand Down Expand Up @@ -132,6 +140,7 @@ class SetupComponentImpl @AssistedInject constructor(
successResults = historyViewModel.state.value.successfulSignals,
failedResults = historyViewModel.state.value.failedSignals
)
_lastEmulatedSignal.value = null
}

override fun onFileFound(ifrFileModel: IfrFileModel) {
Expand All @@ -156,8 +165,14 @@ class SetupComponentImpl @AssistedInject constructor(

override fun dispatchSignal() {
val state = createCurrentSignalViewModel.state.value
val loadedState = state as? CurrentSignalViewModel.State.Loaded ?: return
val signalModel = loadedState.response.signalResponse?.signalModel ?: return
val loadedState = state as? CurrentSignalViewModel.State.Loaded ?: run {
error { "#dispatchSignal dispatch called from unloaded state" }
return
}
val signalModel = loadedState.response.signalResponse?.signalModel ?: run {
error { "#dispatchSignal signalModel is null" }
return
}
val config = EmulateConfig(
keyPath = FlipperFilePath(
ABSOLUTE_TEMP_FOLDER_PATH,
Expand All @@ -170,7 +185,11 @@ class SetupComponentImpl @AssistedInject constructor(
val keyIdentifier = (loadedState.response.signalResponse?.data as? SingleKeyButtonData)
?.keyIdentifier
?: IfrKeyIdentifier.Unknown
dispatchSignalApi.dispatch(config, keyIdentifier)
dispatchSignalApi.dispatch(
config = config,
identifier = keyIdentifier,
onDispatched = { _lastEmulatedSignal.value = loadedState.response.signalResponse }
)
}

override fun onBackClick() = onBackClick.invoke()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,23 +42,20 @@ class DispatchSignalViewModel @Inject constructor(
private val _state = MutableStateFlow<DispatchSignalApi.State>(DispatchSignalApi.State.Pending)
override val state = _state.asStateFlow()

private val _isEmulated = MutableStateFlow(false)
override val isEmulated = _isEmulated.asStateFlow()

private var latestDispatchJob: Job? = null

override fun reset() {
viewModelScope.launch {
latestDispatchJob?.cancelAndJoin()
_state.value = DispatchSignalApi.State.Pending
_isEmulated.value = false
}
}

override fun dispatch(
identifier: IfrKeyIdentifier,
remotes: List<InfraredRemote>,
ffPath: FlipperFilePath
ffPath: FlipperFilePath,
onDispatched: () -> Unit
) {
val i = remotes.indexOfFirst { remote ->
when (identifier) {
Expand Down Expand Up @@ -93,14 +90,18 @@ class DispatchSignalViewModel @Inject constructor(
args = remote.name,
index = i
)
dispatch(config, identifier)
dispatch(config, identifier, onDispatched)
}

override fun dismissBusyDialog() {
_state.value = DispatchSignalApi.State.Pending
}

override fun dispatch(config: EmulateConfig, identifier: IfrKeyIdentifier) {
override fun dispatch(
config: EmulateConfig,
identifier: IfrKeyIdentifier,
onDispatched: () -> Unit
) {
if (latestDispatchJob?.isActive == true) return
latestDispatchJob = viewModelScope.launch(Dispatchers.Main) {
_state.emit(DispatchSignalApi.State.Pending)
Expand All @@ -119,7 +120,7 @@ class DispatchSignalViewModel @Inject constructor(
delay(DEFAULT_SIGNAL_DELAY)
emulateHelper.stopEmulate(this, serviceApi.requestApi)
_state.emit(DispatchSignalApi.State.Pending)
_isEmulated.emit(true)
onDispatched.invoke()
} catch (ignored: AlreadyOpenedAppException) {
_state.emit(DispatchSignalApi.State.FlipperIsBusy)
} catch (e: Exception) {
Expand Down

0 comments on commit 72dac16

Please sign in to comment.