From d282e2648f150778217c4560208c748b6618dfd2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 21 Jul 2026 17:19:45 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../presentation/wallet/ui/WalletScreen2.kt | 20 +++++ .../wallet/ui/components/MarketsHint.kt | 78 ++++++++++++------- .../wallet/ui/components/MarketsTooltip.kt | 42 ++++++++-- .../wallet/ui/components/WalletItemBlocks.kt | 32 +++++++- .../ui/components/common/WalletContent.kt | 8 +- 5 files changed, 140 insertions(+), 40 deletions(-) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt index 7e40f611a4..0def94ef24 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusState import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.nestedscroll.nestedScroll @@ -266,6 +267,10 @@ private fun WalletContent2( val overlay = TangemTheme.colors2.overlay.overlayPrimary + // Root-coordinates bounds of the "Add & Manage" button per pager page, reported by the + // button itself, so the markets hint and tooltip can avoid covering it + val organizeButtonBounds = remember { mutableStateMapOf() } + HorizontalPager( state = walletsPagerState, userScrollEnabled = canPagerScroll, @@ -355,6 +360,15 @@ private fun WalletContent2( promoBannersBlockComponent = promoBannersBlockComponent, walletId = currentWalletId, virtualAccountComponent = virtualAccountComponent, + onOrganizeButtonBoundsChange = remember(currentWalletIndex) { + { bounds -> + if (bounds != null) { + organizeButtonBounds[currentWalletIndex] = bounds + } else { + organizeButtonBounds.remove(currentWalletIndex) + } + } + }, modifier = Modifier .fillMaxSize() .nestedScroll(behavior.nestedScrollConnection), @@ -371,6 +385,9 @@ private fun WalletContent2( .fillMaxWidth(fraction = .6f) .padding(bottom = peekHeight), isVisible = isShowMarketsHint, + obstacleBounds = remember(currentWalletIndex) { + { organizeButtonBounds[currentWalletIndex] } + }, ) } } @@ -386,6 +403,9 @@ private fun WalletContent2( sheetTopInset = TangemTheme.dimens2.x3, bottomSheetState = bottomSheetState, onCloseClick = state.onDismissMarketsTooltip, + obstacleBounds = remember(walletsPagerState, organizeButtonBounds) { + { organizeButtonBounds[walletsPagerState.currentPage] } + }, ) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/MarketsHint.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/MarketsHint.kt index 9ac6ed900e..399c345f9c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/MarketsHint.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/MarketsHint.kt @@ -1,53 +1,77 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components import android.content.res.Configuration -import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.size import androidx.compose.material3.Icon import androidx.compose.material3.Text -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.layout.boundsInRoot +import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.res.painterResource +import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.tangem.core.ui.extensions.conditional import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreviewRedesign import com.tangem.feature.wallet.impl.R +/** + * Hint above the markets bottom sheet suggesting to pull it up to add tokens. + * + * The hint renders only when [isVisible] and its own on-screen bounds do not intersect + * [obstacleBounds] (e.g. the "Add & Manage" button resting nearby), so it never draws + * over other content. + */ @Composable -internal fun MarketsHint(isVisible: Boolean, modifier: Modifier = Modifier) { - AnimatedVisibility( - modifier = modifier, - visible = isVisible, - enter = fadeIn(animationSpec = tween(durationMillis = 300)), - exit = fadeOut(animationSpec = tween(durationMillis = 300)), - ) { - Column( - verticalArrangement = Arrangement.spacedBy(space = 4.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text( - text = stringResourceSafe(R.string.markets_hint), - style = TangemTheme.typography2.bodyRegular15, - color = TangemTheme.colors2.text.neutral.tertiary, - textAlign = TextAlign.Center, - ) - Icon( - modifier = Modifier.size(size = 24.dp), - painter = painterResource(id = R.drawable.ic_chevron_24), - tint = TangemTheme.colors2.graphic.neutral.tertiary, - contentDescription = null, - ) +internal fun MarketsHint(isVisible: Boolean, modifier: Modifier = Modifier, obstacleBounds: () -> Rect? = { null }) { + var hintBounds by remember { mutableStateOf(null) } + val isObstacleInTheWay by remember(obstacleBounds) { + derivedStateOf { + val hint = hintBounds + val obstacle = obstacleBounds() + hint != null && obstacle != null && hint.overlaps(obstacle) } } + + val isShown = isVisible && !isObstacleInTheWay + val alpha by animateFloatAsState( + targetValue = if (isShown) 1f else 0f, + animationSpec = tween(durationMillis = 300), + label = "marketsHintAlpha", + ) + + Column( + modifier = modifier + .onGloballyPositioned { hintBounds = it.boundsInRoot() } + .graphicsLayer { this.alpha = alpha } + .conditional(!isShown) { clearAndSetSemantics { } }, + verticalArrangement = Arrangement.spacedBy(space = 4.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = stringResourceSafe(R.string.markets_hint), + style = TangemTheme.typography2.bodyRegular15, + color = TangemTheme.colors2.text.neutral.tertiary, + textAlign = TextAlign.Center, + ) + Icon( + modifier = Modifier.size(size = 24.dp), + painter = painterResource(id = R.drawable.ic_chevron_24), + tint = TangemTheme.colors2.graphic.neutral.tertiary, + contentDescription = null, + ) + } } // region Preview diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/MarketsTooltip.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/MarketsTooltip.kt index 0d17924be7..1c9e506c96 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/MarketsTooltip.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/MarketsTooltip.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Outline import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource @@ -40,6 +41,17 @@ import com.tangem.feature.wallet.impl.R import kotlinx.coroutines.delay import kotlin.math.roundToInt +/** Gap between the lifted tooltip and the obstacle below it. */ +private val OBSTACLE_GAP = 4.dp + +/** + * Onboarding tooltip for the markets bottom sheet, anchored right above the sheet peek. + * + * @param obstacleBounds provider of root-coordinates bounds of content the tooltip must not cover — + * e.g. the "Add & Manage" button. When the anchored tooltip would overlap the obstacle, the tooltip + * is lifted to sit just above it. + */ +@Suppress("LongParameterList") @Composable internal fun MarketsTooltip( availableHeight: Dp, @@ -48,17 +60,28 @@ internal fun MarketsTooltip( onCloseClick: () -> Unit, sheetTopInset: Dp, modifier: Modifier = Modifier, + obstacleBounds: () -> Rect? = { null }, ) { val density = LocalDensity.current - val tooltipOffset by remember(availableHeight, sheetTopInset) { + var tooltipHeight by remember { mutableIntStateOf(0) } + val tooltipOffset by remember(availableHeight, sheetTopInset, obstacleBounds) { derivedStateOf { - val bottomSheetOffset = try { - // Can throw exception during the first composition - with(density) { bottomSheetState.requireOffset().toDp() } - } catch (e: Exception) { - 0.dp + // Sheet offset is in root coordinates: the scaffold content fills the screen edge to edge + val sheetTop = runCatching { bottomSheetState.requireOffset() }.getOrElse { 0f } + + with(density) { + val anchoredBottom = sheetTop + sheetTopInset.toPx() + val anchoredTop = anchoredBottom - tooltipHeight + val obstacle = obstacleBounds()?.takeIf { tooltipHeight > 0 } + val isObstacleInTheWay = obstacle != null && + obstacle.top < anchoredBottom && obstacle.bottom > anchoredTop + val bottom = if (isObstacleInTheWay) { + obstacle.top - OBSTACLE_GAP.toPx() + } else { + anchoredBottom + } + (bottom - availableHeight.toPx()).toDp() } - bottomSheetOffset + sheetTopInset - availableHeight } } @@ -86,7 +109,10 @@ internal fun MarketsTooltip( ) + fadeIn(), exit = fadeOut(), ) { - MarketsTooltipContent(onCloseClick = onCloseClick) + MarketsTooltipContent( + onCloseClick = onCloseClick, + modifier = Modifier.onSizeChanged { tooltipHeight = it.height }, + ) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletItemBlocks.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletItemBlocks.kt index 31d053f08a..500920fc7e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletItemBlocks.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletItemBlocks.kt @@ -1,7 +1,13 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.layout.boundsInRoot +import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.testTag import com.tangem.core.ui.ds.button.TangemButton import com.tangem.core.ui.extensions.resourceReference @@ -15,6 +21,8 @@ import com.tangem.features.tangempay.entity.TangemPayMainUM import com.tangem.features.virtualaccount.main.component.VirtualAccountMainBlockComponent import com.tangem.features.virtualaccount.main.entity.VirtualAccountMainUM +internal const val ORGANIZE_TOKENS_BUTTON_ITEM_KEY = "OrganizeTokensButton" + internal fun LazyListScope.nftCollections2(state: WalletUM, itemModifier: Modifier) { (state as? WalletUM.Content)?.let { content -> item(key = "NFTCollections", contentType = "NFTCollections") { @@ -26,12 +34,20 @@ internal fun LazyListScope.nftCollections2(state: WalletUM, itemModifier: Modifi } } -internal fun LazyListScope.organizeTokens2(state: WalletUM, itemModifier: Modifier) { +/** + * @param onButtonBoundsChange called with the button root-coordinates bounds on every placement change + * and with `null` when the button leaves composition + */ +internal fun LazyListScope.organizeTokens2( + state: WalletUM, + itemModifier: Modifier, + onButtonBoundsChange: (Rect?) -> Unit, +) { val organizeButton = state.tokensListUM.organizeButtonUM if (organizeButton != null) { item( - key = "OrganizeTokensButton", - contentType = "OrganizeTokensButton", + key = ORGANIZE_TOKENS_BUTTON_ITEM_KEY, + contentType = ORGANIZE_TOKENS_BUTTON_ITEM_KEY, ) { val hapticManager = LocalHapticManager.current val testTag = if (organizeButton.text == resourceReference(R.string.main_add_and_manage_tokens)) { @@ -39,6 +55,12 @@ internal fun LazyListScope.organizeTokens2(state: WalletUM, itemModifier: Modifi } else { MainScreenTestTags.ORGANIZE_TOKENS_BUTTON } + + val currentOnButtonBoundsChange by rememberUpdatedState(onButtonBoundsChange) + DisposableEffect(Unit) { + onDispose { currentOnButtonBoundsChange(null) } + } + TangemButton( buttonUM = organizeButton.copy( onClick = { @@ -46,7 +68,9 @@ internal fun LazyListScope.organizeTokens2(state: WalletUM, itemModifier: Modifi organizeButton.onClick() }, ), - modifier = itemModifier.testTag(testTag), + modifier = itemModifier + .onGloballyPositioned { currentOnButtonBoundsChange(it.boundsInRoot()) } + .testTag(testTag), ) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt index 4489e87a40..0bd93b7cf9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.rememberOverscrollEffect import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Rect import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.dp import androidx.paging.compose.LazyPagingItems @@ -40,6 +41,7 @@ internal fun WalletListContent( tangemPayComponent: TangemPayMainBlockComponent, virtualAccountComponent: VirtualAccountMainBlockComponent, contentPadding: PaddingValues, + onOrganizeButtonBoundsChange: (Rect?) -> Unit, modifier: Modifier = Modifier, promoBannersBlockComponent: PromoBannersBlockComponent? = null, walletId: String? = null, @@ -99,7 +101,11 @@ internal fun WalletListContent( nftCollections2(state = currentWallet, itemModifier = itemModifier) - organizeTokens2(state = currentWallet, itemModifier = itemModifier) + organizeTokens2( + state = currentWallet, + itemModifier = itemModifier, + onButtonBoundsChange = onOrganizeButtonBoundsChange, + ) } }