Updated on 2026-08-14
This commit is contained in:
parent
7e6f015e5b
commit
d282e2648f
5 changed files with 140 additions and 40 deletions
|
|
@ -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<Int, Rect>() }
|
||||
|
||||
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] }
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Rect?>(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
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue