Skip to content

Instantly share code, notes, and snippets.

@halilozercan
Created October 12, 2022 15:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save halilozercan/d1468efa45c4e9a71907623e41c2ccb7 to your computer and use it in GitHub Desktop.
Save halilozercan/d1468efa45c4e9a71907623e41c2ccb7 to your computer and use it in GitHub Desktop.
package com.halilibo.memecreator.ui.editor
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.layout.layout
import androidx.compose.ui.node.Ref
import androidx.compose.ui.text.*
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.*
@OptIn(ExperimentalTextApi::class)
@Composable
fun AutoSizeBasicText(
text: AnnotatedString,
modifier: Modifier = Modifier,
style: TextStyle = TextStyle.Default, // fontSize is ignored
onTextLayout: (TextLayoutResult) -> Unit = {},
minFontSize: TextUnit = 4.sp,
maxFontSize: TextUnit = 144.sp,
autosizeGranularity: Int = 100
) {
val textMeasurer = rememberTextMeasurer()
val autosizeTextMeasurer = remember(textMeasurer) { AutosizeTextMeasurer(textMeasurer) }
val textLayoutResult = remember { Ref<TextLayoutResult?>() }
var flag by remember { mutableStateOf(Unit, neverEqualPolicy()) }
Box(modifier = modifier
.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
val result = autosizeTextMeasurer.measure(
text = text,
style = style,
constraints = constraints.copy(minHeight = 0),
minFontSize = minFontSize,
maxFontSize = maxFontSize,
autosizeGranularity = autosizeGranularity
)
textLayoutResult.value = result
flag = Unit
onTextLayout(result)
layout(placeable.width, placeable.height) {
placeable.placeRelative(0, 0)
}
}
.drawBehind {
flag
val result = textLayoutResult.value
if (result != null) {
translate(
left = (size.width - result.size.width) / 2,
top = (size.height - result.size.height) / 2
) {
drawText(result)
}
}
})
}
@OptIn(ExperimentalTextApi::class)
class AutosizeTextMeasurer(private val textMeasurer: TextMeasurer) {
var textUnitArray: Array<TextUnit> = emptyArray()
@Stable
fun measure(
text: AnnotatedString,
style: TextStyle = TextStyle.Default, // fontSize will be ignored
constraints: Constraints = Constraints(),
minFontSize: TextUnit = 4.sp,
maxFontSize: TextUnit = 144.sp,
autosizeGranularity: Int = 100
): TextLayoutResult {
val fontSize = findFontSize(
text = text,
style = style,
constraints = constraints,
minFontSize = minFontSize,
maxFontSize = maxFontSize,
autosizeGranularity = autosizeGranularity
)
return textMeasurer.measure(
text = text,
style = style.copy(fontSize = fontSize),
overflow = TextOverflow.Clip,
softWrap = true,
maxLines = Int.MAX_VALUE,
constraints = constraints
)
}
fun findFontSize(
text: AnnotatedString,
style: TextStyle = TextStyle.Default, // fontSize will be ignored
constraints: Constraints = Constraints(),
minFontSize: TextUnit = 4.sp,
maxFontSize: TextUnit = 144.sp,
autosizeGranularity: Int = 100
): TextUnit {
require(minFontSize.isSpecified && maxFontSize.isSpecified) {
"min and max font sizes must be specified"
}
require((minFontSize.isSp && maxFontSize.isSp) || (minFontSize.isEm && maxFontSize.isEm)) {
"min and max font sizes must be in the same format"
}
require(maxFontSize.value >= minFontSize.value) {
"max value must be equal to or larger than min value"
}
require(autosizeGranularity >= 0) {
"granularity must be a positive integer"
}
if (textUnitArray.size != 2 + autosizeGranularity ||
textUnitArray.first() != minFontSize ||
textUnitArray.last() != maxFontSize) {
val size = 2 + autosizeGranularity
textUnitArray = Array(size) {
when(it) {
0 -> minFontSize
size - 1 -> maxFontSize
else -> lerp(minFontSize, maxFontSize, (1f / (autosizeGranularity + 1))*it)
}
}
}
val fontSize = binarySearchTextSize(array = textUnitArray) {
val textLayoutResult = textMeasurer.measure(
text = text,
style = style.copy(fontSize = it),
overflow = TextOverflow.Clip,
softWrap = false,
maxLines = Int.MAX_VALUE,
constraints = constraints
)
!textLayoutResult.hasVisualOverflow
}
return textUnitArray[fontSize]
}
}
internal fun binarySearchTextSize(
array: Array<TextUnit>,
start: Int = 0,
end: Int = array.size - 1,
block: (TextUnit) -> Boolean
): Int {
// Array contains a list of sorted text units (font sizes).
// calling block tells whether given font size fits into layout.
// If array is mapped using block, the result would look like
// T T T T ... T T F F .. F
// this functions returns the last T index
// we are down to single index. if this is true, return this, else return -1
if (start == end) {
return if (block(array[start])) {
start
} else {
(start - 1).coerceAtLeast(0)
}
}
val mid = (start + end) / 2
// if true, we need to go higher, else we go lower
return if (block(array[mid])) {
binarySearchTextSize(array, mid+1, end, block)
} else {
binarySearchTextSize(array, start, mid, block)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment