Skip to content

Instantly share code, notes, and snippets.

@inidamleader
Last active July 18, 2024 14:35
Show Gist options
  • Save inidamleader/b594d35362ebcf3cedf81055df519300 to your computer and use it in GitHub Desktop.
Save inidamleader/b594d35362ebcf3cedf81055df519300 to your computer and use it in GitHub Desktop.
Composable function that automatically adjusts the text size to fit within given constraints with optimal performance by using a binary search algorithm
/*
MIT License
Copyright (c) 2024 Reda El Madini
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
// LAST UPDATE: 03 Jul 2024 V6.2
package com.inidamleader.ovtracker.util.compose
import android.util.Log
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.BoxWithConstraintsScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.isSpecified
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFontFamilyResolver
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextMeasurer
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.rememberTextMeasurer
import androidx.compose.ui.text.style.LineHeightStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.isSpecified
import androidx.compose.ui.unit.sp
import com.inidamleader.ovtracker.util.compose.SuggestedFontSizesStatus.Companion.validSuggestedFontSizes
import com.inidamleader.ovtracker.util.compose.geometry.dpSizeRoundToIntSize
import com.inidamleader.ovtracker.util.compose.geometry.intPxToSp
import com.inidamleader.ovtracker.util.compose.geometry.spRoundToPx
import com.inidamleader.ovtracker.util.compose.geometry.spToIntPx
import kotlin.math.min
private const val TAG = "AutoSizeText"
/**
* Composable function that automatically adjusts the text size to fit within given constraints, considering the ratio of line spacing to text size.
*
* Features:
* 1. Best performance: Utilizes a dichotomous binary search algorithm for swift and optimal text size determination without unnecessary iterations.
* 2. Alignment support: Supports six possible alignment values via the Alignment interface.
* 3. Material Design 3 support.
* 4. Font scaling support: User-initiated font scaling doesn't affect the visual rendering output.
* 5. Multiline Support with maxLines Parameter.
*
* @param text the text to be displayed
* @param modifier the [Modifier] to be applied to this layout node
* @param color [Color] to apply to the text. If [Color.Unspecified], and [style] has no color set,
* this will be [LocalContentColor].
* @param suggestedFontSizes The suggested font sizes to choose from (Should be sorted from smallest to largest, not empty and contains only sp text unit).
* @param suggestedFontSizesStatus Whether or not suggestedFontSizes is valid: not empty - contains oly sp text unit - sorted.
* You can check validity by invoking [List<TextUnit>.suggestedFontSizesStatus].
* @param stepGranularityTextSize The step size for adjusting the text size. this parameter is ignored if [suggestedFontSizes] is specified and [suggestedFontSizesStatus] is [SuggestedFontSizesStatus.VALID].
* @param minTextSize The minimum text size allowed. this parameter is ignored if [suggestedFontSizes] is specified or [suggestedFontSizesStatus] is [SuggestedFontSizesStatus.VALID].
* @param maxTextSize The maximum text size allowed.
* @param fontStyle the typeface variant to use when drawing the letters (e.g., italic).
* See [TextStyle.fontStyle].
* @param fontWeight the typeface thickness to use when painting the text (e.g., [FontWeight.Bold]).
* @param fontFamily the font family to be used when rendering the text. See [TextStyle.fontFamily].
* @param letterSpacing the amount of space to add between each letter.
* See [TextStyle.letterSpacing].
* @param textDecoration the decorations to paint on the text (e.g., an underline).
* See [TextStyle.textDecoration].
* @param alignment The alignment of the text within its container.
* @param overflow how visual overflow should be handled.
* @param softWrap whether the text should break at soft line breaks. If false, the glyphs in the
* text will be positioned as if there was unlimited horizontal space. If [softWrap] is false,
* [overflow] and TextAlign may have unexpected effects.
* @param maxLines An optional maximum number of lines for the text to span, wrapping if
* necessary. If the text exceeds the given number of lines, it will be truncated according to
* [overflow] and [softWrap]. It is required that 1 <= [minLines] <= [maxLines].
* @param minLines The minimum height in terms of minimum number of visible lines. It is required
* that 1 <= [minLines] <= [maxLines].
* insert composables into text layout. See [InlineTextContent].
* @param onTextLayout callback that is executed when a new text layout is calculated. A
* [TextLayoutResult] object that callback provides contains paragraph information, size of the
* text, baselines and other details. The callback can be used to add additional decoration or
* functionality to the text. For example, to draw selection around the text.
* @param style style configuration for the text such as color, font, line height etc.
* @param lineSpaceRatio The ratio of line spacing to text size.
*
* @author Reda El Madini - For support, contact gladiatorkilo@gmail.com
*/
@Composable
fun AutoSizeText(
text: String,
modifier: Modifier = Modifier,
color: Color = Color.Unspecified,
suggestedFontSizes: List<TextUnit> = emptyList(),
suggestedFontSizesStatus: SuggestedFontSizesStatus = SuggestedFontSizesStatus.UNKNOWN,
stepGranularityTextSize: TextUnit = TextUnit.Unspecified,
minTextSize: TextUnit = TextUnit.Unspecified,
maxTextSize: TextUnit = TextUnit.Unspecified,
fontStyle: FontStyle? = null,
fontWeight: FontWeight? = null,
fontFamily: FontFamily? = null,
letterSpacing: TextUnit = TextUnit.Unspecified,
textDecoration: TextDecoration? = null,
alignment: Alignment = Alignment.TopStart,
overflow: TextOverflow = TextOverflow.Clip,
softWrap: Boolean = true,
maxLines: Int = Int.MAX_VALUE,
minLines: Int = 1,
onTextLayout: (TextLayoutResult) -> Unit = {},
style: TextStyle = LocalTextStyle.current,
lineSpaceRatio: Float = style.lineHeight.value / style.fontSize.value,
) {
AutoSizeText(
text = AnnotatedString(text),
modifier = modifier,
color = color,
suggestedFontSizes = suggestedFontSizes,
suggestedFontSizesStatus = suggestedFontSizesStatus,
stepGranularityTextSize = stepGranularityTextSize,
minTextSize = minTextSize,
maxTextSize = maxTextSize,
fontStyle = fontStyle,
fontWeight = fontWeight,
fontFamily = fontFamily,
letterSpacing = letterSpacing,
textDecoration = textDecoration,
alignment = alignment,
overflow = overflow,
softWrap = softWrap,
maxLines = maxLines,
minLines = minLines,
onTextLayout = onTextLayout,
style = style,
lineSpacingRatio = lineSpaceRatio,
)
}
/**
* Composable function that automatically adjusts the text size to fit within given constraints using AnnotatedString, considering the ratio of line spacing to text size.
*
* Features:
* Similar to AutoSizeText(String), with support for AnnotatedString.
*
* @param inlineContent a map storing composables that replaces certain ranges of the text, used to
* insert composables into text layout. See [InlineTextContent].
* @see AutoSizeText
*/
@Composable
fun AutoSizeText(
text: AnnotatedString,
modifier: Modifier = Modifier,
color: Color = Color.Unspecified,
suggestedFontSizes: List<TextUnit> = emptyList(),
suggestedFontSizesStatus: SuggestedFontSizesStatus = SuggestedFontSizesStatus.UNKNOWN,
stepGranularityTextSize: TextUnit = TextUnit.Unspecified,
minTextSize: TextUnit = TextUnit.Unspecified,
maxTextSize: TextUnit = TextUnit.Unspecified,
fontStyle: FontStyle? = null,
fontWeight: FontWeight? = null,
fontFamily: FontFamily? = null,
letterSpacing: TextUnit = TextUnit.Unspecified,
textDecoration: TextDecoration? = null,
alignment: Alignment = Alignment.TopStart,
overflow: TextOverflow = TextOverflow.Clip,
softWrap: Boolean = true,
maxLines: Int = Int.MAX_VALUE,
minLines: Int = 1,
inlineContent: Map<String, InlineTextContent> = mapOf(),
onTextLayout: (TextLayoutResult) -> Unit = {},
style: TextStyle = LocalTextStyle.current,
lineSpacingRatio: Float = style.lineHeight.value / style.fontSize.value,
) {
// Change font scale to 1F
val newDensity = Density(density = LocalDensity.current.density, fontScale = 1F)
CompositionLocalProvider(LocalDensity provides newDensity) {
BoxWithConstraints(
modifier = modifier,
contentAlignment = alignment,
) {
val combinedTextStyle = LocalTextStyle.current + style.copy(
color = color.takeIf { it.isSpecified } ?: style.color,
fontStyle = fontStyle ?: style.fontStyle,
fontWeight = fontWeight ?: style.fontWeight,
fontFamily = fontFamily ?: style.fontFamily,
letterSpacing = letterSpacing.takeIf { it.isSpecified } ?: style.letterSpacing,
textDecoration = textDecoration ?: style.textDecoration,
textAlign = when (alignment) {
Alignment.TopStart, Alignment.CenterStart, Alignment.BottomStart -> TextAlign.Start
Alignment.TopCenter, Alignment.Center, Alignment.BottomCenter -> TextAlign.Center
Alignment.TopEnd, Alignment.CenterEnd, Alignment.BottomEnd -> TextAlign.End
else -> TextAlign.Unspecified
},
)
val layoutDirection = LocalLayoutDirection.current
val density = LocalDensity.current
val fontFamilyResolver = LocalFontFamilyResolver.current
val textMeasurer = rememberTextMeasurer()
val coercedLineSpacingRatio = lineSpacingRatio.takeIf { it.isFinite() && it >= 1 } ?: 1F
val shouldMoveBackward: (TextUnit) -> Boolean = {
shouldShrink(
text = text,
textStyle = combinedTextStyle.copy(
fontSize = it,
lineHeight = it * coercedLineSpacingRatio,
),
maxLines = maxLines,
layoutDirection = layoutDirection,
softWrap = softWrap,
density = density,
fontFamilyResolver = fontFamilyResolver,
textMeasurer = textMeasurer,
)
}
val electedFontSize = remember(key1 = suggestedFontSizes) {
if (suggestedFontSizesStatus == SuggestedFontSizesStatus.VALID)
suggestedFontSizes
else
suggestedFontSizes.validSuggestedFontSizes
}?.let {
remember(
key1 = it,
key2 = shouldMoveBackward,
) {
it.findElectedValue(shouldMoveBackward = shouldMoveBackward)
}
} ?: run {
val candidateFontSizesIntProgress = rememberCandidateFontSizesIntProgress(
density = density,
containerDpSize = DpSize(maxWidth, maxHeight),
maxTextSize = maxTextSize,
minTextSize = minTextSize,
stepGranularityTextSize = stepGranularityTextSize,
)
remember(
key1 = candidateFontSizesIntProgress,
key2 = shouldMoveBackward,
) {
candidateFontSizesIntProgress.findElectedValue(
transform = { density.intPxToSp(it) },
shouldMoveBackward = shouldMoveBackward,
)
}
}
if (electedFontSize == 0.sp)
Log.w(
TAG,
"""The text cannot be displayed. Please consider the following options:
| 1. Providing 'suggestedFontSizes' with smaller values that can be utilized.
| 2. Decreasing the 'stepGranularityTextSize' value.
| 3. Adjusting the 'minTextSize' parameter to a suitable value and ensuring the overflow parameter is set to "TextOverflow.Ellipsis".
""".trimMargin(),
)
Text(
text = text,
overflow = overflow,
softWrap = softWrap,
maxLines = maxLines,
minLines = minLines,
inlineContent = inlineContent,
onTextLayout = onTextLayout,
style = combinedTextStyle.copy(
fontSize = electedFontSize,
lineHeight = electedFontSize * coercedLineSpacingRatio,
),
)
}
}
}
private fun BoxWithConstraintsScope.shouldShrink(
text: AnnotatedString,
textStyle: TextStyle,
maxLines: Int,
layoutDirection: LayoutDirection,
softWrap: Boolean,
density: Density,
fontFamilyResolver: FontFamily.Resolver,
textMeasurer: TextMeasurer,
) = textMeasurer.measure(
text = text,
style = textStyle,
overflow = TextOverflow.Clip,
softWrap = softWrap,
maxLines = maxLines,
constraints = constraints,
layoutDirection = layoutDirection,
density = density,
fontFamilyResolver = fontFamilyResolver,
).hasVisualOverflow
@Stable
@Composable
private fun rememberCandidateFontSizesIntProgress(
density: Density,
containerDpSize: DpSize,
minTextSize: TextUnit = TextUnit.Unspecified,
maxTextSize: TextUnit = TextUnit.Unspecified,
stepGranularityTextSize: TextUnit = TextUnit.Unspecified,
): IntProgression {
val max = remember(key1 = density, key2 = maxTextSize, key3 = containerDpSize) {
val intSize = density.dpSizeRoundToIntSize(containerDpSize)
min(intSize.width, intSize.height).let { max ->
maxTextSize
.takeIf { it.isSp }
?.let { density.spRoundToPx(it) }
?.coerceIn(range = 0..max)
?: max
}
}
val min = remember(key1 = density, key2 = minTextSize, key3 = max) {
minTextSize
.takeIf { it.isSp }
?.let { density.spToIntPx(it) }
?.coerceIn(range = 0..max)
?: 0
}
val step = remember(
key1 = listOf(
density,
min,
max,
stepGranularityTextSize,
)
) {
stepGranularityTextSize
.takeIf { it.isSp }
?.let { density.spToIntPx(it) }
?.coerceIn(1, max - min)
?: 1
}
return remember(key1 = min, key2 = max, key3 = step) {
min..max step step
}
}
// This function works by using a binary search algorithm
fun <T> List<T>.findElectedValue(shouldMoveBackward: (T) -> Boolean) = run {
indices.findElectedValue(
transform = { this[it] },
shouldMoveBackward = shouldMoveBackward,
)
}
// This function works by using a binary search algorithm
private fun <T> IntProgression.findElectedValue(
transform: (Int) -> T,
shouldMoveBackward: (T) -> Boolean,
) = run {
var low = first / step
var high = last / step
while (low <= high) {
val mid = low + (high - low) / 2
if (shouldMoveBackward(transform(mid * step)))
high = mid - 1
else
low = mid + 1
}
transform((high * step).coerceAtLeast(first * step))
}
enum class SuggestedFontSizesStatus {
VALID, INVALID, UNKNOWN;
companion object {
val List<TextUnit>.suggestedFontSizesStatus
get() = if (isNotEmpty() && all { it.isSp } && sortedBy { it.value } == this)
VALID
else
INVALID
val List<TextUnit>.validSuggestedFontSizes
get() = takeIf { it.isNotEmpty() } // Optimization: empty check first to immediately return null
?.filter { it.isSp }
?.takeIf { it.isNotEmpty() }
?.sortedBy { it.value }
}
}
@Preview(widthDp = 200, heightDp = 100)
@Preview(widthDp = 200, heightDp = 30)
@Preview(widthDp = 60, heightDp = 30)
@Composable
fun PreviewAutoSizeTextWithMaxLinesSetToIntMaxValue() {
MaterialTheme {
Surface(color = MaterialTheme.colorScheme.primary) {
AutoSizeText(
text = "This is a bunch of text that will be auto sized",
modifier = Modifier.fillMaxSize(),
alignment = Alignment.CenterStart,
style = MaterialTheme.typography.bodyMedium,
)
}
}
}
@Preview(widthDp = 200, heightDp = 100)
@Preview(widthDp = 200, heightDp = 30)
@Preview(widthDp = 60, heightDp = 30)
@Composable
fun PreviewAutoSizeTextWithMinSizeSetTo14() {
MaterialTheme {
Surface(color = MaterialTheme.colorScheme.secondary) {
AutoSizeText(
text = "This is a bunch of text that will be auto sized",
modifier = Modifier.fillMaxSize(),
minTextSize = 14.sp,
alignment = Alignment.CenterStart,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodyMedium,
)
}
}
}
@Preview(widthDp = 200, heightDp = 100)
@Preview(widthDp = 200, heightDp = 30)
@Preview(widthDp = 60, heightDp = 30)
@Composable
fun PreviewAutoSizeTextWithMaxLinesSetToOne() {
MaterialTheme {
Surface(color = MaterialTheme.colorScheme.tertiary) {
AutoSizeText(
text = "This is a bunch of text that will be auto sized",
modifier = Modifier.fillMaxSize(),
alignment = Alignment.Center,
maxLines = 1,
style = MaterialTheme.typography.bodyMedium
)
}
}
}
@Preview(widthDp = 100, heightDp = 50)
@Preview(widthDp = 50, heightDp = 100)
@Composable
fun PreviewAutoSizeTextWithMCharacter() {
MaterialTheme {
Surface(color = MaterialTheme.colorScheme.error) {
AutoSizeText(
text = "m",
modifier = Modifier.fillMaxSize(),
alignment = Alignment.Center,
style = MaterialTheme.typography.bodyMedium,
lineSpaceRatio = 1F,
)
}
}
}
@Preview(widthDp = 100, heightDp = 50)
@Preview(widthDp = 50, heightDp = 100)
@Composable
fun PreviewAutoSizeTextWithYCharacter() {
MaterialTheme {
Surface(color = MaterialTheme.colorScheme.error) {
AutoSizeText(
text = "y",
modifier = Modifier.fillMaxSize(),
alignment = Alignment.Center,
style = MaterialTheme.typography.bodyMedium.copy(
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.Both,
)
),
)
}
}
}
package com.inidamleader.ovtracker.util.compose
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.em
import androidx.compose.ui.unit.sp
import com.inidamleader.ovtracker.util.compose.SuggestedFontSizesStatus.Companion.suggestedFontSizesStatus
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertFails
import kotlin.test.assertTrue
class AutoSizeTextTest {
@Test
fun findElectedIndexThrowException() {
List(size = 0) { it }.run {
assertFails { findElectedValue { false } }
}
List(size = 0) { it }.run {
assertFails { findElectedValue { true } }
}
}
@Test
fun findElectedValueAt0() {
List(size = 1) { it }.run {
assertEquals(
expected = 0,
actual = findElectedValue { false },
)
}
List(size = 1) { it }.run {
assertEquals(
expected = 0,
actual = findElectedValue { true },
)
}
List(size = 2) { it }.run {
assertEquals(
expected = 0,
actual = findElectedValue { true },
)
}
List(size = 15) { it }.run {
assertEquals(
expected = 0,
actual = findElectedValue { true },
)
}
List(size = 16) { it }.run {
assertEquals(
expected = 0,
actual = findElectedValue { true },
)
}
}
@Test
fun findElectedValueAt1() {
List(size = 2) { it }.run {
assertEquals(
expected = 1,
actual = findElectedValue { false },
)
}
List(size = 2) { it }.run {
assertEquals(
expected = 1,
actual = findElectedValue { it > 1 },
)
}
List(size = 15) { it }.run {
assertEquals(
expected = 1,
actual = findElectedValue { it > 1 },
)
}
List(size = 16) { it }.run {
assertEquals(
expected = 1,
actual = findElectedValue { it > 1 },
)
}
}
@Test
fun findElectedValueAt2() {
List(size = 3) { it }.run {
assertEquals(
expected = 2,
actual = findElectedValue { false },
)
}
List(size = 14) { it }.run {
assertEquals(
expected = 2,
actual = findElectedValue { it > 2 },
)
}
List(size = 15) { it }.run {
assertEquals(
expected = 2,
actual = findElectedValue { it > 2 },
)
}
List(size = 16) { it }.run {
assertEquals(
expected = 2,
actual = findElectedValue { it > 2 },
)
}
}
@Test
fun findElectedValueAt9() {
List(size = 10) { it }.run {
assertEquals(
expected = 9,
actual = findElectedValue { false },
)
}
List(size = 14) { it }.run {
assertEquals(
expected = 9,
actual = findElectedValue { it > 9 },
)
}
List(size = 15) { it }.run {
assertEquals(
expected = 9,
actual = findElectedValue { it > 9 },
)
}
List(size = 16) { it }.run {
assertEquals(
expected = 9,
actual = findElectedValue { it > 9 },
)
}
}
@Test
fun findElectedValueAt10() {
List(size = 11) { it }.run {
assertEquals(
expected = 10,
actual = findElectedValue { false },
)
}
List(size = 14) { it }.run {
assertEquals(
expected = 10,
actual = findElectedValue { it > 10 },
)
}
List(size = 15) { it }.run {
assertEquals(
expected = 10,
actual = findElectedValue { it > 10 },
)
}
List(size = 16) { it }.run {
assertEquals(
expected = 10,
actual = findElectedValue { it > 10 },
)
}
}
@Test
fun suggestedFontSizesStatus() {
assertTrue {
listOf(1.sp, 2.sp, 4.sp, 8.sp).suggestedFontSizesStatus == SuggestedFontSizesStatus.VALID
}
assertTrue {
listOf(2.sp, 8.sp, 1.sp, 4.sp).suggestedFontSizesStatus == SuggestedFontSizesStatus.INVALID
}
assertTrue {
emptyList<TextUnit>().suggestedFontSizesStatus == SuggestedFontSizesStatus.INVALID
}
assertTrue {
listOf(1.sp, 2.em, 8.sp, 4.sp).suggestedFontSizesStatus == SuggestedFontSizesStatus.INVALID
}
}
}
package com.inidamleader.ovtracker.util.compose.geometry
import androidx.compose.runtime.Composable
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.geometry.isSpecified
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.isSpecified
import kotlin.math.roundToInt
// Those function are designed to be used in lambdas
// DP
fun Density.dpToSp(dp: Dp) = if (dp.isSpecified) dp.toSp() else TextUnit.Unspecified
fun Density.dpToFloatPx(dp: Dp) = if (dp.isSpecified) dp.toPx() else Float.NaN
fun Density.dpToIntPx(dp: Dp) = if (dp.isSpecified) dp.toPx().toInt() else 0
fun Density.dpRoundToPx(dp: Dp) = if (dp.isSpecified) dp.roundToPx() else 0
@Composable
fun Dp.toSp() = LocalDensity.current.dpToSp(this)
@Composable
fun Dp.toFloatPx() = LocalDensity.current.dpToFloatPx(this)
@Composable
fun Dp.toIntPx() = LocalDensity.current.dpToIntPx(this)
@Composable
fun Dp.roundToPx() = LocalDensity.current.dpRoundToPx(this)
fun Dp.toRecDpSize() = if (isSpecified) DpSize(this, this) else DpSize.Unspecified
fun Dp.toRecDpOffset() = if (isSpecified) DpOffset(this, this) else DpOffset.Unspecified
// TEXT UNIT
fun Density.spToDp(sp: TextUnit) = if (sp.isSpecified) sp.toDp() else Dp.Unspecified
fun Density.spToFloatPx(sp: TextUnit) = if (sp.isSpecified) sp.toPx() else Float.NaN
fun Density.spToIntPx(sp: TextUnit) = if (sp.isSpecified) sp.toPx().toInt() else 0
fun Density.spRoundToPx(sp: TextUnit) = if (sp.isSpecified) sp.roundToPx() else 0
@Composable
fun TextUnit.toDp() = LocalDensity.current.spToDp(this)
@Composable
fun TextUnit.toFloatPx() = LocalDensity.current.spToFloatPx(this)
@Composable
fun TextUnit.toIntPx() = LocalDensity.current.spToIntPx(this)
@Composable
fun TextUnit.roundToPx() = LocalDensity.current.spRoundToPx(this)
// FLOAT
fun Density.floatPxToDp(px: Float) = if (px.isFinite()) px.toDp() else Dp.Unspecified
fun Density.floatPxToSp(px: Float) = if (px.isFinite()) px.toSp() else TextUnit.Unspecified
@Composable
fun Float.toDp() = LocalDensity.current.floatPxToDp(this)
@Composable
fun Float.toSp() = LocalDensity.current.floatPxToSp(this)
fun Float.toIntPx() = if (isFinite()) toInt() else 0
fun Float.roundToPx() = if (isFinite()) roundToInt() else 0
fun Float.toRecSize() = if (isFinite()) Size(this, this) else Size.Unspecified
fun Float.toRecOffset() = if (isFinite()) Offset(this, this) else Offset.Unspecified
// INT
fun Density.intPxToDp(px: Int) = px.toDp()
fun Density.intPxToSp(px: Int) = px.toSp()
@Composable
fun Int.toDp() = LocalDensity.current.intPxToDp(this)
@Composable
fun Int.toSp() = LocalDensity.current.intPxToSp(this)
fun Int.toFloatPx() = toFloat()
fun Int.toRecIntSize() = IntSize(this, this)
fun Int.toRecIntOffset() = IntOffset(this, this)
// DP SIZE
fun Density.dpSizeToIntSize(dpSize: DpSize) =
if (dpSize.isSpecified) IntSize(dpSize.width.toPx().toInt(), dpSize.height.toPx().toInt())
else IntSize.Zero
fun Density.dpSizeRoundToIntSize(dpSize: DpSize) =
if (dpSize.isSpecified) IntSize(dpSize.width.roundToPx(), dpSize.height.roundToPx())
else IntSize.Zero
fun Density.dpSizeToSize(dpSize: DpSize) =
if (dpSize.isSpecified) Size(dpSize.width.toPx(), dpSize.height.toPx())
else Size.Unspecified
@Composable
fun DpSize.toIntSize() = LocalDensity.current.dpSizeToIntSize(this)
@Composable
fun DpSize.roundToIntSize() = LocalDensity.current.dpSizeRoundToIntSize(this)
@Composable
fun DpSize.toSize() = LocalDensity.current.dpSizeToSize(this)
fun DpSize.isSpaced() = isSpecified && width > 0.dp && height > 0.dp
// SIZE
fun Density.sizeToDpSize(size: Size) =
if (size.isSpecified) DpSize(size.width.toDp(), size.height.toDp())
else DpSize.Unspecified
@Composable
fun Size.toDpSize() =
if (isSpecified) LocalDensity.current.sizeToDpSize(this)
else DpSize.Unspecified
fun Size.toIntSize() =
if (isSpecified) IntSize(width.toInt(), height.toInt())
else IntSize.Zero
fun Size.isSpaced() = isSpecified && width > 0F && height > 0F
// INT SIZE
fun Density.intSizeToDpSize(intSize: IntSize) = DpSize(intSize.width.toDp(), intSize.height.toDp())
@Composable
fun IntSize.toDpSize() = LocalDensity.current.intSizeToDpSize(this)
@Composable
fun IntSize.toSize() = Size(width.toFloat(), height.toFloat())
fun IntSize.isSpaced() = width > 0 && height > 0
// DP OFFSET
fun Density.dpOffsetToIntOffset(dpOffset: DpOffset) =
if (dpOffset.isSpecified) IntOffset(dpOffset.x.toPx().toInt(), dpOffset.y.toPx().toInt())
else IntOffset.Zero
fun Density.dpOffsetRoundToIntOffset(dpOffset: DpOffset) =
if (dpOffset.isSpecified) IntOffset(dpOffset.x.roundToPx(), dpOffset.y.roundToPx())
else IntOffset.Zero
fun Density.dpOffsetToOffset(dpOffset: DpOffset) =
if (dpOffset.isSpecified) Offset(dpOffset.x.toPx(), dpOffset.y.toPx())
else Offset.Unspecified
@Composable
fun DpOffset.toIntOffset() = LocalDensity.current.dpOffsetToIntOffset(this)
@Composable
fun DpOffset.roundToIntOffset() = LocalDensity.current.dpOffsetRoundToIntOffset(this)
@Composable
fun DpOffset.toOffset() = LocalDensity.current.dpOffsetToOffset(this)
// OFFSET
fun Density.offsetToDpOffset(offset: Offset) =
if (offset.isSpecified) DpOffset(offset.x.toDp(), offset.y.toDp())
else DpOffset.Unspecified
@Composable
fun Offset.toDpOffset() = LocalDensity.current.offsetToDpOffset(this)
fun Offset.toIntOffset() =
if (isSpecified) IntOffset(x.toInt(), y.toInt())
else IntOffset.Zero
// INT OFFSET
fun Density.intOffsetToDpOffset(intOffset: IntOffset) = DpOffset(intOffset.x.toDp(), intOffset.y.toDp())
@Composable
fun IntOffset.toDpOffset() = LocalDensity.current.intOffsetToDpOffset(this)
fun IntOffset.toOffset() = Offset(x.toFloat(), y.toFloat())
@NelzkieLabs
Copy link

Works wonderfully. Thank you for this

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment