Updated on 2026-08-14

This commit is contained in:
Tangem 2026-05-12 09:49:50 +02:00
parent 03ae5e3331
commit a54c3ce6ea
17 changed files with 269 additions and 241 deletions

View file

@ -1,8 +1,6 @@
package com.tangem.common.ui.markets.tokenselector
import android.content.res.Configuration
import androidx.compose.animation.core.EaseOut
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape
@ -10,6 +8,7 @@ import androidx.compose.material3.Icon
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
@ -19,19 +18,18 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.Fade
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetType
import com.tangem.core.ui.components.haze.hazeEffectTangem
import com.tangem.core.ui.components.haze.hazeSourceTangem
import com.tangem.core.ui.components.topFade
import com.tangem.core.ui.ds.topbar.TangemTopBar
import com.tangem.core.ui.ds.topbar.TangemTopBarType
import com.tangem.core.ui.extensions.clickableSingle
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import dev.chrisbanes.haze.HazeProgressive
import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.rememberHazeState
@ -84,8 +82,11 @@ private fun TokenSelectorContent(
Box(modifier = modifier.fillMaxWidth()) {
val bottomFadeReserve = if (embedded) 0.dp else TangemTheme.dimens2.x10
val bottomListPadding = bottomFadeReserve + scrollBottomInset
val topFadeColor = TangemTheme.colors2.surface.level2.copy(alpha = .95f)
LazyColumn(
modifier = Modifier.hazeSourceTangem(state = hazeState, 1f),
modifier = Modifier
.hazeSourceTangem(state = hazeState, 1f)
.topFade(height = topBarHeight, color = topFadeColor, solidStop = .6f),
contentPadding = PaddingValues(
start = TangemTheme.dimens2.x4,
end = TangemTheme.dimens2.x4,
@ -102,12 +103,6 @@ private fun TokenSelectorContent(
hazeState = hazeState,
onChangeHeight = { topBarHeight = it },
)
Fade(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomCenter),
height = TangemTheme.dimens2.x10,
)
}
}
}
@ -119,7 +114,6 @@ private fun TokenSelectorSheetTopBar(
onChangeHeight: (Dp) -> Unit,
modifier: Modifier = Modifier,
) {
val bgColor = TangemTheme.colors2.surface.level2
val density = LocalDensity.current
TangemTopBar(
modifier = modifier
@ -129,15 +123,6 @@ private fun TokenSelectorSheetTopBar(
onChangeHeight(coordinates.size.height.toDp())
}
}
}
.hazeEffectTangem(state = hazeState) {
backgroundColor = bgColor
progressive = HazeProgressive.verticalGradient(
startIntensity = .55f,
endIntensity = 0f,
preferPerformance = true,
easing = EaseOut,
)
},
type = TangemTopBarType.BottomSheet,
title = resourceReference(R.string.markets_search_portfolio_header),
@ -148,10 +133,8 @@ private fun TokenSelectorSheetTopBar(
tint = TangemTheme.colors2.graphic.neutral.primary,
modifier = Modifier
.size(TangemTheme.dimens2.x11)
.background(
color = TangemTheme.colors2.button.backgroundSecondary,
shape = CircleShape,
)
.clip(CircleShape)
.hazeEffectTangem(state = hazeState) { blurRadius = 8.dp }
.clickableSingle(onClick = onDismiss)
.padding(TangemTheme.dimens2.x2_5),
)

View file

@ -47,6 +47,18 @@ fun BottomFade(
)
}
@Composable
fun TopFade(vararg colorStops: Pair<Float, Color>, modifier: Modifier = Modifier, height: Dp = 100.dp) {
Box(
modifier = modifier
.fillMaxWidth()
.height(height)
.background(
brush = Brush.verticalGradient(colorStops = colorStops),
),
)
}
/**
* A composable that draws a fade effect at the bottom of the screen. Used on screens with a list of repeating
* elements and floating button at the bottom of the screen.

View file

@ -25,10 +25,8 @@ fun Modifier.edgeFade(
isVisible: Boolean = true,
animationSpec: AnimationSpec<Dp>? = null,
color: Color = TangemTheme.colors.background.secondary,
solidStop: Float = 0f,
): Modifier = composed {
require(value = size > 0.dp) {
"Size must be greater than '0'"
}
val animatedSize = animationSpec?.let { spec ->
animateDpAsState(
targetValue = if (isVisible) size else 0.dp,
@ -45,6 +43,7 @@ fun Modifier.edgeFade(
val staticSizePx = if (isVisible) size.toPx() else 0f
val sizePx = animatedSize?.value?.toPx() ?: staticSizePx
if (sizePx <= 0f) return@forEach
val fraction = when (side) {
FadePosition.LEFT, FadePosition.RIGHT -> sizePx / this.size.width
@ -54,6 +53,7 @@ fun Modifier.edgeFade(
drawRect(
brush = Brush.linearGradient(
0f to color,
solidStop.coerceIn(minimumValue = 0f, maximumValue = 1f) * fraction to color,
fraction to Color.Transparent,
start = start,
end = end,
@ -74,6 +74,25 @@ fun Modifier.bottomFade(
color = color,
)
/**
* Draws a vertical gradient fade over the top edge of the content.
*
* @param height the fade region height
* @param color the solid color at the top edge that fades to transparent at the bottom of the region
* @param solidStop fraction (0..1) of [height] kept fully [color] before the fade starts
*/
@Composable
fun Modifier.topFade(
height: Dp,
color: Color = TangemTheme.colors.background.secondary,
solidStop: Float = 0f,
): Modifier = edgeFade(
FadePosition.TOP,
size = height,
color = color,
solidStop = solidStop,
)
enum class FadePosition {
TOP, BOTTOM, LEFT, RIGHT
}

View file

@ -46,9 +46,11 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import com.tangem.core.ui.R
import com.tangem.core.ui.components.fields.entity.SearchBarUM
import com.tangem.core.ui.components.haze.hazeEffectTangem
import com.tangem.core.ui.ds.button.GhostTangemButton
import com.tangem.core.ui.ds.button.TangemButtonSize
import com.tangem.core.ui.extensions.resolveReference
@ -177,7 +179,11 @@ private fun DecorationBox(
contentAlignment = BiasAlignment(horizontalBias = alignmentBias, verticalBias = 0f),
modifier = Modifier
.weight(1f)
.background(color, shape.toShape())
.clip(shape.toShape())
.background(color)
.hazeEffectTangem {
blurRadius = 8.dp
}
.padding(TangemTheme.dimens2.x3),
) {
Row(

View file

@ -1,7 +1,5 @@
package com.tangem.features.feed.components.earn
import androidx.compose.animation.core.EaseOut
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@ -11,9 +9,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.extensions.compose.subscribeAsState
@ -41,7 +40,6 @@ import com.tangem.features.feed.model.earn.EarnModel
import com.tangem.features.feed.model.earn.analytics.EarnSource
import com.tangem.features.feed.ui.components.FeedSearchBar
import com.tangem.features.feed.ui.earn.EarnContent
import dev.chrisbanes.haze.HazeProgressive
internal class DefaultEarnComponent(
appComponentContext: AppComponentContext,
@ -66,16 +64,6 @@ internal class DefaultEarnComponent(
FeedSearchBar(
isSearchBarClickable = bottomSheetState.value == BottomSheetState.EXPANDED,
feedListSearchBar = state.feedListSearchBar,
modifier = Modifier
.drawBehind { drawRect(background) }
.hazeEffectTangem {
progressive = HazeProgressive.verticalGradient(
startIntensity = .55f,
endIntensity = 0f,
preferPerformance = true,
easing = EaseOut,
)
},
startContent = {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_arrow_back_28),
@ -83,10 +71,8 @@ internal class DefaultEarnComponent(
tint = TangemTheme.colors2.graphic.neutral.primary,
modifier = Modifier
.size(TangemTheme.dimens2.x11)
.background(
color = TangemTheme.colors2.button.backgroundSecondary,
shape = CircleShape,
)
.clip(CircleShape)
.hazeEffectTangem { blurRadius = 8.dp }
.clickableSingle(
onClick = state.onBackClick,
enabled = bottomSheetState.value == BottomSheetState.EXPANDED,

View file

@ -1,7 +1,5 @@
package com.tangem.features.feed.components.market.details
import androidx.compose.animation.core.EaseOut
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@ -12,8 +10,10 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.LifecycleStartEffect
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.decompose.ComponentContext
@ -50,7 +50,6 @@ import com.tangem.features.feed.model.market.details.analytics.MarketDetailsAnal
import com.tangem.features.feed.model.market.details.state.TokenNetworksState
import com.tangem.features.feed.ui.market.detailed.MarketsTokenDetailsContent
import com.tangem.features.feed.ui.market.detailed.MarketsTokenDetailsTopBar
import dev.chrisbanes.haze.HazeProgressive
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
@ -161,14 +160,6 @@ internal class DefaultMarketsTokenDetailsComponent(
val background = LocalMainBottomSheetColor.current.value
if (LocalRedesignEnabled.current) {
TangemTopBar(
modifier = Modifier.hazeEffectTangem {
progressive = HazeProgressive.verticalGradient(
startIntensity = .55f,
endIntensity = 0f,
preferPerformance = true,
easing = EaseOut,
)
},
startContent = {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_arrow_back_28),
@ -176,10 +167,8 @@ internal class DefaultMarketsTokenDetailsComponent(
tint = TangemTheme.colors2.graphic.neutral.primary,
modifier = Modifier
.size(TangemTheme.dimens2.x11)
.background(
color = TangemTheme.colors2.button.backgroundSecondary,
shape = CircleShape,
)
.clip(CircleShape)
.hazeEffectTangem { blurRadius = 8.dp }
.clickableSingle(
onClick = { params.onBackClicked() },
enabled = bottomSheetState.value == BottomSheetState.EXPANDED,
@ -194,10 +183,8 @@ internal class DefaultMarketsTokenDetailsComponent(
tint = TangemTheme.colors2.graphic.neutral.primary,
modifier = Modifier
.size(TangemTheme.dimens2.x11)
.background(
color = TangemTheme.colors2.button.backgroundSecondary,
shape = CircleShape,
)
.clip(CircleShape)
.hazeEffectTangem { blurRadius = 8.dp }
.clickableSingle(
onClick = state.onShareClick,
enabled = bottomSheetState.value == BottomSheetState.EXPANDED,

View file

@ -11,9 +11,10 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.LifecycleStartEffect
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
@ -34,7 +35,6 @@ import com.tangem.features.feed.model.market.list.state.SortByTypeUM
import com.tangem.features.feed.ui.components.FeedSearchBar
import com.tangem.features.feed.ui.market.list.MarketsList
import com.tangem.features.feed.ui.market.list.TopBarWithSearch
import dev.chrisbanes.haze.HazeProgressive
import kotlinx.serialization.Serializable
internal class DefaultMarketsTokenListComponent(
@ -54,21 +54,13 @@ internal class DefaultMarketsTokenListComponent(
override fun Title(bottomSheetState: State<BottomSheetState>) {
val state by model.state.collectAsStateWithLifecycle()
val bsState by bottomSheetState
val background = LocalMainBottomSheetColor.current.value
if (LocalRedesignEnabled.current) {
val background = LocalMainBottomSheetColor.current.value
FeedSearchBar(
isSearchBarClickable = bottomSheetState.value == BottomSheetState.EXPANDED,
feedListSearchBar = state.feedListSearchBar,
modifier = Modifier
.drawBehind { drawRect(background) }
.hazeEffectTangem {
progressive = HazeProgressive.verticalGradient(
startIntensity = .55f,
endIntensity = .2f,
preferPerformance = true,
)
},
modifier = Modifier.background(background.copy(alpha = .95f)),
startContent = {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_arrow_back_28),
@ -76,10 +68,8 @@ internal class DefaultMarketsTokenListComponent(
tint = TangemTheme.colors2.graphic.neutral.primary,
modifier = Modifier
.size(TangemTheme.dimens2.x11)
.background(
color = TangemTheme.colors2.button.backgroundSecondary,
shape = CircleShape,
)
.clip(CircleShape)
.hazeEffectTangem { blurRadius = 8.dp }
.clickableSingle(
onClick = clickIntents.onBackClicked,
enabled = bottomSheetState.value == BottomSheetState.EXPANDED,

View file

@ -1,7 +1,5 @@
package com.tangem.features.feed.components.news.details
import androidx.compose.animation.core.EaseOut
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@ -11,8 +9,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
@ -34,7 +34,6 @@ import com.tangem.domain.news.model.NewsListConfig
import com.tangem.features.feed.model.news.details.NewsDetailsModel
import com.tangem.features.feed.ui.news.details.NewsDetailsContent
import com.tangem.features.feed.ui.news.details.state.ArticlesStateUM
import dev.chrisbanes.haze.HazeProgressive
import kotlinx.serialization.Serializable
internal class DefaultNewsDetailsComponent(
@ -50,14 +49,6 @@ internal class DefaultNewsDetailsComponent(
val state by newsDetailsModel.state.collectAsStateWithLifecycle()
if (LocalRedesignEnabled.current) {
TangemTopBar(
modifier = Modifier.hazeEffectTangem {
progressive = HazeProgressive.verticalGradient(
startIntensity = .55f,
endIntensity = 0f,
preferPerformance = true,
easing = EaseOut,
)
},
type = TangemTopBarType.BottomSheet,
startContent = {
Icon(
@ -66,10 +57,8 @@ internal class DefaultNewsDetailsComponent(
tint = TangemTheme.colors2.graphic.neutral.primary,
modifier = Modifier
.size(TangemTheme.dimens2.x11)
.background(
color = TangemTheme.colors2.button.backgroundSecondary,
shape = CircleShape,
)
.clip(CircleShape)
.hazeEffectTangem { blurRadius = 8.dp }
.clickableSingle(
onClick = state.onBackClick,
enabled = bottomSheetState.value == BottomSheetState.EXPANDED,
@ -84,10 +73,8 @@ internal class DefaultNewsDetailsComponent(
tint = TangemTheme.colors2.graphic.neutral.primary,
modifier = Modifier
.size(TangemTheme.dimens2.x11)
.background(
color = TangemTheme.colors2.button.backgroundSecondary,
shape = CircleShape,
)
.clip(CircleShape)
.hazeEffectTangem { blurRadius = 8.dp }
.clickableSingle(
onClick = state.onShareClick,
enabled = bottomSheetState.value == BottomSheetState.EXPANDED &&

View file

@ -10,8 +10,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
@ -32,7 +34,6 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.news.model.NewsListConfig
import com.tangem.features.feed.model.news.list.NewsListModel
import com.tangem.features.feed.ui.news.list.NewsListContent
import dev.chrisbanes.haze.HazeProgressive
import kotlinx.serialization.Serializable
internal class DefaultNewsListComponent(
@ -48,14 +49,7 @@ internal class DefaultNewsListComponent(
val state by newsListModel.state.collectAsStateWithLifecycle()
if (LocalRedesignEnabled.current) {
TangemTopBar(
modifier = Modifier.hazeEffectTangem {
progressive = HazeProgressive.verticalGradient(
startIntensity = .55f,
endIntensity = .2f,
preferPerformance = true,
)
backgroundColor = background
},
modifier = Modifier.background(background.copy(alpha = .95f)),
title = resourceReference(R.string.common_news),
type = TangemTopBarType.BottomSheet,
startContent = {
@ -65,10 +59,8 @@ internal class DefaultNewsListComponent(
tint = TangemTheme.colors2.graphic.neutral.primary,
modifier = Modifier
.size(TangemTheme.dimens2.x11)
.background(
color = TangemTheme.colors2.button.backgroundSecondary,
shape = CircleShape,
)
.clip(CircleShape)
.hazeEffectTangem { blurRadius = 8.dp }
.clickableSingle(
onClick = state.onBackClick,
enabled = bottomSheetState.value == BottomSheetState.EXPANDED,

View file

@ -1,6 +1,5 @@
package com.tangem.features.feed.components.search
import androidx.compose.animation.core.EaseOut
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
@ -13,7 +12,6 @@ import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
import com.tangem.core.ui.components.haze.hazeEffectTangem
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
import com.tangem.core.ui.ds.field.search.TangemFieldShape
@ -25,7 +23,6 @@ import com.tangem.domain.markets.TokenMarketParams
import com.tangem.features.feed.model.search.SearchModel
import com.tangem.features.feed.ui.search.SearchContent
import com.tangem.features.feed.ui.search.state.SearchCallbacks
import dev.chrisbanes.haze.HazeProgressive
internal class DefaultSearchComponent(
appComponentContext: AppComponentContext,
@ -53,14 +50,6 @@ internal class DefaultSearchComponent(
}
TangemTopBar(
modifier = Modifier.hazeEffectTangem {
progressive = HazeProgressive.verticalGradient(
startIntensity = .55f,
endIntensity = 0f,
preferPerformance = true,
easing = EaseOut,
)
},
type = TangemTopBarType.BottomSheet,
reserveSlotSpace = false,
content = {

View file

@ -18,6 +18,7 @@ import com.arkivanov.decompose.extensions.compose.stack.Children
import com.arkivanov.decompose.router.stack.ChildStack
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
import com.tangem.core.ui.components.haze.hazeSourceTangem
import com.tangem.core.ui.components.topFade
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
import com.tangem.core.ui.extensions.conditionalCompose
import com.tangem.core.ui.res.LocalHazeState
@ -26,10 +27,19 @@ import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.WindowInsetsZero
import com.tangem.features.feed.components.FeedEntryChildFactory
import com.tangem.features.feed.ui.utils.FadeConstants.BASE_FADE_LEVEL
import com.tangem.features.feed.ui.utils.contentFeedEntryStackAnimation
import com.tangem.features.feed.ui.utils.topBarFeedEntryAnimatedContentTransitionSpec
import dev.chrisbanes.haze.rememberHazeState
/**
* When the value is `null` (default), the fade covers `topBarHeight`. A screen with sticky chrome
* (e.g. category chips, sort options) can set this to `0.dp` to disable the centralized fade and
* apply its own fade on its inner haze source covering the full sticky header (topbar + chrome).
*/
internal val LocalContentTopFadeHeightOverride = compositionLocalOf<MutableState<Dp?>?> { null }
@OptIn(ExperimentalDecomposeApi::class)
@Composable
internal fun EntryContent(
@ -128,75 +138,131 @@ private fun EntryContentV2(
onExpandSheet: () -> Unit,
isOpenedInBottomSheet: Boolean,
) {
val density = LocalDensity.current
val background = LocalMainBottomSheetColor.current.value
val animationContent = remember { contentFeedEntryStackAnimation() }
val animationAppBar = remember(stackState) { topBarFeedEntryAnimatedContentTransitionSpec(stackState) }
var topBarHeight by remember { mutableStateOf(0.dp) }
val hazeState = rememberHazeState()
val fadeHeightOverride = remember { mutableStateOf<Dp?>(null) }
val effectiveFadeHeight = fadeHeightOverride.value ?: topBarHeight
Surface(contentColor = background) {
CompositionLocalProvider(LocalHazeState provides hazeState) {
CompositionLocalProvider(
LocalHazeState provides hazeState,
LocalContentTopFadeHeightOverride provides fadeHeightOverride,
) {
Box(modifier = Modifier.fillMaxSize()) {
Children(
modifier = Modifier.fillMaxSize(),
stack = stackState.value,
animation = animationContent,
) { child ->
child.instance.Content(
modifier = Modifier
.fillMaxSize()
.conditionalCompose(
condition = !isOpenedInBottomSheet,
modifier = {
padding(top = topBarHeight)
},
)
.hazeSourceTangem(zIndex = 0f, state = hazeState),
contentPadding = PaddingValues(
top = if (isOpenedInBottomSheet) topBarHeight else TangemTheme.dimens2.x2_5,
),
bottomSheetState = bottomSheetState,
)
}
Box(
modifier = Modifier
.align(Alignment.TopStart)
.then(
if (!isOpenedInBottomSheet) {
Modifier.statusBarsPadding()
} else {
Modifier
},
)
.onGloballyPositioned { coordinates ->
if (coordinates.size.height > 0) {
with(density) {
val height = coordinates.size.height.toDp()
topBarHeight = height
onHeaderSizeChange(height)
}
}
},
) {
AnimatedContent(
targetState = stackState.value.active,
transitionSpec = animationAppBar,
contentKey = { it.key },
label = "FeedEntryAppBar",
) { state ->
state.instance.Title(bottomSheetState)
}
CollapsedTitleClickOverlay(
bottomSheetState = bottomSheetState,
onExpandSheet = onExpandSheet,
)
}
ContentBlock(
bottomSheetState = bottomSheetState,
effectiveFadeHeight = effectiveFadeHeight,
isOpenedInBottomSheet = isOpenedInBottomSheet,
stackState = stackState,
topBarHeight = topBarHeight,
)
TitleBlock(
bottomSheetState = bottomSheetState,
stackState = stackState,
onTopBarHeightChang = { dp ->
onHeaderSizeChange(dp)
topBarHeight = dp
},
isOpenedInBottomSheet = isOpenedInBottomSheet,
onExpandSheet = onExpandSheet,
)
}
}
}
}
@Composable
private fun BoxScope.TitleBlock(
bottomSheetState: State<BottomSheetState>,
stackState: State<ChildStack<FeedEntryChildFactory.Child, ComposableModularBottomSheetContentComponent>>,
onTopBarHeightChang: (Dp) -> Unit,
isOpenedInBottomSheet: Boolean,
onExpandSheet: () -> Unit,
modifier: Modifier = Modifier,
) {
val density = LocalDensity.current
val animationAppBar = remember(stackState) { topBarFeedEntryAnimatedContentTransitionSpec(stackState) }
Box(
modifier = modifier
.align(Alignment.TopStart)
.then(
if (!isOpenedInBottomSheet) {
Modifier.statusBarsPadding()
} else {
Modifier
},
)
.onGloballyPositioned { coordinates ->
if (coordinates.size.height > 0) {
with(density) {
val height = coordinates.size.height.toDp()
onTopBarHeightChang(height)
}
}
},
) {
AnimatedContent(
targetState = stackState.value.active,
transitionSpec = animationAppBar,
contentKey = { it.key },
label = "FeedEntryAppBar",
) { state ->
state.instance.Title(bottomSheetState)
}
CollapsedTitleClickOverlay(
bottomSheetState = bottomSheetState,
onExpandSheet = onExpandSheet,
)
}
}
@Composable
private fun BoxScope.ContentBlock(
bottomSheetState: State<BottomSheetState>,
effectiveFadeHeight: Dp,
isOpenedInBottomSheet: Boolean,
stackState: State<ChildStack<FeedEntryChildFactory.Child, ComposableModularBottomSheetContentComponent>>,
topBarHeight: Dp,
modifier: Modifier = Modifier,
) {
val animationContent = remember { contentFeedEntryStackAnimation() }
Children(
modifier = modifier.fillMaxSize(),
stack = stackState.value,
animation = animationContent,
) { child ->
child.instance.Content(
modifier = Modifier
.fillMaxSize()
.conditionalCompose(
condition = !isOpenedInBottomSheet,
modifier = {
padding(top = topBarHeight)
},
)
.hazeSourceTangem(zIndex = 0f, state = LocalHazeState.current)
.conditionalCompose(
condition = isOpenedInBottomSheet,
modifier = {
topFade(
height = effectiveFadeHeight,
color = TangemTheme.colors2.surface.level2.copy(BASE_FADE_LEVEL),
solidStop = .6f,
)
},
),
contentPadding = PaddingValues(
top = if (isOpenedInBottomSheet) topBarHeight else TangemTheme.dimens2.x2_5,
),
bottomSheetState = bottomSheetState,
)
}
}
@Composable
private fun BoxScope.CollapsedTitleClickOverlay(bottomSheetState: State<BottomSheetState>, onExpandSheet: () -> Unit) {
if (bottomSheetState.value == BottomSheetState.COLLAPSED) {

View file

@ -16,6 +16,7 @@ import androidx.compose.ui.text.style.TextOverflow
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.topbar.TangemTopBar
import com.tangem.core.ui.ds.topbar.TangemTopBarType
import com.tangem.core.ui.extensions.conditional
@ -107,7 +108,9 @@ private fun FeedSearchBarV2(
end = if (endContent != null) TangemTheme.dimens2.x3 else 0.dp,
)
.clip(CircleShape)
.background(color = TangemTheme.colors2.button.backgroundSecondary)
.hazeEffectTangem {
blurRadius = 8.dp
}
.conditional(condition = isSearchBarClickable) {
clickable(onClick = feedListSearchBar.onBarClick)
}

View file

@ -1,7 +1,6 @@
package com.tangem.features.feed.ui.feed
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.core.EaseOut
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
@ -16,9 +15,7 @@ import androidx.compose.ui.platform.testTag
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.haze.hazeEffectTangem
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.extensions.conditionalCompose
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemThemePreview
@ -31,7 +28,6 @@ import com.tangem.features.feed.ui.feed.preview.FeedListPreviewDataProvider.crea
import com.tangem.features.feed.ui.feed.state.FeedListSearchBar
import com.tangem.features.feed.ui.feed.state.FeedListUM
import com.tangem.features.feed.ui.feed.state.GlobalFeedState
import dev.chrisbanes.haze.HazeProgressive
@Composable
internal fun FeedListHeader(
@ -39,26 +35,10 @@ internal fun FeedListHeader(
feedListSearchBar: FeedListSearchBar,
modifier: Modifier = Modifier,
) {
val background = LocalMainBottomSheetColor.current.value
FeedSearchBar(
isSearchBarClickable = isSearchBarClickable,
feedListSearchBar = feedListSearchBar,
modifier = modifier
.drawBehind { drawRect(background) }
.conditionalCompose(
condition = LocalRedesignEnabled.current,
modifier = {
hazeEffectTangem {
progressive = HazeProgressive.verticalGradient(
startIntensity = .75f,
endIntensity = 0f,
preferPerformance = true,
easing = EaseOut,
)
}
},
)
.testTag(SEARCH_BAR),
modifier = modifier.testTag(SEARCH_BAR),
)
}

View file

@ -13,6 +13,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager
@ -32,18 +33,18 @@ import com.tangem.core.ui.components.haze.hazeSourceTangem
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.LocalHazeState
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.*
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.features.feed.impl.R
import com.tangem.features.feed.model.market.list.state.*
import com.tangem.features.feed.ui.LocalContentTopFadeHeightOverride
import com.tangem.features.feed.ui.feed.state.FeedListSearchBar
import com.tangem.features.feed.ui.market.list.components.MarketsListLazyColumn
import com.tangem.features.feed.ui.market.list.components.MarketsListSortByBottomSheet
import com.tangem.features.feed.ui.market.list.components.Options
import com.tangem.features.feed.ui.utils.FadeConstants.BASE_FADE_LEVEL
import com.tangem.features.feed.ui.utils.FadeConstants.FIRST_STEP
import com.tangem.features.feed.ui.utils.FadeConstants.FIRST_STEP_FADE_LEVEL
import dev.chrisbanes.haze.rememberHazeState
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.delay
@ -205,6 +206,14 @@ private fun ColumnScope.ContentV2(contentPadding: PaddingValues, state: MarketsL
val scrolledState = remember { mutableStateOf(false) }
var optionsHeight by remember { mutableStateOf(0.dp) }
val density = LocalDensity.current
val fadeColor = TangemTheme.colors2.surface.level2.copy(BASE_FADE_LEVEL)
val topPadding = contentPadding.calculateTopPadding()
val centralFadeOverride = LocalContentTopFadeHeightOverride.current
DisposableEffect(centralFadeOverride) {
centralFadeOverride?.value = 0.dp
onDispose { centralFadeOverride?.value = null }
}
Box(modifier = Modifier.fillMaxSize()) {
ItemsList(
@ -216,10 +225,19 @@ private fun ColumnScope.ContentV2(contentPadding: PaddingValues, state: MarketsL
isInSearchMode = state.isInSearchMode,
state = state.list,
)
TopFade(
modifier = Modifier.padding(top = contentPadding.calculateTopPadding()),
colorStops = arrayOf(
0f to fadeColor,
FIRST_STEP to fadeColor.copy(FIRST_STEP_FADE_LEVEL),
1f to Color.Transparent,
),
height = 20.dp + optionsHeight,
)
Options(
modifier = Modifier
.align(Alignment.TopStart)
.padding(bottom = TangemTheme.dimens2.x4, top = contentPadding.calculateTopPadding())
.padding(bottom = TangemTheme.dimens2.x4, top = topPadding)
.padding(horizontal = TangemTheme.dimens2.x4)
.onGloballyPositioned { coordinates ->
if (coordinates.size.height > 0) {

View file

@ -1,6 +1,5 @@
package com.tangem.features.feed.ui.market.list.components
import androidx.compose.animation.core.EaseOut
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
import androidx.compose.runtime.*
@ -20,14 +19,12 @@ import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds.tabs.TangemSegmentUM
import com.tangem.core.ui.ds.tabs.TangemSegmentedPicker
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.feed.impl.R
import com.tangem.features.feed.model.market.list.state.MarketsListUM
import com.tangem.features.feed.model.market.list.state.SortByMenuUM
import com.tangem.features.feed.model.market.list.state.SortByTypeUM
import dev.chrisbanes.haze.HazeProgressive
import kotlinx.collections.immutable.persistentListOf
import com.tangem.core.ui.ds.button.TangemButtonIconPosition as RedesignTangemButtonIconPosition
@ -120,7 +117,6 @@ private fun OptionsV2(
modifier: Modifier = Modifier,
) {
var isShowDropdownMenu by rememberSaveable { mutableStateOf(false) }
val background = LocalMainBottomSheetColor.current.value
val segmentItems = remember {
persistentListOf(
@ -147,16 +143,7 @@ private fun OptionsV2(
Row(
modifier = Modifier
.fillMaxWidth()
.height(IntrinsicSize.Max)
.hazeEffectTangem {
progressive = HazeProgressive.verticalGradient(
startIntensity = .2f,
endIntensity = 0f,
easing = EaseOut,
preferPerformance = true,
)
backgroundColor = background
},
.height(IntrinsicSize.Max),
horizontalArrangement = Arrangement.SpaceBetween,
) {
PrimaryInverseTangemButton(

View file

@ -1,6 +1,5 @@
package com.tangem.features.feed.ui.news.list
import androidx.compose.animation.core.EaseOut
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyListState
@ -10,25 +9,32 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.TopFade
import com.tangem.core.ui.components.chip.Chip
import com.tangem.core.ui.components.chip.entity.ChipUM
import com.tangem.core.ui.components.haze.hazeEffectTangem
import com.tangem.core.ui.components.haze.hazeSourceTangem
import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.ds.tabs.TangemTab
import com.tangem.core.ui.event.EventEffect
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.res.*
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.feed.ui.LocalContentTopFadeHeightOverride
import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM
import com.tangem.features.feed.ui.news.list.components.NewsListLazyColumn
import com.tangem.features.feed.ui.news.list.state.NewsListState
import com.tangem.features.feed.ui.news.list.state.NewsListUM
import dev.chrisbanes.haze.HazeProgressive
import com.tangem.features.feed.ui.utils.FadeConstants.BASE_FADE_LEVEL
import com.tangem.features.feed.ui.utils.FadeConstants.FIRST_STEP
import com.tangem.features.feed.ui.utils.FadeConstants.FIRST_STEP_FADE_LEVEL
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableSet
@ -93,6 +99,14 @@ internal fun NewsListContentV2(contentPadding: PaddingValues, state: NewsListUM)
val chipsListState = rememberLazyListState()
var chipsHeight by remember { mutableStateOf(0.dp) }
val density = LocalDensity.current
val fadeColor = TangemTheme.colors2.surface.level2.copy(BASE_FADE_LEVEL)
val topPadding = contentPadding.calculateTopPadding()
val centralFadeOverride = LocalContentTopFadeHeightOverride.current
DisposableEffect(centralFadeOverride) {
centralFadeOverride?.value = 0.dp
onDispose { centralFadeOverride?.value = null }
}
ScrollChipsToSelected(state = state, chipsListState = chipsListState)
@ -104,33 +118,35 @@ internal fun NewsListContentV2(contentPadding: PaddingValues, state: NewsListUM)
NewsListLazyColumn(
topContentPadding = contentPadding.calculateTopPadding() + 16.dp + chipsHeight,
modifier = Modifier
.hazeSourceTangem(zIndex = 0f)
.align(Alignment.TopStart),
.align(Alignment.TopStart)
.hazeSourceTangem(zIndex = 0f),
newsListState = state.newsListState,
listOfArticles = state.listOfArticles,
lazyListState = lazyListState,
onArticleClick = state.onArticleClick,
)
TopFade(
modifier = Modifier.padding(top = contentPadding.calculateTopPadding()),
colorStops = arrayOf(
0f to fadeColor,
FIRST_STEP to fadeColor.copy(FIRST_STEP_FADE_LEVEL),
1f to Color.Transparent,
),
height = 20.dp + chipsHeight,
)
LazyRow(
state = chipsListState,
modifier = Modifier
.align(Alignment.TopStart)
.padding(top = contentPadding.calculateTopPadding(), bottom = TangemTheme.dimens2.x4)
.padding(top = topPadding, bottom = TangemTheme.dimens2.x4)
.onGloballyPositioned { coordinates ->
if (coordinates.size.height > 0) {
with(density) {
chipsHeight = coordinates.size.height.toDp()
}
}
}
.hazeEffectTangem {
progressive = HazeProgressive.verticalGradient(
startIntensity = .2f,
endIntensity = 0f,
easing = EaseOut,
preferPerformance = true,
)
backgroundColor = background
},
contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),

View file

@ -0,0 +1,7 @@
package com.tangem.features.feed.ui.utils
internal object FadeConstants {
const val BASE_FADE_LEVEL = .95f
const val FIRST_STEP = .8f
const val FIRST_STEP_FADE_LEVEL = .7f
}