Updated on 2026-08-14
This commit is contained in:
commit
bac330628f
29 changed files with 756 additions and 150 deletions
|
|
@ -129,7 +129,7 @@
|
|||
},
|
||||
{
|
||||
"name": "AND_15482_SURVEYSPARROW_ENABLED",
|
||||
"version": "6.0"
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "AND_15258_QUICK_TOP_UP_ENABLED",
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue