Updated on 2026-08-14
This commit is contained in:
parent
9957e986e1
commit
a774347c70
29 changed files with 887 additions and 86 deletions
|
|
@ -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 = { },
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
15
core/ui/src/main/res/drawable/img_wallet_connect_76.xml
Normal file
15
core/ui/src/main/res/drawable/img_wallet_connect_76.xml
Normal 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue