Updated on 2026-08-14
This commit is contained in:
commit
cd2f5bfeed
14 changed files with 425 additions and 59 deletions
|
|
@ -6,7 +6,6 @@ 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
|
||||
|
|
@ -18,10 +17,12 @@ 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
|
||||
import com.tangem.core.ui.components.atoms.text.BoundCounter
|
||||
|
||||
/**
|
||||
* https://stackoverflow.com/questions/69083061/how-to-make-middle-ellipsis-in-text-with-jetpack-compose
|
||||
*/
|
||||
@Deprecated("Use EllipsisText with TextEllipsis.Middle ellipsis instead")
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun MiddleEllipsisText(
|
||||
|
|
@ -138,38 +139,4 @@ fun MiddleEllipsisText(
|
|||
|
||||
private const val ELLIPSIS_CHARACTERS_COUNT = 3
|
||||
private const val ELLIPSIS_CHARACTER = '.'
|
||||
private val ellipsisText = List(ELLIPSIS_CHARACTERS_COUNT) { ELLIPSIS_CHARACTER }.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
|
||||
}
|
||||
}
|
||||
private val ellipsisText = List(ELLIPSIS_CHARACTERS_COUNT) { ELLIPSIS_CHARACTER }.joinToString(separator = "")
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package com.tangem.core.ui.components.atoms.text
|
||||
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.text.TextLayoutResult
|
||||
|
||||
internal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,242 @@
|
|||
package com.tangem.core.ui.components.atoms.text
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
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.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.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
sealed class TextEllipsis {
|
||||
|
||||
object Middle : TextEllipsis()
|
||||
|
||||
object End : TextEllipsis()
|
||||
|
||||
data class OffsetEnd(
|
||||
val offsetEnd: Int = 0,
|
||||
val hasSeparator: Boolean = true,
|
||||
) : TextEllipsis()
|
||||
}
|
||||
|
||||
/**
|
||||
* https://stackoverflow.com/questions/69083061/how-to-make-middle-ellipsis-in-text-with-jetpack-compose
|
||||
*
|
||||
* Customized Text with ellipsis. Ellipsis can be placed in: Middle, End or OffsetEnd (OffsetEnd with separator).
|
||||
*
|
||||
* * OffsetEnd can be useful to display big amounts with currency symbol. OffsetEnd 0 is equal to End.
|
||||
*/
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun EllipsisText(
|
||||
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,
|
||||
ellipsis: TextEllipsis = TextEllipsis.End,
|
||||
) {
|
||||
val ellipsisText = remember(text) {
|
||||
if (ellipsis is TextEllipsis.OffsetEnd && ellipsis.hasSeparator) {
|
||||
ELLIPSIS_TEXT_WITH_SEPARATOR
|
||||
} else {
|
||||
ELLIPSIS_TEXT
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
var ellipsisWidth = 0f
|
||||
layoutText.indices.toList()
|
||||
.takeLast(ellipsisText.length)
|
||||
.forEach widthLet@{
|
||||
ellipsisWidth += textLayoutResult.getBoundingBox(it).width
|
||||
}
|
||||
|
||||
val availableWidth = constraints.maxWidth - ellipsisWidth
|
||||
val startCounter = BoundCounter(text, textLayoutResult) { it }
|
||||
val endCounter = BoundCounter(text, textLayoutResult) { text.indices.last - it }
|
||||
|
||||
when (ellipsis) {
|
||||
TextEllipsis.Middle -> {
|
||||
middleEllipsisText(
|
||||
availableWidth,
|
||||
startCounter,
|
||||
endCounter,
|
||||
)
|
||||
}
|
||||
TextEllipsis.End -> {
|
||||
offsetEndEllipsisText(
|
||||
availableWidth = availableWidth,
|
||||
startCounter = startCounter,
|
||||
endCounter = endCounter,
|
||||
)
|
||||
}
|
||||
is TextEllipsis.OffsetEnd -> {
|
||||
offsetEndEllipsisText(
|
||||
availableWidth = availableWidth,
|
||||
startCounter = startCounter,
|
||||
endCounter = endCounter,
|
||||
offsetEnd = ellipsis.offsetEnd,
|
||||
withSeparator = ellipsis.hasSeparator,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
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 ELLIPSIS_SEPARATOR = " "
|
||||
private const val ELLIPSIS_TEXT = "..."
|
||||
private const val ELLIPSIS_TEXT_WITH_SEPARATOR = ELLIPSIS_TEXT.plus(ELLIPSIS_SEPARATOR)
|
||||
|
||||
private fun middleEllipsisText(availableWidth: Float, startCounter: BoundCounter, endCounter: BoundCounter): String {
|
||||
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
|
||||
}
|
||||
}
|
||||
return startCounter.string.trimEnd() + ELLIPSIS_TEXT + endCounter.string.reversed().trimStart()
|
||||
}
|
||||
|
||||
private fun offsetEndEllipsisText(
|
||||
availableWidth: Float,
|
||||
startCounter: BoundCounter,
|
||||
endCounter: BoundCounter,
|
||||
offsetEnd: Int = 0,
|
||||
withSeparator: Boolean = false,
|
||||
): String {
|
||||
while (availableWidth - startCounter.width - endCounter.width > 0) {
|
||||
val possibleEndWidth = endCounter.widthWithNextChar()
|
||||
if (
|
||||
offsetEnd > endCounter.string.length &&
|
||||
availableWidth - startCounter.width - possibleEndWidth >= 0
|
||||
) {
|
||||
endCounter.addNextChar()
|
||||
} else if (availableWidth - startCounter.widthWithNextChar() - endCounter.width >= 0) {
|
||||
startCounter.addNextChar()
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
val ellipsis = if (withSeparator) ELLIPSIS_TEXT_WITH_SEPARATOR else ELLIPSIS_TEXT
|
||||
|
||||
return startCounter.string.trimEnd() + ellipsis + endCounter.string.reversed().trimStart()
|
||||
}
|
||||
|
||||
//region Preview
|
||||
@Preview(widthDp = 200)
|
||||
@Composable
|
||||
private fun EllipsisTexPreview(@PreviewParameter(EllipsisTexPreviewParameterProvider::class) ellipsis: TextEllipsis) {
|
||||
TangemTheme {
|
||||
EllipsisText(
|
||||
text = "11111111111111111111111111111111111111111111111111 END",
|
||||
ellipsis = ellipsis,
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class EllipsisTexPreviewParameterProvider : PreviewParameterProvider<TextEllipsis> {
|
||||
override val values: Sequence<TextEllipsis>
|
||||
get() = sequenceOf(
|
||||
TextEllipsis.Middle,
|
||||
TextEllipsis.End,
|
||||
TextEllipsis.OffsetEnd("TEXT".length),
|
||||
TextEllipsis.OffsetEnd("TEXT".length, false),
|
||||
)
|
||||
}
|
||||
//endregion
|
||||
|
|
@ -3,13 +3,13 @@ package com.tangem.core.ui.components.inputrow
|
|||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.atoms.text.EllipsisText
|
||||
import com.tangem.core.ui.components.currency.tokenicon.TokenIcon
|
||||
import com.tangem.core.ui.components.currency.tokenicon.TokenIconState
|
||||
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
|
||||
|
|
@ -56,6 +56,7 @@ fun InputRowApprox(
|
|||
iconState = leftIcon,
|
||||
title = leftTitle,
|
||||
subtitle = leftSubtitle,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_approx_24),
|
||||
|
|
@ -71,6 +72,7 @@ fun InputRowApprox(
|
|||
iconState = rightIcon,
|
||||
title = rightTitle,
|
||||
subtitle = rightSubtitle,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -97,12 +99,12 @@ private fun InputRowApproxItem(
|
|||
start = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
EllipsisText(
|
||||
text = title.resolveReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
Text(
|
||||
EllipsisText(
|
||||
text = subtitle.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
|
|
@ -123,7 +125,7 @@ private fun InputRowApproxPreview_Light() {
|
|||
leftTitle = TextReference.Str("Left title"),
|
||||
leftSubtitle = TextReference.Str("Left subtitle"),
|
||||
rightIcon = TokenIconState.Loading,
|
||||
rightTitle = TextReference.Str("Right title"),
|
||||
rightTitle = TextReference.Str("Right title Right title Right title Right title Right title"),
|
||||
rightSubtitle = TextReference.Str("Right subtitle"),
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors.background.action),
|
||||
|
|
@ -137,7 +139,7 @@ private fun InputRowApproxPreview_Dark() {
|
|||
TangemTheme(isDark = true) {
|
||||
InputRowApprox(
|
||||
leftIcon = TokenIconState.Loading,
|
||||
leftTitle = TextReference.Str("Left title"),
|
||||
leftTitle = TextReference.Str("Left title Left title Left title Left title Left title"),
|
||||
leftSubtitle = TextReference.Str("Left subtitle"),
|
||||
rightIcon = TokenIconState.Loading,
|
||||
rightTitle = TextReference.Str("Right title"),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue