Updated on 2026-08-14

This commit is contained in:
Tangem 2023-01-18 17:51:32 +04:00
parent 9c1931f7d7
commit cfcc704666
21 changed files with 437 additions and 142 deletions

View file

@ -0,0 +1,173 @@
package com.tangem.core.ui.components
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.TextUnit
/**
* https://stackoverflow.com/questions/69083061/how-to-make-middle-ellipsis-in-text-with-jetpack-compose
*/
@Composable
fun MiddleEllipsisText(
text: String,
modifier: Modifier = Modifier,
color: Color = Color.Unspecified,
fontSize: TextUnit = TextUnit.Unspecified,
fontStyle: FontStyle? = null,
fontWeight: FontWeight? = null,
fontFamily: FontFamily? = null,
letterSpacing: TextUnit = TextUnit.Unspecified,
textDecoration: TextDecoration? = null,
textAlign: TextAlign? = null,
lineHeight: TextUnit = TextUnit.Unspecified,
softWrap: Boolean = true,
onTextLayout: (TextLayoutResult) -> Unit = {},
style: TextStyle = LocalTextStyle.current,
) {
// some letters, like "r", will have less width when placed right before "."
// adding a space to prevent such case
val layoutText = remember(text) { "$text $ellipsisText" }
val textLayoutResultState = remember(layoutText) {
mutableStateOf<TextLayoutResult?>(null)
}
SubcomposeLayout(modifier) { constraints ->
// result is ignored - we only need to fill our textLayoutResult
subcompose("measure") {
Text(
text = layoutText,
color = color,
fontSize = fontSize,
fontStyle = fontStyle,
fontWeight = fontWeight,
fontFamily = fontFamily,
letterSpacing = letterSpacing,
textDecoration = textDecoration,
textAlign = textAlign,
lineHeight = lineHeight,
softWrap = softWrap,
maxLines = 1,
onTextLayout = { textLayoutResultState.value = it },
style = style,
)
}.first().measure(Constraints())
// to allow smart cast
val textLayoutResult = textLayoutResultState.value
?: // shouldn't happen - onTextLayout is called before subcompose finishes
return@SubcomposeLayout layout(0, 0) {}
val placeable = subcompose("visible") {
val finalText = remember(text, textLayoutResult, constraints.maxWidth) {
if (text.isEmpty() || textLayoutResult.getBoundingBox(text.indices.last).right <= constraints.maxWidth) {
// text not including ellipsis fits on the first line.
return@remember text
}
val ellipsisWidth = layoutText.indices.toList()
.takeLast(ellipsisCharactersCount)
.let widthLet@{ indices ->
// fix this bug: https://issuetracker.google.com/issues/197146630
// in this case width is invalid
for (i in indices) {
val width = textLayoutResult.getBoundingBox(i).width
if (width > 0) {
return@widthLet width * ellipsisCharactersCount
}
}
// this should not happen, because
// this error occurs only for the last character in the string
throw IllegalStateException("all ellipsis chars have invalid width")
}
val availableWidth = constraints.maxWidth - ellipsisWidth
val startCounter = BoundCounter(text, textLayoutResult) { it }
val endCounter = BoundCounter(text, textLayoutResult) { text.indices.last - it }
while (availableWidth - startCounter.width - endCounter.width > 0) {
val possibleEndWidth = endCounter.widthWithNextChar()
if (
startCounter.width >= possibleEndWidth
&& availableWidth - startCounter.width - possibleEndWidth >= 0
) {
endCounter.addNextChar()
} else if (availableWidth - startCounter.widthWithNextChar() - endCounter.width >= 0) {
startCounter.addNextChar()
} else {
break
}
}
startCounter.string.trimEnd() + ellipsisText + endCounter.string.reversed().trimStart()
}
Text(
text = finalText,
color = color,
fontSize = fontSize,
fontStyle = fontStyle,
fontWeight = fontWeight,
fontFamily = fontFamily,
letterSpacing = letterSpacing,
textDecoration = textDecoration,
textAlign = textAlign,
lineHeight = lineHeight,
softWrap = softWrap,
onTextLayout = onTextLayout,
style = style,
)
}[0].measure(constraints)
layout(placeable.width, placeable.height) {
placeable.place(0, 0)
}
}
}
private const val ellipsisCharactersCount = 3
private const val ellipsisCharacter = '.'
private val ellipsisText = List(ellipsisCharactersCount) { ellipsisCharacter }.joinToString(separator = "")
private class BoundCounter(
private val text: String,
private val textLayoutResult: TextLayoutResult,
private val charPosition: (Int) -> Int,
) {
var string = ""
private set
var width = 0f
private set
private var _nextCharWidth: Float? = null
private var invalidCharsCount = 0
fun widthWithNextChar(): Float =
width + nextCharWidth()
private fun nextCharWidth(): Float =
_nextCharWidth ?: run {
var boundingBox: Rect
// invalidCharsCount fixes this bug: https://issuetracker.google.com/issues/197146630
invalidCharsCount--
do {
boundingBox = textLayoutResult
.getBoundingBox(charPosition(string.count() + ++invalidCharsCount))
} while (boundingBox.right == 0f)
_nextCharWidth = boundingBox.width
boundingBox.width
}
fun addNextChar() {
string += text[charPosition(string.count())]
width += nextCharWidth()
_nextCharWidth = null
}
}

View file

@ -23,6 +23,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
@ -46,15 +47,15 @@ import com.tangem.core.ui.res.TangemTheme
*/
@Composable
fun ResultScreenContent(
resultMessage: String,
onButtonClick: () -> Unit,
modifier: Modifier = Modifier,
resultMessage: AnnotatedString,
@StringRes title: Int = R.string.common_success,
resultColor: Color = TangemTheme.colors.icon.accent,
@DrawableRes icon: Int = R.drawable.ic_check_24,
@DrawableRes secondaryButtonIcon: Int? = null,
@StringRes secondaryButtonText: Int? = null,
onSecondaryButtonClick: (() -> Unit)? = null,
onButtonClick: () -> Unit,
) {
Column(
modifier = modifier
@ -92,6 +93,8 @@ fun ResultScreenContent(
secondaryButtonText = secondaryButtonText,
secondaryButtonIcon = secondaryButtonIcon,
onSecondaryButtonClick = onSecondaryButtonClick,
modifier = Modifier
.fillMaxWidth(),
)
SpacerH12()
}
@ -140,22 +143,23 @@ fun SuccessImage(
@Composable
private fun SecondaryButtonForResultScreen(
modifier: Modifier = Modifier,
@StringRes secondaryButtonText: Int,
onSecondaryButtonClick: () -> Unit,
@DrawableRes secondaryButtonIcon: Int? = null,
onSecondaryButtonClick: (() -> Unit),
) {
if (secondaryButtonIcon != null) {
SecondaryButtonIconLeft(
text = stringResource(id = secondaryButtonText),
icon = painterResource(id = secondaryButtonIcon),
onClick = onSecondaryButtonClick,
modifier = Modifier.fillMaxWidth(),
modifier = modifier,
)
} else {
SecondaryButton(
text = stringResource(id = secondaryButtonText),
onClick = onSecondaryButtonClick,
modifier = Modifier.fillMaxWidth(),
modifier = modifier,
)
}
}
@ -165,7 +169,7 @@ private fun SecondaryButtonForResultScreen(
@Composable
private fun SuccessScreenPreview() {
ResultScreenContent(
resultMessage = "Swap of 1 000 DAI to 1 131,46 MATIC",
resultMessage = AnnotatedString("Swap of 1 000 DAI to 1 131,46 MATIC"),
secondaryButtonText = R.string.swapping_success_view_explorer_button_title,
onSecondaryButtonClick = {},
onButtonClick = {},
@ -174,7 +178,7 @@ private fun SuccessScreenPreview() {
@Preview(showBackground = true)
@Composable
private fun Preview_SuccessScreenContent_InLightTheme() {
fun Preview_SuccessScreenContent_InLightTheme() {
TangemTheme(isDark = false) {
SuccessScreenPreview()
}
@ -182,7 +186,7 @@ private fun Preview_SuccessScreenContent_InLightTheme() {
@Preview(showBackground = true)
@Composable
private fun Preview_SuccessScreenContent_InDarkTheme() {
fun Preview_SuccessScreenContent_InDarkTheme() {
TangemTheme(isDark = true) {
SuccessScreenPreview()
}

View file

@ -1,12 +1,12 @@
package com.tangem.core.ui.components
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
@ -25,11 +25,13 @@ import com.tangem.core.ui.res.TangemTheme
*/
@Composable
fun WarningCard(
modifier: Modifier = Modifier,
title: String,
description: String,
icon: @Composable (() -> Unit)? = null,
) {
WarningCardSurface(
modifier = modifier,
content = {
WarningBody(
title = title,
@ -52,12 +54,14 @@ fun WarningCard(
*/
@Composable
fun ClickableWarningCard(
modifier: Modifier = Modifier,
title: String,
description: String,
icon: @Composable (() -> Unit)? = null,
onClick: () -> Unit,
) {
WarningCardSurface(
modifier = modifier,
content = {
WarningBody(title = title, description = description, icon = icon) {
SpacerW12()
@ -84,12 +88,14 @@ fun ClickableWarningCard(
*/
@Composable
fun RefreshableWaringCard(
modifier: Modifier = Modifier,
title: String,
description: String,
icon: @Composable (() -> Unit)? = null,
onClick: () -> Unit,
) {
WarningCardSurface(
modifier = modifier,
content = {
WarningBody(title = title, description = description, icon = icon) {
SpacerW12()
@ -108,6 +114,7 @@ fun RefreshableWaringCard(
@Composable
private fun WarningBody(
modifier: Modifier = Modifier,
title: String,
description: String,
icon: @Composable (() -> Unit)? = null,
@ -126,19 +133,20 @@ private fun WarningBody(
)
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
private fun WarningCardSurface(
modifier: Modifier = Modifier,
onClick: (() -> Unit)? = null,
content: @Composable () -> Unit,
) {
Surface(
Card(
shape = RoundedCornerShape(TangemTheme.dimens.size12),
color = TangemTheme.colors.background.primary,
backgroundColor = TangemTheme.colors.background.primary,
elevation = TangemTheme.dimens.elevation2,
modifier = Modifier.clickable(
enabled = onClick != null,
onClick = onClick ?: {},
),
onClick = { onClick ?: Unit },
enabled = onClick != null,
modifier = modifier,
) {
content()
}

View file

@ -45,8 +45,8 @@ import com.tangem.core.ui.res.TangemTheme
* @param expandedInitially whether the search is expanded on launch
* @param tint tint for most of the visual elements of toolbar
* @param onBackClick action when close button is clicked
* @param onSearchChange action when search is modified
* @param onSearchDisplayClose action when search is closed
* @param onSearchChanged action when search is modified
* @param onSearchDisplayClosed action when search is closed
*
* @see <a href =
* "https://www.figma.com/file/Vs6SkVsFnUPsSCNwlnVf5U/Android-%E2%80%93-UI?node-id=1123%3A4068&t=xj8BBj5DfCWn2Mli-1"
@ -54,13 +54,14 @@ import com.tangem.core.ui.res.TangemTheme
*/
@Composable
fun ExpandableSearchView(
onBackClick: () -> Unit,
onSearchChange: (String) -> Unit,
onSearchDisplayClose: () -> Unit,
modifier: Modifier = Modifier,
title: String? = null,
placeholderSearchText: String = "",
expandedInitially: Boolean = false,
tint: Color = TangemTheme.colors.text.primary1,
tint: Color = TangemTheme.colors.icon.primary1,
onBackClick: () -> Unit,
onSearchChanged: (String) -> Unit,
onSearchDisplayClosed: () -> Unit,
) {
val (expanded, onExpandedChanged) = remember {
mutableStateOf(expandedInitially)
@ -70,16 +71,18 @@ fun ExpandableSearchView(
if (isSearchFieldVisible) {
ExpandedSearchView(
placeholderSearchText = placeholderSearchText,
onSearchChange = onSearchChange,
onSearchDisplayClose = onSearchDisplayClose,
onExpandedChange = onExpandedChanged,
onSearchChanged = onSearchChanged,
onSearchDisplayClosed = onSearchDisplayClosed,
onExpandedChanged = onExpandedChanged,
modifier = modifier,
tint = tint,
)
} else {
CollapsedSearchView(
title = title,
onBackClick = onBackClick,
onExpandedChange = onExpandedChanged,
onExpandedChanged = onExpandedChanged,
modifier = modifier,
tint = tint,
)
}
@ -88,14 +91,15 @@ fun ExpandableSearchView(
@Composable
private fun CollapsedSearchView(
onBackClick: () -> Unit,
onExpandedChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
title: String? = null,
tint: Color = TangemTheme.colors.background.primary,
onBackClick: () -> Unit,
onExpandedChanged: (Boolean) -> Unit,
tint: Color,
) {
Row(
modifier = Modifier
.background(TangemTheme.colors.background.primary)
modifier = modifier
.background(TangemTheme.colors.background.secondary)
.padding(TangemTheme.dimens.spacing16)
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
@ -123,7 +127,7 @@ private fun CollapsedSearchView(
tint = tint,
contentDescription = null,
modifier = Modifier
.clickable { onExpandedChange(true) },
.clickable { onExpandedChanged(true) },
)
}
}
@ -131,10 +135,11 @@ private fun CollapsedSearchView(
@Composable
private fun ExpandedSearchView(
placeholderSearchText: String,
onSearchChange: (String) -> Unit,
onSearchDisplayClose: () -> Unit,
onExpandedChange: (Boolean) -> Unit,
tint: Color = TangemTheme.colors.background.primary,
onSearchChanged: (String) -> Unit,
onSearchDisplayClosed: () -> Unit,
onExpandedChanged: (Boolean) -> Unit,
modifier: Modifier = Modifier,
tint: Color,
) {
val focusManager = LocalFocusManager.current
val textFieldFocusRequester = remember { FocusRequester() }
@ -146,16 +151,16 @@ private fun ExpandedSearchView(
var textFieldValue by remember { mutableStateOf(TextFieldValue("", TextRange("".length))) }
Row(
modifier = Modifier
.background(TangemTheme.colors.background.primary)
modifier = modifier
.background(TangemTheme.colors.background.secondary)
.fillMaxWidth(),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically,
) {
IconButton(
onClick = {
onExpandedChange(false)
onSearchDisplayClose()
onExpandedChanged(false)
onSearchDisplayClosed()
},
) {
Icon(
@ -168,7 +173,7 @@ private fun ExpandedSearchView(
value = textFieldValue,
onValueChange = {
textFieldValue = it
onSearchChange(it.text)
onSearchChanged(it.text)
},
singleLine = true,
modifier = Modifier
@ -200,8 +205,8 @@ private fun CollapsedSearchViewPreview() {
title = "Choose Token",
onBackClick = {},
placeholderSearchText = "Search",
onSearchChange = {},
onSearchDisplayClose = {},
onSearchChanged = {},
onSearchDisplayClosed = {},
)
}
}
@ -214,9 +219,9 @@ private fun ExpandedSearchViewPreview() {
title = "Choose Token",
onBackClick = {},
placeholderSearchText = "Search",
onSearchChange = {},
onSearchChanged = {},
expandedInitially = true,
onSearchDisplayClose = {},
onSearchDisplayClosed = {},
)
}
}