Updated on 2026-08-14

This commit is contained in:
Tangem 2025-03-26 13:10:44 +05:00
parent 9957e986e1
commit a774347c70
29 changed files with 887 additions and 86 deletions

View file

@ -0,0 +1,47 @@
package com.tangem.core.ui.components.dropdownmenu
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@Suppress("ComposableEventParameterNaming")
@Composable
fun TangemDropdownItem(item: TangemDropdownMenuItem, dismissParent: () -> Unit, modifier: Modifier = Modifier) {
Text(
modifier = modifier
.clickable {
dismissParent()
item.onClick()
}
.padding(vertical = TangemTheme.dimens.spacing8, horizontal = TangemTheme.dimens.spacing16),
text = item.title.resolveReference(),
style = TangemTheme.typography.button.copy(color = item.textColorProvider()),
)
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_TokenDetailsAppBarDropdownItem() {
TangemThemePreview {
TangemDropdownItem(
modifier = Modifier.background(TangemTheme.colors.background.primary),
dismissParent = {},
item = TangemDropdownMenuItem(
title = TextReference.Res(id = R.string.token_details_hide_token),
textColorProvider = { TangemTheme.colors.text.warning },
onClick = { },
),
)
}
}

View file

@ -0,0 +1,232 @@
@file:Suppress("TopLevelPropertyNaming")
package com.tangem.core.ui.components.dropdownmenu
import androidx.compose.animation.core.*
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.DropdownMenu
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.*
import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupPositionProvider
import androidx.compose.ui.window.PopupProperties
import com.tangem.core.ui.res.TangemTheme
import kotlin.math.max
import kotlin.math.min
/**
* Just copy paste [DropdownMenu] from material3 with deleting vertical paddings.
*/
@Composable
fun TangemDropdownMenu(
expanded: Boolean,
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
offset: DpOffset = DpOffset(x = TangemTheme.dimens.spacing20, y = TangemTheme.dimens.spacing10.times(-1)),
properties: PopupProperties = PopupProperties(focusable = true),
content: @Composable ColumnScope.() -> Unit,
) {
val expandedStates = remember { MutableTransitionState(false) }
expandedStates.targetState = expanded
if (expandedStates.currentState || expandedStates.targetState) {
val transformOriginState = remember { mutableStateOf(TransformOrigin.Center) }
val density = LocalDensity.current
val popupPositionProvider = DropdownMenuPositionProvider(
offset,
density,
) { parentBounds, menuBounds ->
transformOriginState.value = calculateTransformOrigin(parentBounds, menuBounds)
}
Popup(
onDismissRequest = onDismissRequest,
popupPositionProvider = popupPositionProvider,
properties = properties,
) {
DropdownMenuContent(
expandedStates = expandedStates,
transformOriginState = transformOriginState,
modifier = modifier,
content = content,
)
}
}
}
private const val InTransitionDuration = 120
private const val OutTransitionDuration = 75
@Suppress("ReusedModifierInstance", "MagicNumber")
@Composable
private fun DropdownMenuContent(
expandedStates: MutableTransitionState<Boolean>,
transformOriginState: MutableState<TransformOrigin>,
modifier: Modifier = Modifier,
content: @Composable ColumnScope.() -> Unit,
) {
// Menu open/close animation.
val transition = updateTransition(expandedStates, "DropDownMenu")
val scale by transition.animateFloat(
transitionSpec = {
if (false isTransitioningTo true) {
// Dismissed to expanded
tween(
durationMillis = InTransitionDuration,
easing = LinearOutSlowInEasing,
)
} else {
// Expanded to dismissed.
tween(
durationMillis = 1,
delayMillis = OutTransitionDuration - 1,
)
}
},
label = "",
) {
if (it) {
// Menu is expanded.
1f
} else {
// Menu is dismissed.
0.8f
}
}
val alpha by transition.animateFloat(
transitionSpec = {
if (false isTransitioningTo true) {
// Dismissed to expanded
tween(durationMillis = 30)
} else {
// Expanded to dismissed.
tween(durationMillis = OutTransitionDuration)
}
},
label = "",
) {
if (it) {
// Menu is expanded.
1f
} else {
// Menu is dismissed.
0f
}
}
Card(
modifier = Modifier.graphicsLayer {
scaleX = scale
scaleY = scale
this.alpha = alpha
transformOrigin = transformOriginState.value
},
elevation = CardDefaults.cardElevation(),
) {
Column(
modifier = modifier
.width(IntrinsicSize.Max)
.verticalScroll(rememberScrollState()),
content = content,
)
}
}
private fun calculateTransformOrigin(parentBounds: IntRect, menuBounds: IntRect): TransformOrigin {
val pivotX = when {
menuBounds.left >= parentBounds.right -> 0f
menuBounds.right <= parentBounds.left -> 1f
menuBounds.width == 0 -> 0f
else -> {
val intersectionCenter =
(max(parentBounds.left, menuBounds.left) + min(parentBounds.right, menuBounds.right)) / 2
(intersectionCenter - menuBounds.left).toFloat() / menuBounds.width
}
}
val pivotY = when {
menuBounds.top >= parentBounds.bottom -> 0f
menuBounds.bottom <= parentBounds.top -> 1f
menuBounds.height == 0 -> 0f
else -> {
val intersectionCenter =
(max(parentBounds.top, menuBounds.top) + min(parentBounds.bottom, menuBounds.bottom)) / 2
(intersectionCenter - menuBounds.top).toFloat() / menuBounds.height
}
}
return TransformOrigin(pivotX, pivotY)
}
private val MenuVerticalMargin = 48.dp
@Immutable
internal data class DropdownMenuPositionProvider(
val contentOffset: DpOffset,
val density: Density,
val onPositionCalculated: (IntRect, IntRect) -> Unit = { _, _ -> },
) : PopupPositionProvider {
override fun calculatePosition(
anchorBounds: IntRect,
windowSize: IntSize,
layoutDirection: LayoutDirection,
popupContentSize: IntSize,
): IntOffset {
// The min margin above and below the menu, relative to the screen.
val verticalMargin = with(density) { MenuVerticalMargin.roundToPx() }
// The content offset specified using the dropdown offset parameter.
val contentOffsetX = with(density) { contentOffset.x.roundToPx() }
val contentOffsetY = with(density) { contentOffset.y.roundToPx() }
// Compute horizontal position.
val toRight = anchorBounds.left + contentOffsetX
val toLeft = anchorBounds.right - contentOffsetX - popupContentSize.width
val toDisplayRight = windowSize.width - popupContentSize.width
val toDisplayLeft = 0
val x = if (layoutDirection == LayoutDirection.Ltr) {
sequenceOf(
toRight,
toLeft,
// If the anchor gets outside of the window on the left, we want to position
// toDisplayLeft for proximity to the anchor. Otherwise, toDisplayRight.
if (anchorBounds.left >= 0) toDisplayRight else toDisplayLeft,
)
} else {
sequenceOf(
toLeft,
toRight,
// If the anchor gets outside of the window on the right, we want to position
// toDisplayRight for proximity to the anchor. Otherwise, toDisplayLeft.
if (anchorBounds.right <= windowSize.width) toDisplayLeft else toDisplayRight,
)
}.firstOrNull {
it >= 0 && it + popupContentSize.width <= windowSize.width
} ?: toLeft
// Compute vertical position.
val toBottom = maxOf(anchorBounds.bottom + contentOffsetY, verticalMargin)
val toTop = anchorBounds.top - contentOffsetY - popupContentSize.height
val toCenter = anchorBounds.top - popupContentSize.height / 2
val toDisplayBottom = windowSize.height - popupContentSize.height - verticalMargin
val y = sequenceOf(toBottom, toTop, toCenter, toDisplayBottom).firstOrNull {
it >= verticalMargin &&
it + popupContentSize.height <= windowSize.height - verticalMargin
} ?: toTop
onPositionCalculated(
anchorBounds,
IntRect(x, y, x + popupContentSize.width, y + popupContentSize.height),
)
return IntOffset(x, y)
}
}

View file

@ -0,0 +1,11 @@
package com.tangem.core.ui.components.dropdownmenu
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import com.tangem.core.ui.extensions.TextReference
data class TangemDropdownMenuItem(
val title: TextReference,
val textColorProvider: @Composable () -> Color,
val onClick: () -> Unit,
)

View file

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="76dp"
android:height="76dp"
android:viewportWidth="76"
android:viewportHeight="76">
<path
android:fillColor="#3B99FC"
android:pathData="M16,0L60,0A16,16 0,0 1,76 16L76,60A16,16 0,0 1,60 76L16,76A16,16 0,0 1,0 60L0,16A16,16 0,0 1,16 0z" />
<group>
<clip-path android:pathData="M11.084,22.167h53.833v31.667h-53.833z" />
<path
android:fillColor="#ffffff"
android:pathData="M22.112,28.351C30.889,20.101 45.126,20.101 53.903,28.351L54.959,29.337C55.403,29.751 55.403,30.417 54.959,30.83L51.348,34.229C51.126,34.429 50.778,34.429 50.556,34.229L49.098,32.856C42.973,27.098 33.042,27.098 26.917,32.856L25.362,34.322C25.139,34.522 24.792,34.522 24.57,34.322L20.945,30.937C20.501,30.524 20.501,29.857 20.945,29.444L22.112,28.351ZM61.376,35.375L64.598,38.4C65.042,38.813 65.042,39.48 64.598,39.893L50.084,53.527C49.639,53.94 48.931,53.94 48.501,53.527L38.209,43.851C38.098,43.745 37.917,43.745 37.806,43.851L27.514,53.527C27.07,53.94 26.362,53.94 25.931,53.527L11.417,39.893C10.973,39.48 10.973,38.813 11.417,38.4L14.639,35.375C15.084,34.962 15.792,34.962 16.223,35.375L26.514,45.051C26.626,45.158 26.806,45.158 26.917,45.051L37.209,35.375C37.653,34.962 38.362,34.962 38.792,35.375L49.084,45.051C49.195,45.158 49.376,45.158 49.487,45.051L59.778,35.375C60.223,34.962 60.931,34.962 61.376,35.375Z" />
</group>
</vector>