Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-22 17:23:08 +04:00
commit bac330628f
29 changed files with 756 additions and 150 deletions

View file

@ -129,7 +129,7 @@
},
{
"name": "AND_15482_SURVEYSPARROW_ENABLED",
"version": "6.0"
"version": "undefined"
},
{
"name": "AND_15258_QUICK_TOP_UP_ENABLED",

View file

@ -2,11 +2,9 @@ package com.tangem.core.ui.components.haze
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import com.tangem.core.ui.res.LocalHazeState
import com.tangem.core.ui.res.LocalPowerSavingState
import com.tangem.core.ui.res.LocalRootBackgroundColor
import dev.chrisbanes.haze.*
import dev.chrisbanes.haze.materials.CupertinoMaterials
@ -25,32 +23,19 @@ fun ProvideHaze(content: @Composable () -> Unit) {
}
/**
* Returns whether the haze blur effect would actually render for the given [state], taking both
* the global [HazeState.blurEnabled] flag and the device's power-saving mode into account.
*
* Callers that pass a fully-transparent fallback to [hazeEffectTangem] should use this to decide
* whether they need to render an opaque fallback layer themselves otherwise the surface can
* become invisible whenever blur is disabled (e.g. while power-saving mode is on).
*/
@Composable
fun isHazeBlurEffectivelyEnabled(state: HazeState = LocalHazeState.current): Boolean {
val isPowerSavingEnabled by LocalPowerSavingState.current.isPowerSavingModeEnabled.collectAsState()
return state.blurEnabled && !isPowerSavingEnabled
}
/**
* Applies a haze effect to the [Modifier] with consideration of global haze settings and power saving mode.
* Applies a haze effect to the [Modifier] with consideration of global haze settings.
*
* @param configure A lambda to configure the [HazeEffectScope].
* @return A [Modifier] with the configured haze effect applied.
*/
@OptIn(ExperimentalHazeMaterialsApi::class)
@Composable
fun Modifier.hazeEffectTangem(
state: HazeState = LocalHazeState.current,
style: HazeStyle = CupertinoMaterials.ultraThin(),
configure: HazeEffectScope.() -> Unit = {},
): Modifier {
val isGlobalBlurEnabled = isHazeBlurEffectivelyEnabled(state)
val isGlobalBlurEnabled = state.blurEnabled
val rootBackground by LocalRootBackgroundColor.current
return hazeEffect(state, style) {
@ -66,25 +51,19 @@ fun Modifier.hazeEffectTangem(
*
* @param style The [HazeStyle] to apply. Defaults to [HazeStyle.Unspecified].
* @param isBlurEnabled A Boolean indicating whether blur is enabled. Defaults to true.
* @param reactToPowerSavingMode A Boolean indicating whether the haze effect should react to power saving mode.
* Defaults to false.
* @param configure A lambda to configure the [HazeEffectScope].
* @return A [Modifier] with the configured haze foreground effect applied.
*/
@Composable
fun Modifier.hazeForegroundEffectTangem(
style: HazeStyle = HazeStyle.Unspecified,
reactToPowerSavingMode: Boolean = false,
isBlurEnabled: Boolean = true,
configure: HazeEffectScope.() -> Unit = {},
): Modifier {
val powerSavingEnabled = LocalPowerSavingState.current.isPowerSavingModeEnabled.collectAsState()
val isGlobalBlurEnabled = isBlurEnabled && (!reactToPowerSavingMode || !powerSavingEnabled.value)
return hazeEffect(
style = style,
) {
blurEnabled = isGlobalBlurEnabled
blurEnabled = isBlurEnabled
configure()
}
}

View file

@ -74,8 +74,9 @@ fun TangemContextMenu(
}
}
private const val IN_TRANSITION_DURATION = 120
private const val OUT_TRANSITION_DURATION = 75
private const val ENTER_SPRING_DAMPING = 0.82f
private const val ENTER_SPRING_STIFFNESS = 1100f
@Suppress("ReusedModifierInstance", "MagicNumber")
@Composable
@ -91,10 +92,10 @@ private fun DropdownMenuContent(
val scale by transition.animateFloat(
transitionSpec = {
if (false isTransitioningTo true) {
// Dismissed to expanded
tween(
durationMillis = IN_TRANSITION_DURATION,
easing = LinearOutSlowInEasing,
// Dismissed to expanded — springy iOS-like pop scaling up from the anchor.
spring(
dampingRatio = ENTER_SPRING_DAMPING,
stiffness = ENTER_SPRING_STIFFNESS,
)
} else {
// Expanded to dismissed.

View file

@ -3,6 +3,7 @@ package com.tangem.core.ui.ds.contextmenu
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.ds.checkbox.TangemCheckbox
@ -18,12 +19,13 @@ import com.tangem.core.ui.res.TangemTheme
fun TangemContextMenuCheckboxItem(title: TextReference, isChecked: Boolean, onClick: () -> Unit) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.width(238.dp)
.clickableSingle(onClick = onClick)
.padding(
vertical = TangemTheme.dimens2.x5,
vertical = TangemTheme.dimens2.x2_5,
horizontal = TangemTheme.dimens2.x4,
),
) {

View file

@ -7,7 +7,9 @@ import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.ScrollScope
import androidx.compose.foundation.gestures.scrollable
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
@ -19,7 +21,10 @@ import androidx.compose.ui.unit.dp
import com.tangem.core.ui.ds.topbar.collapsing.entity.TangemCollapsingAppBarState
import com.tangem.core.ui.ds.topbar.collapsing.entity.TopBarScrollDirection
import com.tangem.core.ui.ds.topbar.collapsing.entity.rememberTangemCollapsingAppBarState
import com.tangem.core.ui.haptic.TangemHapticEffect
import com.tangem.core.ui.res.LocalHapticManager
import com.tangem.core.ui.utils.toPx
import kotlinx.coroutines.flow.drop
import kotlin.math.abs
import kotlin.math.absoluteValue
@ -48,6 +53,9 @@ fun rememberTangemExitUntilCollapsedScrollBehavior(
partialHeightLimit = partialCollapsedHeight.toPx(),
isTopOverscrollEnabled = isTopOverscrollEnabled,
)
SnapThresholdHapticEffect(state = topBarState)
return exitUntilCollapsedScrollBehavior(
state = topBarState,
snapAnimationSpec = snapAnimationSpec,
@ -55,6 +63,35 @@ fun rememberTangemExitUntilCollapsedScrollBehavior(
)
}
/**
* Provides a detent-style haptic feedback as the collapsing app bar crosses the threshold between its
* expanded and collapsed snap states
*
* @param state The state of the collapsing app bar to observe.
* @param snapThreshold The collapsed fraction (0f..1f) at which the haptic detent fires. Default is 0.5f,
* the midpoint between the collapse and expand snap thresholds.
*/
@Composable
private fun SnapThresholdHapticEffect(
state: TangemCollapsingAppBarState,
@Suppress("MagicNumber") snapThreshold: Float = 0.5f,
) {
val hapticManager = LocalHapticManager.current
LaunchedEffect(state, hapticManager, snapThreshold) {
snapshotFlow { state.collapsedFraction >= snapThreshold }
.drop(1) // skip the initial value so we only react to actual crossings
.collect { isPastThreshold ->
hapticManager.perform(
if (isPastThreshold) {
TangemHapticEffect.View.GestureThresholdActivate
} else {
TangemHapticEffect.View.GestureThresholdDeactivate
},
)
}
}
}
/**
* A scroll behavior for a collapsing top app bar that collapses when scrolling up and expands when scrolling down.
* When the user stops scrolling, the app bar will settle to either fully collapsed or fully expanded state

View file

@ -18,9 +18,9 @@ import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.haze.hazeEffectTangem
import com.tangem.core.ui.components.haze.isHazeBlurEffectivelyEnabled
import com.tangem.core.ui.extensions.conditionalCompose
import com.tangem.core.ui.extensions.softLayerShadow
import com.tangem.core.ui.res.LocalHazeState
import com.tangem.core.ui.res.TangemTheme
import dev.chrisbanes.haze.HazeStyle
import dev.chrisbanes.haze.HazeTint
@ -134,14 +134,13 @@ private fun Modifier.materialBorder(shape: Shape): Modifier = border(
* When the haze state is enabled, paints a haze-blurred backdrop. When disabled, layers two
* solid colors so the result still reads as "tinted fill" instead of going transparent.
*
* Uses [isHazeBlurEffectivelyEnabled] (rather than [LocalHazeState]'s `blurEnabled` directly) so
* the solid fallback is also applied when blur is suppressed for reasons other than the haze flag
* most notably when the device is in power-saving mode. Otherwise the haze modifier's
* `fallbackTint = HazeTint(Color.Transparent)` would leave the surface fully transparent.
* Reads [LocalHazeState]'s `blurEnabled` so the solid fallback is applied whenever blur is off
* otherwise the haze modifier's `fallbackTint = HazeTint(Color.Transparent)` would leave the
* surface fully transparent.
*/
@Composable
private fun Modifier.materialFill(): Modifier {
val isBlurEnabled = isHazeBlurEffectivelyEnabled()
val isBlurEnabled = LocalHazeState.current.blurEnabled
val hazed = hazeEffectTangem(
style = HazeStyle(
backgroundColor = TangemTheme.colors3.material.fill.blur,