From 50e7cdbeb58c0fb4109426884d6789c79dbf057c Mon Sep 17 00:00:00 2001 From: Marcel Schnelle Date: Mon, 2 Sep 2024 18:45:28 +0900 Subject: [PATCH 1/8] Introduce AutoSizeText component & use it for the top bar title of all main screens --- .../designsystem/component/AutoSizeText.kt | 94 +++++++++++++++++++ .../component/AnimatedTextTopAppBar.kt | 8 +- .../confsched/sessions/TimetableScreen.kt | 14 ++- 3 files changed, 105 insertions(+), 11 deletions(-) create mode 100644 core/designsystem/src/commonMain/kotlin/io/github/droidkaigi/confsched/designsystem/component/AutoSizeText.kt diff --git a/core/designsystem/src/commonMain/kotlin/io/github/droidkaigi/confsched/designsystem/component/AutoSizeText.kt b/core/designsystem/src/commonMain/kotlin/io/github/droidkaigi/confsched/designsystem/component/AutoSizeText.kt new file mode 100644 index 000000000..f64d6132b --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/io/github/droidkaigi/confsched/designsystem/component/AutoSizeText.kt @@ -0,0 +1,94 @@ +package io.github.droidkaigi.confsched.designsystem.component + +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.BoxWithConstraintsScope +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.TextStyle +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.TextUnit +import kotlin.math.ceil + +/** + * A [Text] component that automatically downsizes its font size to fit its content, + * with the upper bound being defined by the font size of its [style]. + */ +@Composable +fun AutoSizeText( + text: String, + modifier: Modifier = Modifier, + textAlign: TextAlign = TextAlign.Unspecified, + maxLines: Int = Int.MAX_VALUE, + style: TextStyle = LocalTextStyle.current, + color: Color = Color.Unspecified, +) { + BoxWithConstraints( + modifier = modifier, + contentAlignment = when (textAlign) { + TextAlign.Left, TextAlign.Start -> Alignment.CenterStart + TextAlign.Center -> Alignment.Center + TextAlign.Right, TextAlign.End -> Alignment.CenterEnd + TextAlign.Justify -> Alignment.Center + else -> Alignment.CenterStart + }, + ) { + Text( + text = text, + color = color, + fontSize = calculateFontSize(text, style, color, textAlign, maxLines), + style = style, + maxLines = maxLines, + textAlign = textAlign, + ) + } +} + +@Composable +private fun BoxWithConstraintsScope.calculateFontSize( + text: String, + style: TextStyle, + color: Color, + textAlign: TextAlign, + maxLines: Int, +): TextUnit = with(LocalDensity.current) { + // Upper bound of the font size, will decrease in the `while` loop below + var targetFontSize = style.fontSize + + // Calculate the text layout using the current `targetFontSize` + val calculateParagraph = @Composable { + val finalStyle = style.merge( + TextStyle( + color = color, + fontSize = targetFontSize, + textAlign = textAlign, + ), + ) + + Paragraph( + text = text, + style = finalStyle, + maxLines = maxLines, + constraints = Constraints(maxWidth = ceil(maxWidth.toPx()).toInt()), + density = this, + fontFamilyResolver = LocalFontFamilyResolver.current, + ) + } + + var paragraph = calculateParagraph() + + // Keep decreasing the font size until the text fits in the box without exceeding the max lines + while (paragraph.didExceedMaxLines || maxHeight < paragraph.height.toDp() || maxWidth < paragraph.minIntrinsicWidth.toDp()) { + targetFontSize *= 0.95 + paragraph = calculateParagraph() + } + + targetFontSize +} diff --git a/core/droidkaigiui/src/commonMain/kotlin/io/github/droidkaigi/confsched/droidkaigiui/component/AnimatedTextTopAppBar.kt b/core/droidkaigiui/src/commonMain/kotlin/io/github/droidkaigi/confsched/droidkaigiui/component/AnimatedTextTopAppBar.kt index 6075e6e1b..1c781edf4 100644 --- a/core/droidkaigiui/src/commonMain/kotlin/io/github/droidkaigi/confsched/droidkaigiui/component/AnimatedTextTopAppBar.kt +++ b/core/droidkaigiui/src/commonMain/kotlin/io/github/droidkaigi/confsched/droidkaigiui/component/AnimatedTextTopAppBar.kt @@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarColors import androidx.compose.material3.TopAppBarDefaults @@ -17,6 +16,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.text.style.TextAlign +import io.github.droidkaigi.confsched.designsystem.component.AutoSizeText @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -38,7 +38,7 @@ fun AnimatedTextTopAppBar( TopAppBar( title = { Box(modifier = Modifier.fillMaxWidth()) { - Text( + AutoSizeText( text = title, color = textColor, modifier = Modifier @@ -47,10 +47,11 @@ fun AnimatedTextTopAppBar( alpha = 1f - transitionFraction }, textAlign = TextAlign.Start, + maxLines = 1, style = MaterialTheme.typography.headlineSmall, ) - Text( + AutoSizeText( text = title, color = textColor, modifier = Modifier @@ -60,6 +61,7 @@ fun AnimatedTextTopAppBar( alpha = transitionFraction }, textAlign = TextAlign.Center, + maxLines = 1, style = MaterialTheme.typography.titleMedium, ) } diff --git a/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/TimetableScreen.kt b/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/TimetableScreen.kt index 49c349b89..410fdb1a1 100644 --- a/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/TimetableScreen.kt +++ b/feature/sessions/src/commonMain/kotlin/io/github/droidkaigi/confsched/sessions/TimetableScreen.kt @@ -18,10 +18,10 @@ import androidx.compose.material.icons.filled.Search import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -31,9 +31,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.testTag -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.dropUnlessResumed import androidx.navigation.NavController import androidx.navigation.NavGraph.Companion.findStartDestination @@ -47,6 +45,7 @@ import conference_app_2024.feature.sessions.generated.resources.timeline_view import conference_app_2024.feature.sessions.generated.resources.timetable import io.github.droidkaigi.confsched.compose.EventFlow import io.github.droidkaigi.confsched.compose.rememberEventFlow +import io.github.droidkaigi.confsched.designsystem.component.AutoSizeText import io.github.droidkaigi.confsched.designsystem.theme.KaigiTheme import io.github.droidkaigi.confsched.droidkaigiui.SnackbarMessageEffect import io.github.droidkaigi.confsched.droidkaigiui.UserMessageStateHolder @@ -158,12 +157,11 @@ private fun TimetableScreen( Row( verticalAlignment = Alignment.CenterVertically, ) { - Text( + AutoSizeText( + modifier = Modifier.weight(1f), text = stringResource(SessionsRes.string.timetable), - fontSize = 24.sp, - lineHeight = 32.sp, - fontWeight = FontWeight.W400, - modifier = Modifier.weight(1F), + style = MaterialTheme.typography.headlineSmall, + maxLines = 1, ) IconButton( onClick = dropUnlessResumed(block = onSearchClick), From 0e3f70f2f4b289ca75cc0b4fa2bfd3b8056f2eb6 Mon Sep 17 00:00:00 2001 From: Marcel Schnelle Date: Sat, 31 Aug 2024 08:37:27 +0900 Subject: [PATCH 2/8] Apply AutoSizeText to other title bars in the app --- .../droidkaigiui/component/AnimatedLargeTopAppBar.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/droidkaigiui/src/commonMain/kotlin/io/github/droidkaigi/confsched/droidkaigiui/component/AnimatedLargeTopAppBar.kt b/core/droidkaigiui/src/commonMain/kotlin/io/github/droidkaigi/confsched/droidkaigiui/component/AnimatedLargeTopAppBar.kt index fd5271c20..55db9a3d1 100644 --- a/core/droidkaigiui/src/commonMain/kotlin/io/github/droidkaigi/confsched/droidkaigiui/component/AnimatedLargeTopAppBar.kt +++ b/core/droidkaigiui/src/commonMain/kotlin/io/github/droidkaigi/confsched/droidkaigiui/component/AnimatedLargeTopAppBar.kt @@ -14,7 +14,6 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LargeTopAppBar import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarColors import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior @@ -29,6 +28,7 @@ import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import io.github.droidkaigi.confsched.designsystem.component.AutoSizeText @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -68,7 +68,7 @@ fun AnimatedLargeTopAppBar( // No animation required as it is erased with alpha exit = ExitTransition.None, ) { - Text( + AutoSizeText( text = title, modifier = Modifier.then( when (isCenterTitle) { @@ -82,6 +82,7 @@ fun AnimatedLargeTopAppBar( }, ), textAlign = TextAlign.Center, + maxLines = 1, ) } }, From 1d3ecb6419fabc74763405795869e0546e7acddb Mon Sep 17 00:00:00 2001 From: Marcel Schnelle Date: Mon, 2 Sep 2024 21:11:25 +0900 Subject: [PATCH 3/8] Apply AutoTextSize to the new AnimatedMediumTopAppBar --- .../droidkaigiui/component/AnimatedMediumTopAppBar.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/droidkaigiui/src/commonMain/kotlin/io/github/droidkaigi/confsched/droidkaigiui/component/AnimatedMediumTopAppBar.kt b/core/droidkaigiui/src/commonMain/kotlin/io/github/droidkaigi/confsched/droidkaigiui/component/AnimatedMediumTopAppBar.kt index 86635b829..6c6080814 100644 --- a/core/droidkaigiui/src/commonMain/kotlin/io/github/droidkaigi/confsched/droidkaigiui/component/AnimatedMediumTopAppBar.kt +++ b/core/droidkaigiui/src/commonMain/kotlin/io/github/droidkaigi/confsched/droidkaigiui/component/AnimatedMediumTopAppBar.kt @@ -14,7 +14,6 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MediumTopAppBar -import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarColors import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior @@ -29,6 +28,7 @@ import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import io.github.droidkaigi.confsched.designsystem.component.AutoSizeText @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -68,7 +68,7 @@ fun AnimatedMediumTopAppBar( // No animation required as it is erased with alpha exit = ExitTransition.None, ) { - Text( + AutoSizeText( text = title, modifier = Modifier.then( when (isCenterTitle) { @@ -77,11 +77,13 @@ fun AnimatedMediumTopAppBar( .padding(end = navigationIconWidthDp.dp) .fillMaxWidth() } + false -> Modifier null -> Modifier.alpha(0f) }, ), textAlign = TextAlign.Center, + maxLines = 1, ) } }, From a01e906e40e4f5f0219e009984e24a90df622f7d Mon Sep 17 00:00:00 2001 From: Marcel Schnelle Date: Tue, 3 Sep 2024 19:36:11 +0900 Subject: [PATCH 4/8] Find the best font size for AutoTextSize using binary search, if needed --- .../designsystem/component/AutoSizeText.kt | 68 +++++++++++++------ 1 file changed, 47 insertions(+), 21 deletions(-) diff --git a/core/designsystem/src/commonMain/kotlin/io/github/droidkaigi/confsched/designsystem/component/AutoSizeText.kt b/core/designsystem/src/commonMain/kotlin/io/github/droidkaigi/confsched/designsystem/component/AutoSizeText.kt index f64d6132b..0cbca1919 100644 --- a/core/designsystem/src/commonMain/kotlin/io/github/droidkaigi/confsched/designsystem/component/AutoSizeText.kt +++ b/core/designsystem/src/commonMain/kotlin/io/github/droidkaigi/confsched/designsystem/component/AutoSizeText.kt @@ -15,6 +15,9 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.TextUnitType +import androidx.compose.ui.unit.em +import androidx.compose.ui.unit.sp import kotlin.math.ceil /** @@ -58,37 +61,60 @@ private fun BoxWithConstraintsScope.calculateFontSize( color: Color, textAlign: TextAlign, maxLines: Int, -): TextUnit = with(LocalDensity.current) { - // Upper bound of the font size, will decrease in the `while` loop below - var targetFontSize = style.fontSize - - // Calculate the text layout using the current `targetFontSize` - val calculateParagraph = @Composable { +): TextUnit { + // Helper function to calculate if a given text size + // would cause an overflow when placed into this BoxWithConstraints + val hasOverflowWhenPlaced: @Composable TextUnit.() -> Boolean = { val finalStyle = style.merge( TextStyle( color = color, - fontSize = targetFontSize, + fontSize = this, textAlign = textAlign, ), ) - Paragraph( - text = text, - style = finalStyle, - maxLines = maxLines, - constraints = Constraints(maxWidth = ceil(maxWidth.toPx()).toInt()), - density = this, - fontFamilyResolver = LocalFontFamilyResolver.current, - ) + with(LocalDensity.current) { + Paragraph( + text = text, + style = finalStyle, + maxLines = maxLines, + constraints = Constraints(maxWidth = ceil(maxWidth.toPx()).toInt()), + density = this, + fontFamilyResolver = LocalFontFamilyResolver.current, + ).run { + didExceedMaxLines || maxHeight < height.toDp() || maxWidth < minIntrinsicWidth.toDp() + } + } } - var paragraph = calculateParagraph() + // If the original text size fits already without overflowing, + // then there is no need to do anything + if (!style.fontSize.hasOverflowWhenPlaced()) { + return style.fontSize + } + + // Otherwise, find the biggest font size that still fits using binary search + var lo = 1 + var hi = style.fontSize.value.toInt() + val type = style.fontSize.type - // Keep decreasing the font size until the text fits in the box without exceeding the max lines - while (paragraph.didExceedMaxLines || maxHeight < paragraph.height.toDp() || maxWidth < paragraph.minIntrinsicWidth.toDp()) { - targetFontSize *= 0.95 - paragraph = calculateParagraph() + while (lo <= hi) { + val mid = lo + (hi - lo) / 2 + if (mid.asTextUnit(type).hasOverflowWhenPlaced()) { + hi = mid - 1 + } else { + lo = mid + 1 + } } - targetFontSize + // After the binary search, the right pointer is the largest size + // that still works without overflowing the box' + return hi.asTextUnit(type) +} + +private fun Int.asTextUnit(type: TextUnitType) = when (type) { + TextUnitType.Sp -> this.sp + TextUnitType.Em -> this.em + TextUnitType.Unspecified -> TextUnit.Unspecified + else -> error("Invalid TextUnitType: $type") } From 9c82a2e7787d1ce9d22d92662627740a1798e22e Mon Sep 17 00:00:00 2001 From: Marcel Schnelle Date: Tue, 3 Sep 2024 20:50:24 +0900 Subject: [PATCH 5/8] Add font scale test for AutoSizeText to timetable screen This required a new type of matcher for asserting the line count of a text node --- .../designsystem/component/AutoSizeText.kt | 3 ++- .../testing/robot/TimetableScreenRobot.kt | 12 +++++++++++- .../confsched/testing/utils/Matchers.kt | 19 +++++++++++++++++++ .../confsched/sessions/TimetableScreenTest.kt | 12 ++++++++++++ .../confsched/sessions/TimetableScreen.kt | 3 ++- 5 files changed, 46 insertions(+), 3 deletions(-) diff --git a/core/designsystem/src/commonMain/kotlin/io/github/droidkaigi/confsched/designsystem/component/AutoSizeText.kt b/core/designsystem/src/commonMain/kotlin/io/github/droidkaigi/confsched/designsystem/component/AutoSizeText.kt index 0cbca1919..063046646 100644 --- a/core/designsystem/src/commonMain/kotlin/io/github/droidkaigi/confsched/designsystem/component/AutoSizeText.kt +++ b/core/designsystem/src/commonMain/kotlin/io/github/droidkaigi/confsched/designsystem/component/AutoSizeText.kt @@ -10,6 +10,7 @@ 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.semantics.semantics import androidx.compose.ui.text.Paragraph import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextAlign @@ -34,7 +35,7 @@ fun AutoSizeText( color: Color = Color.Unspecified, ) { BoxWithConstraints( - modifier = modifier, + modifier = modifier.semantics(mergeDescendants = true) {}, contentAlignment = when (textAlign) { TextAlign.Left, TextAlign.Start -> Alignment.CenterStart TextAlign.Center -> Alignment.Center diff --git a/core/testing/src/main/java/io/github/droidkaigi/confsched/testing/robot/TimetableScreenRobot.kt b/core/testing/src/main/java/io/github/droidkaigi/confsched/testing/robot/TimetableScreenRobot.kt index 80da65f33..ac886e908 100644 --- a/core/testing/src/main/java/io/github/droidkaigi/confsched/testing/robot/TimetableScreenRobot.kt +++ b/core/testing/src/main/java/io/github/droidkaigi/confsched/testing/robot/TimetableScreenRobot.kt @@ -27,11 +27,13 @@ import io.github.droidkaigi.confsched.model.DroidKaigi2024Day import io.github.droidkaigi.confsched.model.TimetableItem import io.github.droidkaigi.confsched.sessions.TimetableScreen import io.github.droidkaigi.confsched.sessions.TimetableScreenTestTag +import io.github.droidkaigi.confsched.sessions.TimetableTitleTestTag import io.github.droidkaigi.confsched.sessions.TimetableUiTypeChangeButtonTestTag import io.github.droidkaigi.confsched.sessions.component.TimetableGridItemTestTag import io.github.droidkaigi.confsched.sessions.section.TimetableGridTestTag import io.github.droidkaigi.confsched.sessions.section.TimetableListTestTag import io.github.droidkaigi.confsched.sessions.section.TimetableTabTestTag +import io.github.droidkaigi.confsched.testing.utils.assertLineCount import kotlinx.datetime.LocalDateTime import kotlinx.datetime.TimeZone import kotlinx.datetime.toInstant @@ -41,9 +43,11 @@ class TimetableScreenRobot @Inject constructor( private val screenRobot: DefaultScreenRobot, private val timetableServerRobot: DefaultTimetableServerRobot, private val deviceSetupRobot: DefaultDeviceSetupRobot, + fontScaleRobot: DefaultFontScaleRobot, ) : ScreenRobot by screenRobot, TimetableServerRobot by timetableServerRobot, - DeviceSetupRobot by deviceSetupRobot { + DeviceSetupRobot by deviceSetupRobot, + FontScaleRobot by fontScaleRobot { val clickedItems = mutableSetOf() fun setupTimetableScreenContent(customTime: LocalDateTime? = null) { @@ -68,6 +72,12 @@ class TimetableScreenRobot @Inject constructor( waitUntilIdle() } + fun checkTitleDisplayedInSingleLine() { + composeTestRule + .onNode(hasTestTag(TimetableTitleTestTag)) + .assertLineCount(1) + } + fun clickFirstSession() { composeTestRule .onAllNodes(hasTestTag(TimetableItemCardTestTag)) diff --git a/core/testing/src/main/java/io/github/droidkaigi/confsched/testing/utils/Matchers.kt b/core/testing/src/main/java/io/github/droidkaigi/confsched/testing/utils/Matchers.kt index 5d612613c..3ad7ceb7a 100644 --- a/core/testing/src/main/java/io/github/droidkaigi/confsched/testing/utils/Matchers.kt +++ b/core/testing/src/main/java/io/github/droidkaigi/confsched/testing/utils/Matchers.kt @@ -1,12 +1,15 @@ package io.github.droidkaigi.confsched.testing.utils +import androidx.compose.ui.semantics.SemanticsActions import androidx.compose.ui.semantics.SemanticsNode import androidx.compose.ui.semantics.SemanticsProperties import androidx.compose.ui.semantics.getOrNull import androidx.compose.ui.test.SemanticsMatcher import androidx.compose.ui.test.SemanticsNodeInteraction import androidx.compose.ui.test.SemanticsNodeInteractionCollection +import androidx.compose.ui.text.TextLayoutResult import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue fun hasTestTag( testTag: String, @@ -66,6 +69,22 @@ fun SemanticsNodeInteraction.assertTextDoesNotContain( } } +fun SemanticsNodeInteraction.assertLineCount(expectedCount: Int) { + fetchSemanticsNode() + .let { node -> + val results = mutableListOf() + node.config.getOrNull(SemanticsActions.GetTextLayoutResult) + ?.action + ?.invoke(results) + val result = results.firstOrNull() + + assertTrue( + "Node has unexpected line count (expected $expectedCount, but was ${result?.lineCount})", + result?.lineCount == expectedCount, + ) + } +} + private fun buildErrorMessageForMinimumCountMismatch( errorMessage: String, foundNodes: List, diff --git a/feature/sessions/src/androidUnitTest/kotlin/io/github/droidkaigi/confsched/sessions/TimetableScreenTest.kt b/feature/sessions/src/androidUnitTest/kotlin/io/github/droidkaigi/confsched/sessions/TimetableScreenTest.kt index 1beee51f1..bcc315cec 100644 --- a/feature/sessions/src/androidUnitTest/kotlin/io/github/droidkaigi/confsched/sessions/TimetableScreenTest.kt +++ b/feature/sessions/src/androidUnitTest/kotlin/io/github/droidkaigi/confsched/sessions/TimetableScreenTest.kt @@ -128,6 +128,18 @@ class TimetableScreenTest(private val testCase: DescribedBehavior Unit, @@ -158,7 +159,7 @@ private fun TimetableScreen( verticalAlignment = Alignment.CenterVertically, ) { AutoSizeText( - modifier = Modifier.weight(1f), + modifier = Modifier.testTag(TimetableTitleTestTag).weight(1f), text = stringResource(SessionsRes.string.timetable), style = MaterialTheme.typography.headlineSmall, maxLines = 1, From 9b7456b959624566d29badfbca05e40766938c74 Mon Sep 17 00:00:00 2001 From: Marcel Schnelle Date: Tue, 3 Sep 2024 20:53:35 +0900 Subject: [PATCH 6/8] Small comment fix --- .../droidkaigi/confsched/designsystem/component/AutoSizeText.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/designsystem/src/commonMain/kotlin/io/github/droidkaigi/confsched/designsystem/component/AutoSizeText.kt b/core/designsystem/src/commonMain/kotlin/io/github/droidkaigi/confsched/designsystem/component/AutoSizeText.kt index 063046646..80933d7e9 100644 --- a/core/designsystem/src/commonMain/kotlin/io/github/droidkaigi/confsched/designsystem/component/AutoSizeText.kt +++ b/core/designsystem/src/commonMain/kotlin/io/github/droidkaigi/confsched/designsystem/component/AutoSizeText.kt @@ -109,7 +109,7 @@ private fun BoxWithConstraintsScope.calculateFontSize( } // After the binary search, the right pointer is the largest size - // that still works without overflowing the box' + // that still works without overflowing the box return hi.asTextUnit(type) } From be532bb4a0380db939a5b00aadb65762b4faa19b Mon Sep 17 00:00:00 2001 From: Marcel Schnelle Date: Wed, 4 Sep 2024 19:24:16 +0900 Subject: [PATCH 7/8] Remember the calculated font size to avoid unnecessary recalculation --- .../designsystem/component/AutoSizeText.kt | 90 ++++++++++--------- 1 file changed, 48 insertions(+), 42 deletions(-) diff --git a/core/designsystem/src/commonMain/kotlin/io/github/droidkaigi/confsched/designsystem/component/AutoSizeText.kt b/core/designsystem/src/commonMain/kotlin/io/github/droidkaigi/confsched/designsystem/component/AutoSizeText.kt index 80933d7e9..ad25fea6d 100644 --- a/core/designsystem/src/commonMain/kotlin/io/github/droidkaigi/confsched/designsystem/component/AutoSizeText.kt +++ b/core/designsystem/src/commonMain/kotlin/io/github/droidkaigi/confsched/designsystem/component/AutoSizeText.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.BoxWithConstraintsScope import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -47,7 +48,7 @@ fun AutoSizeText( Text( text = text, color = color, - fontSize = calculateFontSize(text, style, color, textAlign, maxLines), + fontSize = rememberFontSize(text, style, color, textAlign, maxLines), style = style, maxLines = maxLines, textAlign = textAlign, @@ -56,61 +57,66 @@ fun AutoSizeText( } @Composable -private fun BoxWithConstraintsScope.calculateFontSize( +private fun BoxWithConstraintsScope.rememberFontSize( text: String, style: TextStyle, color: Color, textAlign: TextAlign, maxLines: Int, ): TextUnit { - // Helper function to calculate if a given text size - // would cause an overflow when placed into this BoxWithConstraints - val hasOverflowWhenPlaced: @Composable TextUnit.() -> Boolean = { - val finalStyle = style.merge( - TextStyle( - color = color, - fontSize = this, - textAlign = textAlign, - ), - ) + val density = LocalDensity.current + val fontFamilyResolver = LocalFontFamilyResolver.current + + return remember(text, style.fontSize, textAlign, maxLines, density, maxWidth, maxHeight) { + // Helper function to calculate if a given text size + // would cause an overflow when placed into this BoxWithConstraints + val hasOverflowWhenPlaced: TextUnit.() -> Boolean = { + val finalStyle = style.merge( + TextStyle( + color = color, + fontSize = this, + textAlign = textAlign, + ), + ) - with(LocalDensity.current) { - Paragraph( - text = text, - style = finalStyle, - maxLines = maxLines, - constraints = Constraints(maxWidth = ceil(maxWidth.toPx()).toInt()), - density = this, - fontFamilyResolver = LocalFontFamilyResolver.current, - ).run { - didExceedMaxLines || maxHeight < height.toDp() || maxWidth < minIntrinsicWidth.toDp() + with(density) { + Paragraph( + text = text, + style = finalStyle, + maxLines = maxLines, + constraints = Constraints(maxWidth = ceil(maxWidth.toPx()).toInt()), + density = this, + fontFamilyResolver = fontFamilyResolver, + ).run { + didExceedMaxLines || maxHeight < height.toDp() || maxWidth < minIntrinsicWidth.toDp() + } } } - } - // If the original text size fits already without overflowing, - // then there is no need to do anything - if (!style.fontSize.hasOverflowWhenPlaced()) { - return style.fontSize - } + // If the original text size fits already without overflowing, + // then there is no need to do anything + if (!style.fontSize.hasOverflowWhenPlaced()) { + style.fontSize + } - // Otherwise, find the biggest font size that still fits using binary search - var lo = 1 - var hi = style.fontSize.value.toInt() - val type = style.fontSize.type + // Otherwise, find the biggest font size that still fits using binary search + var lo = 1 + var hi = style.fontSize.value.toInt() + val type = style.fontSize.type - while (lo <= hi) { - val mid = lo + (hi - lo) / 2 - if (mid.asTextUnit(type).hasOverflowWhenPlaced()) { - hi = mid - 1 - } else { - lo = mid + 1 + while (lo <= hi) { + val mid = lo + (hi - lo) / 2 + if (mid.asTextUnit(type).hasOverflowWhenPlaced()) { + hi = mid - 1 + } else { + lo = mid + 1 + } } - } - // After the binary search, the right pointer is the largest size - // that still works without overflowing the box - return hi.asTextUnit(type) + // After the binary search, the right pointer is the largest size + // that still works without overflowing the box + hi.asTextUnit(type) + } } private fun Int.asTextUnit(type: TextUnitType) = when (type) { From d0ad0ffdaf88c1620bdfd664ae7f860d794392b4 Mon Sep 17 00:00:00 2001 From: Marcel Schnelle Date: Thu, 5 Sep 2024 00:50:37 +0200 Subject: [PATCH 8/8] Update core/designsystem/src/commonMain/kotlin/io/github/droidkaigi/confsched/designsystem/component/AutoSizeText.kt Co-authored-by: Takahiro Menju --- .../droidkaigi/confsched/designsystem/component/AutoSizeText.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/designsystem/src/commonMain/kotlin/io/github/droidkaigi/confsched/designsystem/component/AutoSizeText.kt b/core/designsystem/src/commonMain/kotlin/io/github/droidkaigi/confsched/designsystem/component/AutoSizeText.kt index ad25fea6d..20d773e33 100644 --- a/core/designsystem/src/commonMain/kotlin/io/github/droidkaigi/confsched/designsystem/component/AutoSizeText.kt +++ b/core/designsystem/src/commonMain/kotlin/io/github/droidkaigi/confsched/designsystem/component/AutoSizeText.kt @@ -96,7 +96,7 @@ private fun BoxWithConstraintsScope.rememberFontSize( // If the original text size fits already without overflowing, // then there is no need to do anything if (!style.fontSize.hasOverflowWhenPlaced()) { - style.fontSize + return@remember style.fontSize } // Otherwise, find the biggest font size that still fits using binary search