-
Notifications
You must be signed in to change notification settings - Fork 200
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Introduce AutoSizeText component & use it for the top bar title of al…
…l main screens
- Loading branch information
1 parent
53f3418
commit 386206b
Showing
4 changed files
with
153 additions
and
12 deletions.
There are no files selected for viewing
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
142 changes: 142 additions & 0 deletions
142
...c/commonMain/kotlin/io/github/droidkaigi/confsched/droidkaigiui/component/AutoSizeText.kt
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
package io.github.droidkaigi.confsched.droidkaigiui.component | ||
|
||
import androidx.compose.foundation.layout.BoxWithConstraints | ||
import androidx.compose.material3.LocalTextStyle | ||
import androidx.compose.material3.Text | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.ui.Alignment | ||
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.graphics.Color | ||
import androidx.compose.ui.platform.LocalDensity | ||
import androidx.compose.ui.platform.LocalFontFamilyResolver | ||
import androidx.compose.ui.text.Paragraph | ||
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.unit.Constraints | ||
import androidx.compose.ui.unit.Dp | ||
import androidx.compose.ui.unit.TextUnit | ||
import androidx.compose.ui.unit.dp | ||
import androidx.compose.ui.unit.isFinite | ||
import androidx.compose.ui.unit.isSpecified | ||
import androidx.compose.ui.unit.isUnspecified | ||
import androidx.compose.ui.unit.sp | ||
import kotlin.math.absoluteValue | ||
import kotlin.math.ceil | ||
|
||
/** | ||
* A text component that automatically resizes its font size to fit its content, | ||
* up to the given [maxFontSize]. (If you don't want to limit the upper bounds | ||
* of the text size, use [TextUnit.Unspecified] for that parameter.) | ||
* | ||
* Adapted from Alchitry on Stack Overflow, licensed under CC BY-SA 4.0: | ||
* https://stackoverflow.com/a/71416530 | ||
*/ | ||
@Composable | ||
fun AutoSizeText( | ||
text: String, | ||
modifier: Modifier = Modifier, | ||
acceptableError: Dp = 5.dp, | ||
color: Color = Color.Unspecified, | ||
fontStyle: FontStyle? = null, | ||
fontWeight: FontWeight? = null, | ||
fontFamily: FontFamily? = null, | ||
letterSpacing: TextUnit = TextUnit.Unspecified, | ||
textDecoration: TextDecoration? = null, | ||
textAlign: TextAlign? = null, | ||
contentAlignment: Alignment? = null, | ||
lineHeight: TextUnit = TextUnit.Unspecified, | ||
maxLines: Int = Int.MAX_VALUE, | ||
onTextLayout: (TextLayoutResult) -> Unit = {}, | ||
style: TextStyle = LocalTextStyle.current, | ||
maxFontSize: TextUnit = style.fontSize, | ||
) { | ||
val alignment: Alignment = contentAlignment ?: when (textAlign) { | ||
TextAlign.Left -> Alignment.TopStart | ||
TextAlign.Right -> Alignment.TopEnd | ||
TextAlign.Center -> Alignment.Center | ||
TextAlign.Justify -> Alignment.TopCenter | ||
TextAlign.Start -> Alignment.TopStart | ||
TextAlign.End -> Alignment.TopEnd | ||
else -> Alignment.TopStart | ||
} | ||
BoxWithConstraints(modifier = modifier, contentAlignment = alignment) { | ||
var shrunkFontSize = if (maxFontSize.isSpecified) maxFontSize else 100.sp | ||
|
||
val calculateIntrinsics = @Composable { | ||
val mergedStyle = style.merge( | ||
TextStyle( | ||
color = color, | ||
fontSize = shrunkFontSize, | ||
fontWeight = fontWeight, | ||
textAlign = textAlign ?: TextAlign.Unspecified, | ||
lineHeight = lineHeight, | ||
fontFamily = fontFamily, | ||
textDecoration = textDecoration, | ||
fontStyle = fontStyle, | ||
letterSpacing = letterSpacing, | ||
), | ||
) | ||
Paragraph( | ||
text = text, | ||
style = mergedStyle, | ||
constraints = Constraints(maxWidth = ceil(LocalDensity.current.run { maxWidth.toPx() }).toInt()), | ||
density = LocalDensity.current, | ||
fontFamilyResolver = LocalFontFamilyResolver.current, | ||
spanStyles = listOf(), | ||
placeholders = listOf(), | ||
maxLines = maxLines, | ||
ellipsis = false, | ||
) | ||
} | ||
|
||
var intrinsics = calculateIntrinsics() | ||
|
||
val targetWidth = maxWidth - acceptableError / 2f | ||
|
||
check(targetWidth.isFinite || maxFontSize.isSpecified) { | ||
"maxFontSize must be specified if the target with isn't finite!" | ||
} | ||
|
||
with(LocalDensity.current) { | ||
// this loop will attempt to quickly find the correct size font by scaling it by the error | ||
// it only runs if the max font size isn't specified or the font must be smaller | ||
// minIntrinsicWidth is "The width for text if all soft wrap opportunities were taken." | ||
if (maxFontSize.isUnspecified || targetWidth < intrinsics.minIntrinsicWidth.toDp()) { | ||
while ((targetWidth - intrinsics.minIntrinsicWidth.toDp()).toPx().absoluteValue.toDp() > acceptableError / 2f) { | ||
shrunkFontSize *= targetWidth.toPx() / intrinsics.minIntrinsicWidth | ||
intrinsics = calculateIntrinsics() | ||
} | ||
} | ||
// checks if the text fits in the bounds and scales it by 90% until it does | ||
while (intrinsics.didExceedMaxLines || maxHeight < intrinsics.height.toDp() || maxWidth < intrinsics.minIntrinsicWidth.toDp()) { | ||
shrunkFontSize *= 0.9f | ||
intrinsics = calculateIntrinsics() | ||
} | ||
} | ||
|
||
if (maxFontSize.isSpecified && shrunkFontSize > maxFontSize) { | ||
shrunkFontSize = maxFontSize | ||
} | ||
|
||
Text( | ||
text = text, | ||
color = color, | ||
fontSize = shrunkFontSize, | ||
fontStyle = fontStyle, | ||
fontWeight = fontWeight, | ||
fontFamily = fontFamily, | ||
letterSpacing = letterSpacing, | ||
textDecoration = textDecoration, | ||
textAlign = textAlign, | ||
lineHeight = lineHeight, | ||
onTextLayout = onTextLayout, | ||
maxLines = maxLines, | ||
style = style, | ||
) | ||
} | ||
} |
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
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