こんにちは。Android アプリ開発担当の nagayama(@nagayan_dev)です。
今回は Jetpack Compose で破線付きテキストの作り方をご紹介します。

はじめに
テキストに線を加える表現方法にはさまざまな種類があります。強調したい場合は「下線」を、変更履歴を示したい場合は「取消線」を使うことが一般的です。その中でも、軽い強調として「破線」を加えたテキスト表現があります。今回は、この「破線付きのテキスト」をJetpack Composeで実現する方法をご紹介します。
下線付きテキストの作り方
まずは下線付きテキストについて解説します。
Text の引数で TextDecoration を指定することができます。TextDecoration は Text と一緒に描画する横線を設定するものになります。種類としては 下線 (Underline) と 取消線 (LineThrough) が用意されており、下記のように引数に指定するだけで簡単に描画することができます。
Column {
Text(
text = "下線",
textDecoration = TextDecoration.Underline,
)
Text(
text = "取り消し線",
textDecoration = TextDecoration.LineThrough,
)
}

破線付きテキストの作り方
では本題の破線付きテキストについてです。
残念ながら下線のように用意はされておらず、自作する必要があります。
方法としては、
- 破線を描く
- 描いた破線を Text の下に表示する
とやっていきます。
1. 破線を描く
破線の描画方法です。 Canvas を使用します。
まず Canvas で線を描画するため、Canvas の onDraw で drawLine を行います。
主に設定するパラメータは下記になります。
- color:線の色
- start:線の描画開始位置( Offset 型)
- end:線の描画終了位置( Offset 型)
- strokeWidth:線の太さ
- cap:線の端の表現(Butt 又は Round 又は Square)
- pathEffect:線のオプション効果
横方向の線を描画するために、 start と end の引数に指定する Offset 型の y 値は同じ size.height にし、 x 値を 0f から size.width に設定します。
線を破線にするために、pathEffect に PathEffect.dashPathEffect を設定します。引数は
- intervals:「破線 1 つの長さ」と「破線の間隔」を配列で定義します。そのため要素数は偶数になります。
- phase:intervals に対するオフセット値。破線の開始位置をずらしたい場合に値を調整します。不要な場合は「破線 1 つの長さ」と「破線の間隔」を足した値を設定します。
です。
実装は下記のようになります。
// 線の太さ
val strokeWidth: Float = 4f
// 破線1つの長さ
val dashedLength: Float = 30f
// 破線の間隔
val dashedInterval: Float = 15f
Canvas(
modifier = Modifier
.fillMaxSize(),
onDraw = {
drawLine(
color = Color.Red,
start = Offset(0f, size.height),
end = Offset(size.width, size.height),
strokeWidth = strokeWidth,
cap = StrokeCap.Round,
pathEffect = PathEffect.dashPathEffect(
intervals = floatArrayOf(dashedLength, dashedInterval),
phase = dashedLength + dashedInterval,
)
)
}
)

2. 描いた破線を Text の下に表示する
続けて、先ほど作成した破線を Text の下に描画させます。
Column を用いて上に Text を、下に破線を配置します。上記のコードをそのまま配置すると破線だけ大きく表示されてしまうため、modifier を Modifier.matchParentSize() に設定します。これによりText のサイズと合わせて表示してくれます。
Box {
Text(text = "破線付きテキストの作り方")
Canvas(
modifier = Modifier
.matchParentSize(),
onDraw = {
drawLine(
~省略~
)
}
)
}

以上が破線付きテキストの作成方法になります。
最後に Text の必要な引数を添えて、破線付きテキストを共通で使用できるよう Composable を作成します。
@Composable
fun DashedText(
text: String,
modifier: Modifier = Modifier,
color: Color = Color.Unspecified,
fontSize: TextUnit = TextUnit.Unspecified,
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.Clip,
softWrap: Boolean = true,
maxLines: Int = Int.MAX_VALUE,
minLines: Int = 1,
onTextLayout: ((TextLayoutResult) -> Unit)? = null,
style: TextStyle = LocalTextStyle.current,
// 破線の色
dashedColor: Color = Color.Unspecified,
// 破線の太さ
dashedWidth: Float = 4f,
// 破線1つの長さ
dashedLength: Float = 30f,
// 破線の間隔
dashedInterval: Float = 15f
) {
Box(
modifier = Modifier
.wrapContentWidth(),
contentAlignment = Alignment.BottomCenter
) {
Text(text, modifier, color, fontSize, fontStyle, fontWeight, fontFamily, letterSpacing, textDecoration, textAlign, lineHeight, overflow, softWrap, maxLines, minLines, onTextLayout, style)
Canvas(
modifier = Modifier
.matchParentSize()
) {
drawLine(
color = dashedColor,
start = Offset(0f, size.height),
end = Offset(size.width, size.height),
strokeWidth = 2f,
cap = StrokeCap.Round,
pathEffect = PathEffect.dashPathEffect(
intervals = floatArrayOf(dashedLength, dashedInterval),
phase = dashedLength + dashedInterval,
)
)
}
}
}
残念ながら今回ご紹介した破線付きテキストは 1 行のみの対応例になります。複数行を可能にするには描画するテキストの長さや行間隔を考慮する必要があります。
まとめ
Jetpack Compose で破線付きテキストの作り方をご紹介しました。下線や取消線といったメジャーなテキストの表現を簡単に実現できる方法が用意されていました。また破線付きテキストといった特殊な表現でも、Canvas を活用することで綺麗に実装できます。Jetpack Compose の柔軟性と応用力の高さが感じられますね。これからも Jetpack Compose ライフを楽しみましょう。