Updated on 2026-08-14
This commit is contained in:
parent
1ffec50319
commit
95116dc872
16 changed files with 688 additions and 185 deletions
|
|
@ -79,7 +79,7 @@ private fun DropdownMenuContent(
|
|||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
// Menu open/close animation.
|
||||
val transition = updateTransition(expandedStates, "DropDownMenu")
|
||||
val transition = rememberTransition(expandedStates, "DropDownMenu")
|
||||
|
||||
val scale by transition.animateFloat(
|
||||
transitionSpec = {
|
||||
|
|
|
|||
|
|
@ -48,9 +48,9 @@ fun TangemCheckbox(
|
|||
isEnabled: Boolean = true,
|
||||
) {
|
||||
val shape = if (isRounded) {
|
||||
RoundedCornerShape(TangemTheme.dimens2.x1)
|
||||
} else {
|
||||
CircleShape
|
||||
} else {
|
||||
RoundedCornerShape(TangemTheme.dimens2.x1)
|
||||
}
|
||||
Box(
|
||||
modifier = modifier
|
||||
|
|
|
|||
|
|
@ -0,0 +1,288 @@
|
|||
package com.tangem.core.ui.ds.contextmenu
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.TransformOrigin
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
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.components.haze.hazeEffectTangem
|
||||
import com.tangem.core.ui.components.haze.hazeSourceTangem
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
import com.tangem.core.ui.test.PopUpMenuTestTags
|
||||
import dev.chrisbanes.haze.rememberHazeState
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* Just copy paste [DropdownMenu] from material3 with deleting vertical paddings.
|
||||
*/
|
||||
@Composable
|
||||
fun TangemContextMenu(
|
||||
expanded: Boolean,
|
||||
onDismissRequest: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
offset: DpOffset = DpOffset.Zero,
|
||||
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 IN_TRANSITION_DURATION = 120
|
||||
private const val OUT_TRANSITION_DURATION = 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 = rememberTransition(expandedStates, "DropDownMenu")
|
||||
|
||||
val scale by transition.animateFloat(
|
||||
transitionSpec = {
|
||||
if (false isTransitioningTo true) {
|
||||
// Dismissed to expanded
|
||||
tween(
|
||||
durationMillis = IN_TRANSITION_DURATION,
|
||||
easing = LinearOutSlowInEasing,
|
||||
)
|
||||
} else {
|
||||
// Expanded to dismissed.
|
||||
tween(
|
||||
durationMillis = 1,
|
||||
delayMillis = OUT_TRANSITION_DURATION - 1,
|
||||
)
|
||||
}
|
||||
},
|
||||
label = "",
|
||||
) { isExpanded ->
|
||||
if (isExpanded) {
|
||||
// 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 = OUT_TRANSITION_DURATION)
|
||||
}
|
||||
},
|
||||
label = "",
|
||||
) { isExpanded ->
|
||||
if (isExpanded) {
|
||||
// Menu is expanded.
|
||||
1f
|
||||
} else {
|
||||
// Menu is dismissed.
|
||||
0f
|
||||
}
|
||||
}
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(TangemTheme.dimens2.x5))
|
||||
.graphicsLayer {
|
||||
scaleX = scale
|
||||
scaleY = scale
|
||||
this.alpha = alpha
|
||||
transformOrigin = transformOriginState.value
|
||||
},
|
||||
elevation = CardDefaults.cardElevation(),
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.width(IntrinsicSize.Max)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.clip(RoundedCornerShape(TangemTheme.dimens2.x5))
|
||||
.background(TangemTheme.colors2.contextMenu.background)
|
||||
.testTag(PopUpMenuTestTags.CONTAINER),
|
||||
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 { element ->
|
||||
element >= verticalMargin &&
|
||||
element + popupContentSize.height <= windowSize.height - verticalMargin
|
||||
} ?: toTop
|
||||
|
||||
onPositionCalculated(
|
||||
anchorBounds,
|
||||
IntRect(
|
||||
left = x,
|
||||
top = y,
|
||||
right = x + popupContentSize.width,
|
||||
bottom = y + popupContentSize.height,
|
||||
),
|
||||
)
|
||||
return IntOffset(x, y)
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Composable
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun TangemContextMenu_Preview() {
|
||||
TangemThemePreviewRedesign {
|
||||
val hazeState = rememberHazeState()
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(TangemTheme.colors2.surface.level1)
|
||||
.hazeSourceTangem(state = hazeState, zIndex = -1f),
|
||||
) {
|
||||
TangemContextMenu(
|
||||
expanded = true,
|
||||
onDismissRequest = { },
|
||||
modifier = Modifier.hazeEffectTangem(state = hazeState),
|
||||
) {
|
||||
TangemContextMenuCheckboxItem(
|
||||
title = stringReference("Sort by balance"),
|
||||
isChecked = true,
|
||||
onClick = {},
|
||||
)
|
||||
HorizontalDivider(
|
||||
thickness = 0.5.dp,
|
||||
color = TangemTheme.colors2.border.neutral.quaternary,
|
||||
)
|
||||
TangemContextMenuCheckboxItem(
|
||||
title = stringReference("Group tokens"),
|
||||
isChecked = false,
|
||||
onClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package com.tangem.core.ui.ds.contextmenu
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.ds.checkbox.TangemCheckbox
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.clickableSingle
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
/**
|
||||
* Item with checkbox for [TangemContextMenu].
|
||||
*/
|
||||
@Composable
|
||||
fun TangemContextMenuCheckboxItem(title: TextReference, isChecked: Boolean, onClick: () -> Unit) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.width(238.dp)
|
||||
.clickableSingle(onClick = onClick)
|
||||
.padding(
|
||||
vertical = TangemTheme.dimens2.x5,
|
||||
horizontal = TangemTheme.dimens2.x4,
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
text = title.resolveReference(),
|
||||
style = TangemTheme.typography2.headingSemibold17,
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
)
|
||||
TangemCheckbox(
|
||||
modifier = Modifier,
|
||||
isRounded = true,
|
||||
isChecked = isChecked,
|
||||
onCheckedChange = { /* no-op */ },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -5,19 +5,23 @@ import androidx.annotation.DrawableRes
|
|||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.lerp
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
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 com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.extensions.*
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
|
@ -30,11 +34,9 @@ import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
|||
* @param title The title text to be displayed in the center of the top bar.
|
||||
* @param modifier Modifier to be applied to the top bar.
|
||||
* @param subtitle Optional subtitle text to be displayed below the title.
|
||||
* @param startIconRes Optional drawable resource ID for the start icon.
|
||||
* @param onStartContentClick Optional click action for the start icon.
|
||||
* @param endIconRes Optional drawable resource ID for the end icon.
|
||||
* @param onEndContentClick Optional click action for the end icon.
|
||||
* @param isGhostButtons Flag to determine if ghost button styling should be applied.
|
||||
* @param startActionUM Optional action data for the start action icon.
|
||||
* @param endActionUM Optional action data for the end action icon.
|
||||
* @param titleIconRes Optional drawable resource ID for the icon to be displayed next to the title.
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
|
|
@ -43,56 +45,95 @@ fun TangemTopBar(
|
|||
modifier: Modifier = Modifier,
|
||||
title: TextReference? = null,
|
||||
subtitle: TextReference? = null,
|
||||
@DrawableRes startIconRes: Int? = null,
|
||||
onStartContentClick: (() -> Unit)? = null,
|
||||
@DrawableRes endIconRes: Int? = null,
|
||||
onEndContentClick: (() -> Unit)? = null,
|
||||
startActionUM: TangemTopBarActionUM? = null,
|
||||
endActionUM: TangemTopBarActionUM? = null,
|
||||
@DrawableRes titleIconRes: Int? = null,
|
||||
titleStyle: TextStyle = TangemTheme.typography2.headingSemibold17,
|
||||
isGhostButtons: Boolean = false,
|
||||
) {
|
||||
TangemTopBarInner(
|
||||
TangemTopBar(
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
titleIconRes = titleIconRes,
|
||||
modifier = modifier,
|
||||
content = {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x0_5),
|
||||
) {
|
||||
TangemTopBarTitle(title = title, titleIconRes = titleIconRes, titleStyle = titleStyle)
|
||||
AnimatedVisibility(
|
||||
visible = subtitle != null,
|
||||
label = "Subtitle Visibility",
|
||||
) {
|
||||
val wrappedSubtitle = remember(this) { requireNotNull(subtitle) }
|
||||
Text(
|
||||
text = wrappedSubtitle.resolveAnnotatedReference(),
|
||||
color = TangemTheme.colors2.text.neutral.secondary,
|
||||
style = TangemTheme.typography2.bodyRegular15,
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
startContent = if (startIconRes != null) {
|
||||
{ TangemTopBarIcon(iconRes = startIconRes) }
|
||||
startContent = if (startActionUM != null) {
|
||||
{ TangemTopBarActionContent(startActionUM) }
|
||||
} else {
|
||||
null
|
||||
},
|
||||
onStartContentClick = onStartContentClick,
|
||||
endContent = if (endIconRes != null) {
|
||||
{ TangemTopBarIcon(iconRes = endIconRes) }
|
||||
endContent = if (endActionUM != null) {
|
||||
{ TangemTopBarActionContent(endActionUM) }
|
||||
} else {
|
||||
null
|
||||
},
|
||||
onEndContentClick = onEndContentClick,
|
||||
isGhostButtons = isGhostButtons,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A top bar composable that displays a title and optional start and end icons.
|
||||
* [Figma](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=8435-74860&m=dev)
|
||||
*
|
||||
* @param title The title text to be displayed in the center of the top bar.
|
||||
* @param modifier Modifier to be applied to the top bar.
|
||||
* @param subtitle Optional subtitle text to be displayed below the title.
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
private fun TangemTopBarTitle(title: TextReference?, @DrawableRes titleIconRes: Int?, titleStyle: TextStyle) {
|
||||
fun TangemTopBar(
|
||||
modifier: Modifier = Modifier,
|
||||
title: TextReference? = null,
|
||||
subtitle: TextReference? = null,
|
||||
@DrawableRes titleIconRes: Int? = null,
|
||||
startContent: @Composable (() -> Unit)? = null,
|
||||
endContent: @Composable (() -> Unit)? = null,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(TangemTheme.dimens2.x16)
|
||||
.padding(TangemTheme.dimens2.x4, TangemTheme.dimens2.x3),
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = startContent != null,
|
||||
modifier = Modifier.align(Alignment.CenterStart),
|
||||
label = "Start Content Visibility",
|
||||
) {
|
||||
startContent?.invoke()
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x0_5),
|
||||
) {
|
||||
TangemTopBarTitle(title = title, titleIconRes = titleIconRes)
|
||||
AnimatedVisibility(
|
||||
visible = subtitle != null,
|
||||
label = "Subtitle Visibility",
|
||||
) {
|
||||
val wrappedSubtitle = remember(this) { requireNotNull(subtitle) }
|
||||
Text(
|
||||
text = wrappedSubtitle.resolveAnnotatedReference(),
|
||||
color = TangemTheme.colors2.text.neutral.secondary,
|
||||
style = TangemTheme.typography2.bodyRegular15,
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = endContent != null,
|
||||
modifier = Modifier.align(Alignment.CenterEnd),
|
||||
label = "End Content Visibility",
|
||||
) {
|
||||
endContent?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TangemTopBarTitle(title: TextReference?, @DrawableRes titleIconRes: Int?) {
|
||||
AnimatedVisibility(
|
||||
visible = title != null,
|
||||
label = "Title Visibility",
|
||||
|
|
@ -127,7 +168,7 @@ private fun TangemTopBarTitle(title: TextReference?, @DrawableRes titleIconRes:
|
|||
Text(
|
||||
text = wrappedTitle.resolveAnnotatedReference(),
|
||||
color = TangemTheme.colors2.text.neutral.primary,
|
||||
style = titleStyle,
|
||||
style = TangemTheme.typography2.headingSemibold17,
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 1,
|
||||
)
|
||||
|
|
@ -136,12 +177,29 @@ private fun TangemTopBarTitle(title: TextReference?, @DrawableRes titleIconRes:
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun TangemTopBarIcon(@DrawableRes iconRes: Int) {
|
||||
fun TangemTopBarActionContent(
|
||||
actionUM: TangemTopBarActionUM,
|
||||
modifier: Modifier = Modifier,
|
||||
iconSize: Dp = TangemTheme.dimens2.x8,
|
||||
) {
|
||||
val background = lerp(
|
||||
start = Color.Transparent,
|
||||
stop = TangemTheme.colors2.button.backgroundSecondary,
|
||||
fraction = actionUM.ghostModeProgress,
|
||||
)
|
||||
val padding = (TangemTheme.dimens2.x10 - iconSize) / 2
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(id = iconRes),
|
||||
imageVector = ImageVector.vectorResource(id = actionUM.iconRes),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors2.graphic.neutral.primary,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
modifier = modifier
|
||||
.size(TangemTheme.dimens2.x10)
|
||||
.clip(CircleShape)
|
||||
.conditional(actionUM.isActionable) { background(background) }
|
||||
.conditionalCompose(actionUM.isActionable && actionUM.onClick != null) {
|
||||
clickableSingle(onClick = requireNotNull(actionUM.onClick))
|
||||
}
|
||||
.padding(padding),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -154,13 +212,10 @@ private fun TangemTopBar_Preview(@PreviewParameter(PreviewProvider::class) param
|
|||
TangemTopBar(
|
||||
title = params.title,
|
||||
subtitle = params.subtitle,
|
||||
startIconRes = params.startIconRes,
|
||||
endIconRes = params.endIconRes,
|
||||
titleIconRes = params.titleIconRes,
|
||||
isGhostButtons = params.isGhostButtons,
|
||||
onStartContentClick = {},
|
||||
onEndContentClick = {},
|
||||
modifier = Modifier.background(TangemTheme.colors2.surface.level1),
|
||||
startContent = params.startActionUM?.let { { TangemTopBarActionContent(it) } },
|
||||
endContent = params.endActionUM?.let { { TangemTopBarActionContent(it) } },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -168,10 +223,9 @@ private fun TangemTopBar_Preview(@PreviewParameter(PreviewProvider::class) param
|
|||
private class TangemTopBarPreviewData(
|
||||
val title: TextReference? = null,
|
||||
val subtitle: TextReference? = null,
|
||||
val isGhostButtons: Boolean = false,
|
||||
val titleIconRes: Int? = null,
|
||||
val startIconRes: Int? = null,
|
||||
val endIconRes: Int? = null,
|
||||
val startActionUM: TangemTopBarActionUM? = null,
|
||||
val endActionUM: TangemTopBarActionUM? = null,
|
||||
)
|
||||
|
||||
private class PreviewProvider : PreviewParameterProvider<TangemTopBarPreviewData> {
|
||||
|
|
@ -179,41 +233,79 @@ private class PreviewProvider : PreviewParameterProvider<TangemTopBarPreviewData
|
|||
get() = sequenceOf(
|
||||
TangemTopBarPreviewData(
|
||||
title = stringReference("Title"),
|
||||
startIconRes = R.drawable.ic_tangem_24,
|
||||
endIconRes = R.drawable.ic_more_vertical_24,
|
||||
isGhostButtons = true,
|
||||
startActionUM = TangemTopBarActionUM(
|
||||
iconRes = R.drawable.ic_tangem_24,
|
||||
onClick = {},
|
||||
isActionable = false,
|
||||
),
|
||||
endActionUM = TangemTopBarActionUM(
|
||||
iconRes = R.drawable.ic_more_default_24,
|
||||
onClick = {},
|
||||
isActionable = false,
|
||||
),
|
||||
),
|
||||
TangemTopBarPreviewData(
|
||||
title = stringReference("Title"),
|
||||
subtitle = stringReference("Subtitle"),
|
||||
startIconRes = R.drawable.ic_tangem_24,
|
||||
endIconRes = R.drawable.ic_more_vertical_24,
|
||||
isGhostButtons = true,
|
||||
startActionUM = TangemTopBarActionUM(
|
||||
iconRes = R.drawable.ic_tangem_24,
|
||||
onClick = {},
|
||||
isActionable = true,
|
||||
ghostModeProgress = 1f,
|
||||
),
|
||||
endActionUM = TangemTopBarActionUM(
|
||||
iconRes = R.drawable.ic_more_default_24,
|
||||
onClick = {},
|
||||
isActionable = true,
|
||||
ghostModeProgress = 1f,
|
||||
),
|
||||
),
|
||||
TangemTopBarPreviewData(
|
||||
title = stringReference("Title"),
|
||||
subtitle = stringReference("Subtitle"),
|
||||
titleIconRes = R.drawable.ic_tangem_24,
|
||||
startIconRes = R.drawable.ic_tangem_24,
|
||||
endIconRes = R.drawable.ic_more_vertical_24,
|
||||
isGhostButtons = true,
|
||||
startActionUM = TangemTopBarActionUM(
|
||||
iconRes = R.drawable.ic_tangem_24,
|
||||
onClick = {},
|
||||
isActionable = false,
|
||||
),
|
||||
endActionUM = TangemTopBarActionUM(
|
||||
iconRes = R.drawable.ic_more_default_24,
|
||||
onClick = {},
|
||||
isActionable = true,
|
||||
ghostModeProgress = 1f,
|
||||
),
|
||||
),
|
||||
TangemTopBarPreviewData(
|
||||
subtitle = stringReference("Subtitle"),
|
||||
titleIconRes = R.drawable.ic_tangem_24,
|
||||
startIconRes = R.drawable.ic_tangem_24,
|
||||
endIconRes = R.drawable.ic_more_vertical_24,
|
||||
isGhostButtons = true,
|
||||
startActionUM = TangemTopBarActionUM(
|
||||
iconRes = R.drawable.ic_tangem_24,
|
||||
onClick = {},
|
||||
isActionable = true,
|
||||
),
|
||||
endActionUM = TangemTopBarActionUM(
|
||||
iconRes = R.drawable.ic_more_default_24,
|
||||
onClick = {},
|
||||
isActionable = false,
|
||||
),
|
||||
),
|
||||
TangemTopBarPreviewData(
|
||||
title = stringReference("Title"),
|
||||
endIconRes = R.drawable.ic_more_vertical_24,
|
||||
isGhostButtons = true,
|
||||
endActionUM = TangemTopBarActionUM(
|
||||
iconRes = R.drawable.ic_more_default_24,
|
||||
onClick = {},
|
||||
isActionable = false,
|
||||
),
|
||||
),
|
||||
TangemTopBarPreviewData(
|
||||
title = stringReference("Title"),
|
||||
startIconRes = R.drawable.ic_tangem_24,
|
||||
isGhostButtons = true,
|
||||
startActionUM = TangemTopBarActionUM(
|
||||
iconRes = R.drawable.ic_tangem_24,
|
||||
onClick = {},
|
||||
isActionable = true,
|
||||
ghostModeProgress = 1f,
|
||||
),
|
||||
),
|
||||
TangemTopBarPreviewData(
|
||||
title = combinedReference(
|
||||
|
|
@ -238,8 +330,16 @@ private class PreviewProvider : PreviewParameterProvider<TangemTopBarPreviewData
|
|||
},
|
||||
),
|
||||
),
|
||||
startIconRes = R.drawable.ic_tangem_24,
|
||||
endIconRes = R.drawable.ic_more_vertical_24,
|
||||
startActionUM = TangemTopBarActionUM(
|
||||
iconRes = R.drawable.ic_tangem_24,
|
||||
onClick = {},
|
||||
isActionable = true,
|
||||
),
|
||||
endActionUM = TangemTopBarActionUM(
|
||||
iconRes = R.drawable.ic_more_default_24,
|
||||
onClick = {},
|
||||
isActionable = false,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.core.ui.ds.topbar
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.FloatRange
|
||||
|
||||
/**
|
||||
* User model for top bar action
|
||||
*
|
||||
* @property iconRes resource id of action icon
|
||||
* @property isActionable if true, action will be clickable, otherwise - not
|
||||
* @property onClick lambda be invoked when action component is clicked. If null, action will not be clickable
|
||||
* @property ghostModeProgress progress of ghost mode animation, from 0f to 1f.
|
||||
*/
|
||||
data class TangemTopBarActionUM(
|
||||
@param:DrawableRes val iconRes: Int,
|
||||
val isActionable: Boolean = true,
|
||||
val onClick: (() -> Unit)? = null,
|
||||
@param:FloatRange(0.0, 1.0) val ghostModeProgress: Float = 0f,
|
||||
)
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
package com.tangem.core.ui.ds.topbar
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import com.tangem.core.ui.extensions.clickableSingle
|
||||
import com.tangem.core.ui.extensions.conditional
|
||||
import com.tangem.core.ui.extensions.conditionalCompose
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
/**
|
||||
* Internal top bar composable that arranges optional start, center, and end content.
|
||||
* [Figma](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=8435-74860&m=dev)
|
||||
*
|
||||
* @param modifier Modifier to be applied to the top bar.
|
||||
* @param content Center content of the top bar.
|
||||
* @param startContent Optional start content of the top bar.
|
||||
* @param onStartContentClick Optional click action for the start content.
|
||||
* @param endContent Optional end content of the top bar.
|
||||
* @param onEndContentClick Optional click action for the end content.
|
||||
* @param isGhostButtons Flag to determine if ghost button styling should be applied.
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
internal fun TangemTopBarInner(
|
||||
modifier: Modifier = Modifier,
|
||||
content: (@Composable () -> Unit)? = null,
|
||||
startContent: (@Composable () -> Unit)? = null,
|
||||
onStartContentClick: (() -> Unit)? = null,
|
||||
endContent: (@Composable () -> Unit)? = null,
|
||||
onEndContentClick: (() -> Unit)? = null,
|
||||
isGhostButtons: Boolean = false,
|
||||
) {
|
||||
val statusBarHeight = with(LocalDensity.current) { WindowInsets.statusBars.getTop(density = this).toDp() }
|
||||
Box(
|
||||
modifier = modifier
|
||||
.height(TangemTheme.dimens2.x16 + statusBarHeight)
|
||||
.fillMaxWidth()
|
||||
.padding(top = statusBarHeight)
|
||||
.padding(TangemTheme.dimens2.x4, TangemTheme.dimens2.x3),
|
||||
) {
|
||||
val iconModifier = Modifier
|
||||
.size(TangemTheme.dimens2.x10)
|
||||
.clip(RoundedCornerShape(TangemTheme.dimens2.x25))
|
||||
.conditionalCompose(isGhostButtons) {
|
||||
background(TangemTheme.colors2.button.backgroundSecondary)
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = startContent != null,
|
||||
modifier = Modifier.align(Alignment.CenterStart),
|
||||
label = "Start Content Visibility",
|
||||
) {
|
||||
Box(
|
||||
modifier = iconModifier
|
||||
.conditional(onStartContentClick != null) {
|
||||
clickableSingle { onStartContentClick?.invoke() }
|
||||
}
|
||||
.conditionalCompose(isGhostButtons) { padding(TangemTheme.dimens2.x1) },
|
||||
) {
|
||||
startContent?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = content != null,
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
) {
|
||||
content?.invoke()
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = endContent != null,
|
||||
modifier = Modifier.align(Alignment.CenterEnd),
|
||||
label = "End Content Visibility",
|
||||
) {
|
||||
Box(
|
||||
modifier = iconModifier
|
||||
.conditional(onEndContentClick != null) {
|
||||
clickableSingle { onEndContentClick?.invoke() }
|
||||
}
|
||||
.conditionalCompose(isGhostButtons) { padding(TangemTheme.dimens2.x1) },
|
||||
) {
|
||||
endContent?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@ object TangemColorPalette {
|
|||
// endregion Dark
|
||||
|
||||
// region Dark Alpha
|
||||
val Dark_05 = Color(0x0D1E1E1E)
|
||||
val Dark_10 = Color(0x1A1E1E1E)
|
||||
val Dark_20 = Color(0x331E1E1E)
|
||||
val Dark_30 = Color(0x4D1E1E1E)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ class TangemColors2 internal constructor(
|
|||
val skeleton: Skeleton,
|
||||
val markers: Markers,
|
||||
val tabs: Tabs,
|
||||
val contextMenu: ContextMenu,
|
||||
) {
|
||||
|
||||
@Stable
|
||||
|
|
@ -332,15 +333,23 @@ class TangemColors2 internal constructor(
|
|||
class Neutral internal constructor(
|
||||
primary: Color,
|
||||
secondary: Color,
|
||||
tertiary: Color,
|
||||
quaternary: Color,
|
||||
) {
|
||||
var primary by mutableStateOf(primary)
|
||||
private set
|
||||
var secondary by mutableStateOf(secondary)
|
||||
private set
|
||||
var tertiary by mutableStateOf(tertiary)
|
||||
private set
|
||||
var quaternary by mutableStateOf(quaternary)
|
||||
private set
|
||||
|
||||
fun update(other: Neutral) {
|
||||
primary = other.primary
|
||||
secondary = other.secondary
|
||||
tertiary = other.tertiary
|
||||
quaternary = other.quaternary
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -612,6 +621,18 @@ class TangemColors2 internal constructor(
|
|||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
class ContextMenu internal constructor(
|
||||
background: Color,
|
||||
) {
|
||||
var background by mutableStateOf(background)
|
||||
private set
|
||||
|
||||
fun update(other: ContextMenu) {
|
||||
background = other.background
|
||||
}
|
||||
}
|
||||
|
||||
fun update(other: TangemColors2) {
|
||||
text.update(other.text)
|
||||
graphic.update(other.graphic)
|
||||
|
|
@ -625,5 +646,6 @@ class TangemColors2 internal constructor(
|
|||
skeleton.update(other.skeleton)
|
||||
markers.update(other.markers)
|
||||
tabs.update(other.tabs)
|
||||
contextMenu.update(other.contextMenu)
|
||||
}
|
||||
}
|
||||
|
|
@ -79,6 +79,8 @@ private fun lightThemeColors2(): TangemColors2 {
|
|||
neutral = TangemColors2.Border.Neutral(
|
||||
primary = TangemColorPalette.Light3,
|
||||
secondary = TangemColorPalette.Light5,
|
||||
tertiary = TangemColorPalette.Light_10,
|
||||
quaternary = TangemColorPalette.Dark_10,
|
||||
),
|
||||
status = TangemColors2.Border.Status(
|
||||
accent = TangemColorPalette.Azure,
|
||||
|
|
@ -186,6 +188,9 @@ private fun lightThemeColors2(): TangemColors2 {
|
|||
backgroundTertiary = TangemColorPalette.White,
|
||||
backgroundQuaternary = TangemColorPalette.Dark_20,
|
||||
)
|
||||
val contextMenu = TangemColors2.ContextMenu(
|
||||
background = TangemColorPalette.Dark_05,
|
||||
)
|
||||
return TangemColors2(
|
||||
text = text,
|
||||
graphic = graphic,
|
||||
|
|
@ -199,6 +204,7 @@ private fun lightThemeColors2(): TangemColors2 {
|
|||
skeleton = skeleton,
|
||||
markers = markers,
|
||||
tabs = tabs,
|
||||
contextMenu = contextMenu,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -241,6 +247,8 @@ private fun darkThemeColors2(): TangemColors2 {
|
|||
neutral = TangemColors2.Border.Neutral(
|
||||
primary = TangemColorPalette.Dark4,
|
||||
secondary = TangemColorPalette.Dark4,
|
||||
tertiary = TangemColorPalette.Light_10,
|
||||
quaternary = TangemColorPalette.Light_10,
|
||||
),
|
||||
status = TangemColors2.Border.Status(
|
||||
accent = TangemColorPalette.Azure,
|
||||
|
|
@ -348,6 +356,9 @@ private fun darkThemeColors2(): TangemColors2 {
|
|||
backgroundTertiary = TangemColorPalette.Light_10,
|
||||
backgroundQuaternary = TangemColorPalette.Light_10,
|
||||
)
|
||||
val contextMenu = TangemColors2.ContextMenu(
|
||||
background = TangemColorPalette.Light_10,
|
||||
)
|
||||
return TangemColors2(
|
||||
text = text,
|
||||
graphic = graphic,
|
||||
|
|
@ -361,5 +372,6 @@ private fun darkThemeColors2(): TangemColors2 {
|
|||
skeleton = skeleton,
|
||||
markers = markers,
|
||||
tabs = tabs,
|
||||
contextMenu = contextMenu,
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue