Updated on 2026-08-14
This commit is contained in:
parent
47db65fdbe
commit
9f2b2eac71
21 changed files with 658 additions and 72 deletions
|
|
@ -2,6 +2,7 @@ package com.tangem.core.ui
|
|||
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.ui.components.snackbar.TangemTopSnackbarHostState
|
||||
import com.tangem.core.ui.haptic.VibratorHapticManager
|
||||
import com.tangem.core.ui.message.EventMessageHandler
|
||||
import com.tangem.core.ui.theme.AppThemeModeHolder
|
||||
|
|
@ -15,6 +16,8 @@ interface UiDependencies {
|
|||
|
||||
val globalSnackbarHostState: SnackbarHostState
|
||||
|
||||
val globalTopSnackbarHostState: TangemTopSnackbarHostState
|
||||
|
||||
val eventMessageHandler: EventMessageHandler
|
||||
|
||||
val designFeatureToggles: DesignFeatureToggles
|
||||
|
|
|
|||
|
|
@ -1,16 +1,27 @@
|
|||
package com.tangem.core.ui.components.bottomsheets.internal
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.input.key.*
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.haze.hazeSourceTangem
|
||||
import com.tangem.core.ui.components.snackbar.TangemTopSnackbarHost
|
||||
import com.tangem.core.ui.res.LocalHazeState
|
||||
import com.tangem.core.ui.res.LocalRedesignEnabled
|
||||
import com.tangem.core.ui.res.LocalTopSnackbarHostState
|
||||
import dev.chrisbanes.haze.rememberHazeState
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
|
@ -31,6 +42,9 @@ fun ModalBottomSheetWithBackHandling(
|
|||
contentWindowInsets: @Composable () -> WindowInsets = { BottomSheetDefaults.windowInsets },
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
val topSnackbarHostState = LocalTopSnackbarHostState.current
|
||||
val isRedesignEnabled = LocalRedesignEnabled.current
|
||||
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = onDismissRequest,
|
||||
modifier = modifier,
|
||||
|
|
@ -47,7 +61,27 @@ fun ModalBottomSheetWithBackHandling(
|
|||
shouldDismissOnBackPress = onBack == null,
|
||||
),
|
||||
content = {
|
||||
content()
|
||||
if (isRedesignEnabled) {
|
||||
Box {
|
||||
val hazeState = rememberHazeState()
|
||||
|
||||
Column(Modifier.hazeSourceTangem(hazeState, zIndex = -1f)) {
|
||||
content()
|
||||
}
|
||||
|
||||
CompositionLocalProvider(LocalHazeState provides hazeState) {
|
||||
TangemTopSnackbarHost(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopCenter)
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(top = 24.dp),
|
||||
hostState = topSnackbarHostState,
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
content()
|
||||
}
|
||||
|
||||
BackHandler(enabled = onBack != null && sheetState.targetValue != SheetValue.Hidden) {
|
||||
onBack?.invoke()
|
||||
|
|
|
|||
|
|
@ -167,6 +167,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicModalBottomSheetWit
|
|||
val model = config.content as? T ?: return
|
||||
|
||||
val bsContent: @Composable ColumnScope.() -> Unit = {
|
||||
// FIXME: Use LocalWindowSize.current
|
||||
val maxHeight = LocalConfiguration.current.screenHeightDp * MODAL_SHEET_MAX_HEIGHT
|
||||
val initial = 0
|
||||
val scrollState = rememberScrollState(initial = initial)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import androidx.compose.ui.Modifier
|
|||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Deprecated("Use TangemTopSnackbarHost instead. Will be removed with redesign")
|
||||
@Composable
|
||||
fun CopiedTextSnackbarHost(hostState: SnackbarHostState, modifier: Modifier = Modifier) {
|
||||
SnackbarHost(hostState = hostState, modifier = modifier) {
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import com.tangem.core.ui.res.TangemThemePreview
|
|||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Deprecated("With redesign, this component is no longer used. Please use both TangemSnackbar and TangemTopSnackbar")
|
||||
@Composable
|
||||
fun TangemSnackbar(data: SnackbarData, modifier: Modifier = Modifier, actionOnNewLine: Boolean = false) {
|
||||
Snackbar(
|
||||
|
|
|
|||
|
|
@ -27,6 +27,10 @@ import com.tangem.core.ui.res.TangemThemePreview
|
|||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Deprecated(
|
||||
"With redesign, this component is no longer used. " +
|
||||
"Please use both TangemSnackbarHost and TangemTopSnackbarHost",
|
||||
)
|
||||
@Composable
|
||||
fun TangemSnackbarHost(
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,180 @@
|
|||
package com.tangem.core.ui.components.snackbar
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.SpacerW
|
||||
import com.tangem.core.ui.components.haze.hazeEffectTangem
|
||||
import com.tangem.core.ui.ds.button.SecondaryTangemButton
|
||||
import com.tangem.core.ui.ds.button.TangemButtonShape
|
||||
import com.tangem.core.ui.ds.button.TangemButtonSize
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.message.SnackbarMessage
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
|
||||
/**
|
||||
* Top snackbar with an optional leading icon and an optional action button.
|
||||
* Intended to be used with [TangemTopSnackbarHost].
|
||||
*
|
||||
* @param snackbarMessage message data
|
||||
* @param modifier modifier
|
||||
*/
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun TangemTopSnackbar(snackbarMessage: SnackbarMessage, modifier: Modifier = Modifier) {
|
||||
val actionLabel = snackbarMessage.actionLabel
|
||||
val action = snackbarMessage.action
|
||||
val hasAction = actionLabel != null && action != null
|
||||
|
||||
var isTextOverflowing by remember { mutableStateOf(false) }
|
||||
val shape = if (isTextOverflowing) {
|
||||
RoundedCornerShape(TangemTheme.dimens2.x5)
|
||||
} else {
|
||||
TangemTheme.shapes.roundedCornersXLarge
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.shadow(elevation = TangemTheme.dimens.elevation4, shape = shape, clip = false)
|
||||
.background(color = TangemTheme.colors2.controls.backgroundDefault, shape = shape)
|
||||
.clip(shape)
|
||||
.hazeEffectTangem()
|
||||
.sizeIn(minHeight = TangemTheme.dimens2.x11)
|
||||
.padding(start = TangemTheme.dimens2.x5, end = TangemTheme.dimens2.x1)
|
||||
.padding(vertical = TangemTheme.dimens2.x1),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(top = if (isTextOverflowing) TangemTheme.dimens2.x3 else 0.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
if (snackbarMessage.startIconId != null) {
|
||||
Icon(
|
||||
painter = painterResource(id = snackbarMessage.startIconId),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(TangemTheme.dimens2.x5),
|
||||
tint = TangemTheme.colors2.graphic.neutral.secondary,
|
||||
)
|
||||
|
||||
SpacerW(TangemTheme.dimens2.x2)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = snackbarMessage.message.resolveReference(),
|
||||
modifier = Modifier.weight(1f, fill = false),
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
style = TangemTheme.typography.body2,
|
||||
onTextLayout = { if (hasAction) isTextOverflowing = it.hasVisualOverflow },
|
||||
)
|
||||
|
||||
SpacerW(TangemTheme.dimens2.x4)
|
||||
|
||||
if (hasAction && !isTextOverflowing) {
|
||||
SecondaryTangemButton(
|
||||
text = actionLabel,
|
||||
onClick = action,
|
||||
size = TangemButtonSize.X9,
|
||||
shape = TangemButtonShape.Rounded,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (hasAction && isTextOverflowing) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
top = TangemTheme.dimens2.x4,
|
||||
bottom = TangemTheme.dimens2.x1,
|
||||
),
|
||||
contentAlignment = Alignment.CenterEnd,
|
||||
) {
|
||||
SecondaryTangemButton(
|
||||
text = actionLabel,
|
||||
onClick = action,
|
||||
size = TangemButtonSize.X9,
|
||||
shape = TangemButtonShape.Rounded,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview_TangemTopSnackbar_WithAction() {
|
||||
TangemThemePreviewRedesign {
|
||||
Column(
|
||||
modifier = Modifier.padding(TangemTheme.dimens.spacing16),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
TangemTopSnackbar(
|
||||
snackbarMessage = SnackbarMessage(
|
||||
startIconId = R.drawable.ic_eye_off_outline_24,
|
||||
message = stringReference("Balances hidden"),
|
||||
actionLabel = stringReference("Undo"),
|
||||
action = {},
|
||||
),
|
||||
)
|
||||
TangemTopSnackbar(
|
||||
snackbarMessage = SnackbarMessage(
|
||||
startIconId = R.drawable.ic_eye_off_outline_24,
|
||||
message = stringReference(
|
||||
"Balances hidden long long text that should be truncated with ellipsis at the end",
|
||||
),
|
||||
actionLabel = stringReference("Undo"),
|
||||
action = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview_TangemTopSnackbar_NoAction() {
|
||||
TangemThemePreviewRedesign {
|
||||
Column(
|
||||
modifier = Modifier.padding(TangemTheme.dimens.spacing16),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
TangemTopSnackbar(
|
||||
snackbarMessage = SnackbarMessage(
|
||||
startIconId = R.drawable.ic_check_24,
|
||||
message = stringReference("Text copied to clipboard"),
|
||||
),
|
||||
)
|
||||
TangemTopSnackbar(
|
||||
snackbarMessage = SnackbarMessage(
|
||||
message = stringReference("Operation completed"),
|
||||
),
|
||||
)
|
||||
TangemTopSnackbar(
|
||||
snackbarMessage = SnackbarMessage(
|
||||
message = stringReference(
|
||||
"Operation completed long long text that should be truncated with ellipsis at the end",
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,274 @@
|
|||
@file:Suppress("MagicNumber")
|
||||
package com.tangem.core.ui.components.snackbar
|
||||
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.AnimationSpec
|
||||
import androidx.compose.animation.core.Spring
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.TransformOrigin
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.platform.AccessibilityManager
|
||||
import androidx.compose.ui.platform.LocalAccessibilityManager
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.message.SnackbarMessage
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
/**
|
||||
* Snackbar host that displays a [TangemTopSnackbar] sliding and scaling in from the top of the screen.
|
||||
* Typical placement: at the top of the screen's root [Box], above the main content.
|
||||
*
|
||||
* ```
|
||||
* Box(Modifier.fillMaxSize()) {
|
||||
* MainContent()
|
||||
* TangemTopSnackbarHost(
|
||||
* hostState = topSnackbarHostState,
|
||||
* modifier = Modifier
|
||||
* .align(Alignment.TopCenter)
|
||||
* .statusBarsPadding()
|
||||
* .padding(top = TangemTheme.dimens.spacing8),
|
||||
* )
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param hostState state that controls which snackbar is shown
|
||||
* @param modifier modifier applied to the host container
|
||||
*/
|
||||
@Composable
|
||||
fun TangemTopSnackbarHost(hostState: TangemTopSnackbarHostState, modifier: Modifier = Modifier) {
|
||||
val myDepth = remember(hostState) { hostState.registerHost() }
|
||||
DisposableEffect(hostState) {
|
||||
onDispose { hostState.unregisterHost(myDepth) }
|
||||
}
|
||||
|
||||
// Only the deepest host in the composition tree handles the snackbar.
|
||||
val isDeepest = hostState.activeHostDepth == myDepth
|
||||
val currentSnackbar = if (isDeepest) hostState.currentSnackbar else null
|
||||
val accessibilityManager = LocalAccessibilityManager.current
|
||||
|
||||
LaunchedEffect(currentSnackbar) {
|
||||
if (currentSnackbar == null) return@LaunchedEffect
|
||||
val duration = currentSnackbar.duration.toMillis(
|
||||
hasAction = currentSnackbar.action != null,
|
||||
accessibilityManager = accessibilityManager,
|
||||
)
|
||||
delay(duration)
|
||||
currentSnackbar.onDismissRequest()
|
||||
}
|
||||
|
||||
ScaleFromTopWithFade(
|
||||
current = currentSnackbar,
|
||||
modifier = modifier,
|
||||
) { snackbar ->
|
||||
TangemTopSnackbar(
|
||||
snackbarMessage = snackbar,
|
||||
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun SnackbarMessage.Duration.toMillis(hasAction: Boolean, accessibilityManager: AccessibilityManager?): Long {
|
||||
val original =
|
||||
when (this) {
|
||||
SnackbarMessage.Duration.Indefinite -> Long.MAX_VALUE
|
||||
SnackbarMessage.Duration.Long -> 10000L
|
||||
SnackbarMessage.Duration.Short -> 4000L
|
||||
}
|
||||
if (accessibilityManager == null) {
|
||||
return original
|
||||
}
|
||||
return accessibilityManager.calculateRecommendedTimeoutMillis(
|
||||
originalTimeoutMillis = original,
|
||||
containsIcons = true,
|
||||
containsText = true,
|
||||
containsControls = hasAction,
|
||||
)
|
||||
}
|
||||
|
||||
@Stable
|
||||
class TangemTopSnackbarHostState {
|
||||
|
||||
private val mutex = Mutex()
|
||||
|
||||
var currentSnackbar by mutableStateOf<SnackbarMessage?>(null)
|
||||
|
||||
// Tracks which depth level is the deepest registered host.
|
||||
// Composed as observable state so hosts recompose when a deeper/shallower one is added/removed.
|
||||
var activeHostDepth by mutableIntStateOf(0)
|
||||
private set
|
||||
|
||||
private var hostDepthCounter = 0
|
||||
|
||||
internal fun registerHost(): Int {
|
||||
hostDepthCounter++
|
||||
activeHostDepth = hostDepthCounter
|
||||
return hostDepthCounter
|
||||
}
|
||||
|
||||
internal fun unregisterHost(depth: Int) {
|
||||
if (depth == hostDepthCounter) {
|
||||
hostDepthCounter--
|
||||
activeHostDepth = hostDepthCounter
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun showSnackbar(
|
||||
message: String,
|
||||
actionLabel: String? = null,
|
||||
withDismissAction: Boolean = false,
|
||||
duration: SnackbarMessage.Duration =
|
||||
if (actionLabel == null) SnackbarMessage.Duration.Short else SnackbarMessage.Duration.Indefinite,
|
||||
) {
|
||||
showSnackbar(
|
||||
SnackbarMessage(
|
||||
message = stringReference(message),
|
||||
actionLabel = actionLabel?.let { stringReference(it) },
|
||||
duration = duration,
|
||||
onDismissRequest = {},
|
||||
action = if (withDismissAction) {
|
||||
null
|
||||
} else {
|
||||
{}
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun showSnackbar(message: SnackbarMessage) {
|
||||
mutex.withLock {
|
||||
try {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
currentSnackbar = message.withDismiss(
|
||||
onDismiss = { if (continuation.isActive) continuation.resume(Unit) },
|
||||
)
|
||||
}
|
||||
} finally {
|
||||
currentSnackbar = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun SnackbarMessage.withDismiss(onDismiss: () -> Unit): SnackbarMessage {
|
||||
return copy(
|
||||
onDismissRequest = {
|
||||
onDismissRequest()
|
||||
onDismiss()
|
||||
},
|
||||
action = if (action != null) {
|
||||
{
|
||||
action.invoke()
|
||||
onDismiss()
|
||||
}
|
||||
} else {
|
||||
null
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// Adapted from Material3's FadeInFadeOutWithScale, with scale pivot anchored at the top center.
|
||||
@Suppress("UnsafeCallOnNullableType")
|
||||
@Composable
|
||||
private fun ScaleFromTopWithFade(
|
||||
current: SnackbarMessage?,
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable (SnackbarMessage) -> Unit,
|
||||
) {
|
||||
val state = remember { ScaleFromTopState<SnackbarMessage?>() }
|
||||
if (current != state.current) {
|
||||
state.current = current
|
||||
val keys = state.items.map { it.key }.toMutableList()
|
||||
if (!keys.contains(current)) keys.add(current)
|
||||
state.items.clear()
|
||||
keys.filterNotNull().mapTo(state.items) { key ->
|
||||
ScaleFromTopItem(key) { children ->
|
||||
val isVisible = key == current
|
||||
val opacity = animatedOpacity(
|
||||
animation = tween(durationMillis = 200),
|
||||
visible = isVisible,
|
||||
onAnimationFinish = {
|
||||
if (key != state.current) {
|
||||
state.items.removeAll { it.key == key }
|
||||
state.scope?.invalidate()
|
||||
}
|
||||
},
|
||||
)
|
||||
val scale = animatedScale(
|
||||
enterAnimation = spring(
|
||||
dampingRatio = Spring.DampingRatioMediumBouncy,
|
||||
stiffness = Spring.StiffnessMedium,
|
||||
),
|
||||
exitAnimation = tween(durationMillis = 180),
|
||||
visible = isVisible,
|
||||
)
|
||||
Box(
|
||||
Modifier.graphicsLayer(
|
||||
scaleX = scale.value,
|
||||
scaleY = scale.value,
|
||||
alpha = opacity.value,
|
||||
transformOrigin = TransformOrigin(pivotFractionX = 0.5f, pivotFractionY = 0f),
|
||||
),
|
||||
) {
|
||||
children()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Box(modifier = modifier.fillMaxWidth(), contentAlignment = Alignment.TopCenter) {
|
||||
state.scope = currentRecomposeScope
|
||||
state.items.forEach { (item, transition) -> key(item) { transition { content(item!!) } } }
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DoubleMutabilityForCollection")
|
||||
private class ScaleFromTopState<T> {
|
||||
var current: Any? = Any()
|
||||
var items = mutableListOf<ScaleFromTopItem<T>>()
|
||||
var scope: RecomposeScope? = null
|
||||
}
|
||||
|
||||
private data class ScaleFromTopItem<T>(
|
||||
val key: T,
|
||||
val transition: @Composable (content: @Composable () -> Unit) -> Unit,
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun animatedOpacity(
|
||||
animation: AnimationSpec<Float>,
|
||||
visible: Boolean,
|
||||
onAnimationFinish: () -> Unit = {},
|
||||
): State<Float> {
|
||||
val alpha = remember { Animatable(if (visible) 0f else 1f) }
|
||||
LaunchedEffect(visible) {
|
||||
alpha.animateTo(if (visible) 1f else 0f, animationSpec = animation)
|
||||
onAnimationFinish()
|
||||
}
|
||||
return alpha.asState()
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun animatedScale(
|
||||
enterAnimation: AnimationSpec<Float>,
|
||||
exitAnimation: AnimationSpec<Float>,
|
||||
visible: Boolean,
|
||||
): State<Float> {
|
||||
val scale = remember { Animatable(if (visible) 0.85f else 1f) }
|
||||
LaunchedEffect(visible) {
|
||||
scale.animateTo(
|
||||
targetValue = if (visible) 1f else 0.85f,
|
||||
animationSpec = if (visible) enterAnimation else exitAnimation,
|
||||
)
|
||||
}
|
||||
return scale.asState()
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@ import com.tangem.core.ui.extensions.resourceReference
|
|||
@Immutable
|
||||
sealed interface EventMessage : UiMessage
|
||||
|
||||
@Immutable
|
||||
data class ToastMessage(
|
||||
val message: TextReference,
|
||||
val duration: Duration = ToastMessage.Duration.Short,
|
||||
|
|
@ -48,8 +49,10 @@ data class ToastMessage(
|
|||
* @param actionLabel The label of the action button. Optional, `null` by default.
|
||||
* @param action The action to perform when the action button is clicked. Optional, `null` by default.
|
||||
* */
|
||||
@Immutable
|
||||
data class SnackbarMessage(
|
||||
val message: TextReference,
|
||||
@field:DrawableRes val startIconId: Int? = null,
|
||||
val duration: Duration = Duration.Short,
|
||||
val onDismissRequest: () -> Unit = {},
|
||||
val actionLabel: TextReference? = null,
|
||||
|
|
@ -92,6 +95,7 @@ data class SnackbarMessage(
|
|||
* `true` by default.
|
||||
* @param onDismissRequest The action to perform when the dialog is dismissed.
|
||||
* */
|
||||
@Immutable
|
||||
data class DialogMessage(
|
||||
val message: TextReference,
|
||||
val title: TextReference? = null,
|
||||
|
|
@ -146,6 +150,7 @@ data class DialogMessage(
|
|||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class GlobalLoadingMessage(val isShow: Boolean) : EventMessage
|
||||
|
||||
/**
|
||||
|
|
@ -159,6 +164,7 @@ data class GlobalLoadingMessage(val isShow: Boolean) : EventMessage
|
|||
* @param secondAction The second action to perform. Optional, `null` by default.
|
||||
* @param onDismissRequest The action to perform when the bottom sheet is dismissed.
|
||||
* */
|
||||
@Immutable
|
||||
data class BottomSheetMessage(
|
||||
@DrawableRes val iconResId: Int? = null,
|
||||
val title: TextReference? = null,
|
||||
|
|
@ -202,6 +208,7 @@ data class BottomSheetMessage(
|
|||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class BottomSheetMessageV2(
|
||||
val messageBottomSheetUMV2: MessageBottomSheetUMV2,
|
||||
) : EventMessage
|
||||
|
|
|
|||
|
|
@ -23,10 +23,13 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
|||
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUM
|
||||
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetV2
|
||||
import com.tangem.core.ui.components.snackbar.TangemTopSnackbarHostState
|
||||
import com.tangem.core.ui.event.EventEffect
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.LocalEventMessageHandler
|
||||
import com.tangem.core.ui.res.LocalRedesignEnabled
|
||||
import com.tangem.core.ui.res.LocalSnackbarHostState
|
||||
import com.tangem.core.ui.res.LocalTopSnackbarHostState
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
@Composable
|
||||
|
|
@ -36,6 +39,10 @@ fun EventMessageEffect(
|
|||
onShowSnackbar: suspend (SnackbarMessage, Context) -> Unit = { message, context ->
|
||||
showSnackbar(snackbarHostState, message, context)
|
||||
},
|
||||
topSnackbarHostState: TangemTopSnackbarHostState = LocalTopSnackbarHostState.current,
|
||||
onShowTopSnackbar: suspend (SnackbarMessage) -> Unit = { message ->
|
||||
topSnackbarHostState.showSnackbar(message)
|
||||
},
|
||||
onShowToast: (ToastMessage, Context) -> Unit = { message, context -> showToast(message, context) },
|
||||
) {
|
||||
val messageEvent by messageHandler.collectAsState()
|
||||
|
|
@ -45,11 +52,16 @@ fun EventMessageEffect(
|
|||
var bottomSheetMessage: BottomSheetMessage? by remember { mutableStateOf(value = null) }
|
||||
var bottomSheetMessageV2: BottomSheetMessageV2? by remember { mutableStateOf(value = null) }
|
||||
var loadingMessage: GlobalLoadingMessage? by remember { mutableStateOf(value = null) }
|
||||
val isRedesignEnabled = LocalRedesignEnabled.current
|
||||
|
||||
EventEffect(event = messageEvent) { message ->
|
||||
when (message) {
|
||||
is SnackbarMessage -> {
|
||||
onShowSnackbar(message, context)
|
||||
if (isRedesignEnabled) {
|
||||
onShowTopSnackbar(message)
|
||||
} else {
|
||||
onShowSnackbar(message, context)
|
||||
}
|
||||
}
|
||||
is DialogMessage -> {
|
||||
dialogMessage = message
|
||||
|
|
@ -220,7 +232,8 @@ private fun showToast(message: ToastMessage, context: Context) {
|
|||
Toast.makeText(
|
||||
/* context = */ context,
|
||||
/* text = */ message.message.resolveReference(context.resources),
|
||||
/* duration = */ when (message.duration) {
|
||||
/* duration = */
|
||||
when (message.duration) {
|
||||
ToastMessage.Duration.Short -> Toast.LENGTH_SHORT
|
||||
ToastMessage.Duration.Long -> Toast.LENGTH_LONG
|
||||
},
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import com.tangem.core.ui.components.SystemBarsIconsController
|
|||
import com.tangem.core.ui.components.TangemShimmer
|
||||
import com.tangem.core.ui.components.powersaving.PowerSavingState
|
||||
import com.tangem.core.ui.components.powersaving.rememberPowerSavingState
|
||||
import com.tangem.core.ui.components.snackbar.TangemTopSnackbarHostState
|
||||
import com.tangem.core.ui.components.text.BladeAnimation
|
||||
import com.tangem.core.ui.components.text.rememberBladeAnimation
|
||||
import com.tangem.core.ui.haptic.DefaultHapticManager
|
||||
|
|
@ -54,6 +55,7 @@ fun TangemTheme(
|
|||
vibratorHapticManager = uiDependencies.vibratorHapticManager,
|
||||
snackbarHostState = uiDependencies.globalSnackbarHostState,
|
||||
eventMessageHandler = uiDependencies.eventMessageHandler,
|
||||
topSnackbarHostState = uiDependencies.globalTopSnackbarHostState,
|
||||
overrideSystemBarColors = overrideSystemBarColors,
|
||||
typography = typography,
|
||||
dimens = dimens,
|
||||
|
|
@ -76,6 +78,7 @@ fun TangemTheme(
|
|||
vibratorHapticManager: VibratorHapticManager? = null,
|
||||
eventMessageHandler: EventMessageHandler = remember { EventMessageHandler() },
|
||||
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
|
||||
topSnackbarHostState: TangemTopSnackbarHostState = remember { TangemTopSnackbarHostState() },
|
||||
overrideSystemBarColors: Boolean = true,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
|
|
@ -119,6 +122,7 @@ fun TangemTheme(
|
|||
LocalIsInDarkTheme provides isDark,
|
||||
LocalHapticManager provides hapticManager,
|
||||
LocalSnackbarHostState provides snackbarHostState,
|
||||
LocalTopSnackbarHostState provides topSnackbarHostState,
|
||||
LocalEventMessageHandler provides eventMessageHandler,
|
||||
LocalWindowSize provides windowSize,
|
||||
LocalBladeAnimation provides rememberBladeAnimation(),
|
||||
|
|
@ -389,6 +393,10 @@ val LocalSnackbarHostState = staticCompositionLocalOf<SnackbarHostState> {
|
|||
error("No SnackbarHostState provided")
|
||||
}
|
||||
|
||||
val LocalTopSnackbarHostState = staticCompositionLocalOf<TangemTopSnackbarHostState> {
|
||||
error("No TangemTopSnackbarHostState provided")
|
||||
}
|
||||
|
||||
val LocalEventMessageHandler = staticCompositionLocalOf<EventMessageHandler> {
|
||||
error("No EventMessageHandler provided")
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue