Skip to content

Instantly share code, notes, and snippets.

@warahiko
Last active December 14, 2023 04:00
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 warahiko/573748442a331a6244853570f0abe755 to your computer and use it in GitHub Desktop.
Save warahiko/573748442a331a6244853570f0abe755 to your computer and use it in GitHub Desktop.
Sample of auto-sizable Text composable
@Composable
fun AutoSizableText(
text: String,
modifier: Modifier = Modifier,
minFontSize: TextUnit = 12.sp,
maxFontSize: TextUnit = 112.sp,
granularityInPx: Int = 1,
) {
val autoSizer = rememberAutoSizer(minFontSize, maxFontSize, granularityInPx)
val style = LocalTextStyle.current
BoxWithConstraints(modifier = modifier) {
val fontSize = remember(text, autoSizer, style, constraints) {
autoSizer.autoSize(
text = text,
style = style,
constraints = constraints,
overflow = TextOverflow.Ellipsis,
)
}
Text(
text = text,
fontSize = fontSize,
overflow = TextOverflow.Ellipsis,
style = style,
)
}
}
class AutoSizer(
minFontSize: TextUnit,
maxFontSize: TextUnit,
granularityInPx: Int,
density: Density,
private val textMeasurer: TextMeasurer,
) {
// 候補となるフォントサイズのリスト
// 二分探索できるように順に並べる
private val fontSizes: List<TextUnit> = generateSequence(minFontSize) { previous ->
with(density) {
(previous.toPx() + granularityInPx).toSp()
}
}
.takeWhile { it < maxFontSize }
.plus(maxFontSize)
.toList()
fun autoSize(
text: String,
style: TextStyle,
constraints: Constraints,
overflow: TextOverflow,
): TextUnit {
// 二分探索で最適なフォントサイズを取得する
val index = fontSizes.binarySearch { targetFontSize ->
val result = textMeasurer.measure(
text = text,
style = style.copy(fontSize = targetFontSize),
constraints = constraints,
overflow = overflow,
)
// はみ出していれば正、そうでなければ負を返すと、
// 「最適なフォントサイズの『inverted insertion point』」が得られる
// see: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/binary-search.html
if (result.hasVisualOverflow) 1 else -1
}
assert(index < 0)
val insertionPoint = -(index + 1)
// 最適なフォントサイズの挿入位置は、はみ出すフォントサイズのうち最小のものの位置に等しいので、
// その一つ前を返す
val resultIndex = (insertionPoint - 1).coerceAtLeast(0)
return fontSizes[resultIndex]
}
}
@Composable
fun rememberAutoSizer(
minFontSize: TextUnit,
maxFontSize: TextUnit,
granularityInPx: Int,
): AutoSizer {
val density = LocalDensity.current
val textMeasurer = rememberTextMeasurer()
return remember(minFontSize, maxFontSize, granularityInPx, density, textMeasurer) {
AutoSizer(minFontSize, maxFontSize, granularityInPx, density, textMeasurer)
}
}
@Composable
fun SimpleAutoSizableText(
text: String,
modifier: Modifier = Modifier,
minFontSize: TextUnit = 12.sp,
maxFontSize: TextUnit = 112.sp,
granularityInPx: Int = 1,
) {
val density = LocalDensity.current
var fontSize by remember(text, minFontSize, maxFontSize, granularityInPx, density) {
mutableStateOf(maxFontSize)
}
Text(
text = text,
fontSize = fontSize,
overflow = TextOverflow.Ellipsis,
modifier = modifier,
onTextLayout = { textLayoutResult ->
if (textLayoutResult.hasVisualOverflow && fontSize > minFontSize) {
fontSize = with(density) {
(fontSize.toPx() - granularityInPx).toSp()
}
}
}
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment