Jetpack ComposeでLinkify Textやる
はじめに
初めまして、nacatlです。
年内にブログ一個書くぞという勢いでZenn初投稿していくスタイル。
機会があって最近調べたネタを書いて一年を〆たいと思います。
経緯と目的
ここで言うLinkify
とは文字列にURL
が入ってるときに、自動検出してリンクアクションにしてくれる機能のことを指しています。
AndroidViewの頃は下記のフラグで一発でした。
<TextView
android:autoLink="web"
/>
ただ、昨今のAndroid UI開発において、UIは基本的にComposeで構成します。
そして上記のフラグはandroidx.compose.ui
のバージョン1.7.6
において実装されていません。
というわけで、Text Composable
で文章中のURLを抽出してリンクアクションを実装する方法を調査してみました。
実装
結論から言えば以下のリポジトリに実装を載せておきました。
中身について簡単に説明していきます。
目指すべきゴール
Text Composable
内の文章からではなく外部の変数なり定数なりからURL
を入れる場合なら、公式ドキュメントに記載があります。
逆に言えば、リンク化するURL
とString
内におけるindex(start - end)
の抽出ができれば、あとはドキュメント通りにComposeのAnnotatedString
へLinkAnnotation
の形で付与できるはずです。
なので、方針として「リンク化するURL
とString
内におけるindex(start - end)
の抽出」からLinkAnnotation
への変換を目指します。
URLとindexの抽出と変換
参考にさせていただいたのは主にこのStackOverFlowのスレッドです。
これを見て、自分なりの方針を固めました。
- Android SDK公式の
android.text.util.Linkify
でURL判定、リンクアクションが挿入されたSpannableString
を作成する- 公式で処理があるなら自前でメンテナンスしたくないな感
- しかし
android.text.util.Linkify
は基本的にTextView
に用いるSpannableString
用 - なので
Text Composable
に入れても反応しない。そのままでは使えないので次へ
-
SpannableString
からSpansをgetする - getしたSpansの
URL
とindex(start - end)
をLinkAnnotation
としてAnnotatedString
に付与する -
Text Composable
にAnnotatedString
を渡す
import android.text.SpannableString
import android.text.style.URLSpan
import android.text.util.Linkify
/**
* ref. [linkify-with-compose-text(stack overflow)](https://stackoverflow.com/questions/66130513/linkify-with-compose-text)
* 一度[SpannableString]に[Linkify]で追加したリンクを抽出し、[AnnotatedString]に貼り直す
*
* @param spannable 元となる[SpannableString]
* @param linkStyles リンクテキストに充てる[TextLinkStyles]
* @param linkInteractionListener リンクタップ時のコールバック。nullでは[Intent.ACTION_VIEW]相当でURLを開く
*/
private fun AnnotatedString.Builder.linkifyFromSpannable(
spannable: SpannableString,
linkStyles: TextLinkStyles,
linkInteractionListener: LinkInteractionListener?,
) {
// 1. 公式のLinkifyでURL判定、リンクアクションが挿入されたSpannableStringを作成する
Linkify.addLinks(
spannable,
Linkify.WEB_URLS, // "http://", "https://", "rtsp://", "ftp://" に対応
)
// 2. SpannableからSpansをgetする
val urlSpans = spannable.getSpans(
0,
spannable.length,
URLSpan::class.java,
)
// 3. getしたSpansの`URL`と`index(start - end)`を`LinkAnnotation`としてAnnotatedStringに付与する
urlSpans.forEach { urlSpan ->
val start = spannable.getSpanStart(urlSpan)
val end = spannable.getSpanEnd(urlSpan)
val linkAnnotation = LinkAnnotation.Url(
url = urlSpan.url,
styles = linkStyles,
linkInteractionListener = linkInteractionListener,
)
addLink(
url = linkAnnotation,
start = start,
end = end,
)
}
}
対応schemeについては、android.text.util.Linkify.WEB_URLS
が内部実装として以下の判定を行っていることからわかります。
if ((mask & WEB_URLS) != 0) {
gatherLinks(links, text, Patterns.AUTOLINK_WEB_URL,
new String[] { "http://", "https://", "rtsp://", "ftp://" },
sUrlMatchFilter, null);
}
なお、android.text.util.Linkify
にはEMAIL_ADDRESSES
やPHONE_NUMBERS
の抽出もありますが、必要なら条件に足すか別に作るか検討すればいいと思います。
Text Composableへの接続
前節でAnnotatedString.Builder
の拡張関数としてリンク抽出とLinkAnnotation
付与を行えたので、あとは特に難しくありません。
Text Composable
と同様にString
とAnnotatedString
を受けられるようにそれぞれの口を作って配置すれば終わります。
サンプルリポジトリではいろいろ設定してますが、環境に応じてComposableの切り方やパラメータなどカスタムするのがいいと思います。
ちなみにAnnotation
は後勝ちっぽいので、文字列に色付けてたりしてもURL
部分だけ上書きされます。
また、ClickableText Composable
は1.7.0
からDeprecatedなので利用していません。
@Composable
fun LinkifyText(
text: String, // or AnnotatedString,
~~~~,
) {
val linkifyText = remember(text) {
text.linkify(
linkStyles = linkStyles,
linkInteractionListener = linkInteractionListener,
)
}
Text(
text = linkifyText,
~~~~,
)
}
private fun String.linkify( // or AnnotatedString.linkify
linkStyles: TextLinkStyles,
linkInteractionListener: LinkInteractionListener?,
) = buildAnnotatedString {
append(this@linkify)
linkifyFromSpannable(
spannable = SpannableString(this@linkify),
linkStyles = linkStyles,
linkInteractionListener = linkInteractionListener,
)
}
サンプルリポジトリの結果スクショです。
余談
検討したけど採用しなかった手法がもう一つあります。
android.text.util.Linkify
でSpannableString
を作るまでは一緒ですが、その後にSpannsをgetするのではなくhtmlで繋ぎこめます。
Linkify.addLinks(
spannable,
Linkify.WEB_URLS, // "http://", "https://", "rtsp://", "ftp://" に対応
)
Text(
text = AnnotatedString.fromHtml(
htmlString = spannable.toHtml(),
),
)
ただ、文字列そのものをhtml形式に変換してしまう都合上、副作用を懸念して今回の処理では採用を見送りました。
何かしらで使えるかもしれないので備忘録として記録しておきます。
まとめ
年内に一つブログ書きたいなという欲が急に沸いたので、Jetpack ComposeでLinkifyTextを実装する方法を自分なりにまとめてみました。
この記事が皆様のお役に立てれば幸いです。
Discussion