-
-
Save halilozercan/d1468efa45c4e9a71907623e41c2ccb7 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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