LoginSignup
6
1

More than 1 year has passed since last update.

【SwiftUI/JetpackCompose】文字サイズの自動調整 ~比較で覚える宣言的モバイルUI~

Last updated at Posted at 2022-10-29

SwiftUIとJetpackComposeで、同セグメントの機能を両フレームワークでつくろうとした時に

  • きれいに対応関係がまとまっている
  • コピペで動く
  • 最もシンプルな実装

そんな記事があったらいいなと思い、備忘録も兼ねて自分が実装してみた範囲でまとめていきたいと思います。初心者ですので、より良い実装をご存知の方がいらっしゃいましたら、ご教示ください。

今回は、文字サイズの自動調整編です。

なお、見た目をSwiftUI寄りにしがちです

対応関係

【SwiftUI】minimumScaleFactor(_:)修飾子をつける
【JetpackCompose】(現状公式のものはないので)自作のTextコンポーザブルを作成する

SwiftUI

SwiftUIでは、Textビューに.minimumScaleFactor(最小サイズの倍率)という修飾子をつけることで実現できます。

minimumScaleFactor(_:)

公式の定義】

func minimumScaleFactor(_ factor: CGFloat) -> some View

【使い方】
引数には、縮小する倍率の最小値を入れます。0.5を入れた場合、0.5倍のサイズまでは縮小され、それ以下になると省略されます。

Text("Hello, World!")
    .minimumScaleFactor(0.5) // 0.5倍のサイズまで縮小され、
                             // それより小さい場合は文末が「...」になる

具体例

.lineLimit(1)にすることで、行数を1行に限定しています。

struct MinimumScaleFactorExample: View {
    var body: some View {
        VStack(spacing: 20) {
            Text("Hello, World!")
                .font(.largeTitle)
                .lineLimit(1)
                .minimumScaleFactor(0.5)
            
            Text("Hello, World!")
                .font(.largeTitle)
                .lineLimit(1)
                .minimumScaleFactor(0.5)
                .frame(width: 160)
            
            Text("Hello, World!")
                .font(.largeTitle)
                .lineLimit(1)
                .minimumScaleFactor(0.5)
                .frame(width: 80)
        }
    }
}

実行結果

きちんと文字が縮小されています。指定した最小倍率以下になると、文末が「...」になっています。
スクリーンショット 2022-10-29 17.27.39.png

Jetpack Compose

Jetpack Composeはまだ登場してから日が浅いこともあり、まだ公式のものはなく、自作する必要があります。

自作する

こちらのStackOverflowの記事を参考に、若干改造させていただきました。

名前は任意ですが、表示時にサイズを調節する、Textをラップした以下のようなコンポーザブルを作ります。
ポイントはonTextLayoutという引数で、これにより文字のレイアウトが計算されたときに、コールバックを実行してくれます。
もう一つのポイントはModifierのdrawWithContent修飾子で、この中でdrawContent()をBooleanで分岐させることで、計算が終わって初めて表示されるようになります。

AutoResizeTextコンポーザブル
AutoResizeText.kt
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.graphics.Color
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.text.style.TextOverflow
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.sp

@Composable
fun AutoResizeText(
    modifier: Modifier = Modifier,
    text: String,
    fontSizeRange: FontSizeRange,
    color: Color = Color.Unspecified,
    maxLines: Int = Int.MAX_VALUE,
    fontStyle: FontStyle? = null,
    fontWeight: FontWeight? = null,
    fontFamily: FontFamily? = null,
    letterSpacing: TextUnit = TextUnit.Unspecified,
    textDecoration: TextDecoration? = null,
    textAlign: TextAlign? = null,
    lineHeight: TextUnit = TextUnit.Unspecified,
    overflow: TextOverflow = TextOverflow.Ellipsis, // あふれた場合は省略する
    softWrap: Boolean = true,
    style: TextStyle = LocalTextStyle.current,
) {
    var fontSizeValue by remember { mutableStateOf(fontSizeRange.max.value) }
    var readyToDraw by remember { mutableStateOf(false) }

    Text(
        text = text,
        color = color,
        maxLines = maxLines,
        fontStyle = fontStyle,
        fontWeight = fontWeight,
        fontFamily = fontFamily,
        letterSpacing = letterSpacing,
        textDecoration = textDecoration,
        textAlign = textAlign,
        lineHeight = lineHeight,
        overflow = overflow,
        softWrap = softWrap,
        style = style,
        fontSize = fontSizeValue.sp,
        onTextLayout = {
            if (it.didOverflowHeight && !readyToDraw) {
                val nextFontSizeValue = fontSizeValue - fontSizeRange.step.value
                if (nextFontSizeValue <= fontSizeRange.min.value) {
                    // 最小サイズに到達した場合、縮小を終了する。
                    fontSizeValue = fontSizeRange.min.value
                    readyToDraw = true
                } else {
                    // まだ領域より大きく、最小サイズに達していない場合は縮小を続ける
                    fontSizeValue = nextFontSizeValue
                }
            } else {
                // 最適なサイズに達した時にことを知らせる
                readyToDraw = true
            }
        },
        modifier = modifier.drawWithContent { if (readyToDraw) drawContent() }
    )
}

data class FontSizeRange(
    val min: TextUnit,
    val max: TextUnit,
    val step: TextUnit = DEFAULT_TEXT_STEP,
) {
    companion object {
        private val DEFAULT_TEXT_STEP = 1.sp
    }
}

【使い方】
文字サイズの最小値と最大値をそれぞれ指定します。

AutoResizeText(
    text = "Hello World!",
    fontSizeRange = FontSizeRange(min = 20.sp, max = 40.sp)
)

具体例

maxLines = 1にすることで、行数を1行に限定しています。

@Composable
fun AutoResizeTextExample (
    modifier: Modifier = Modifier
) {
    Box(
        modifier = modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        Column(
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.spacedBy(20.dp)
        ) {
            AutoResizeText(
                text = "Hello World!",
                maxLines = 1,
                fontSizeRange = FontSizeRange(min = 20.sp, max = 40.sp)
            )

            AutoResizeText(
                modifier = Modifier.width(160.dp),
                text = "Hello World!",
                maxLines = 1,
                fontSizeRange = FontSizeRange(min = 20.sp, max = 40.sp)
            )

            AutoResizeText(
                modifier = Modifier.width(80.dp),
                text = "Hello World!",
                maxLines = 1,
                fontSizeRange = FontSizeRange(min = 20.sp, max = 40.sp)
            )
        }
    }
}

実行結果

こちらでも、きちんと文字が縮小されています。指定した最小サイズ以下になると、文末が「...」になります。(Textの引数にoverflow = TextOverflow.Ellipsisを指定しているため)
なお、プレビューはInteractive Modeでないとうまく表示されません。
スクリーンショット 2022-10-29 19.25.05.png

まとめ

文字サイズの自動調整

対応関係を再掲します。
【SwiftUI】minimumScaleFactor(_:)修飾子をつける
【JetpackCompose】(現状公式のものはないので)自作のTextコンポーザブルを作成する

Jetpack Composeでは、現時点では自作する必要があるとはいえ、TextコンポーザブルのonTextLayoutという引数のおかげで、ちゃんと実装できるようになっているのがありがたいです。

参考

【SwiftUI】
【SwiftUI】Textの使い方 by カピ通信
minimumscalefactor by Apple公式

【Jetpack Compose】
android:autoSizeTextType in Jetpack Compose by StackOverflow
Compose 修飾子のリスト by AndroidDeveloper
Jetpack Compose のフェーズ by AndroidDeveloper
JETPACK COMPOSE: TEXTコンポーザブルを使う by TECHBOOSTER

6
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
1