Jetpack ComposeのText関数にリンクを付加する

Jetpack Composeを使用して、Text関数にリンクを付加する方法について。

まず、表示するテキストをURLとURL以外に分割します。

/**
 * @param type URLのときは"URL"、URL以外のときは"TEXT
 * @param text 表示するテキスト
 */
data class TextOrUrlString(val type: String, val text: String)

/**
 * textをURLとそれ以外に分割する
 */
private fun parseText(text: String): List<TextOrUrlString> {
    val regex = Regex(
        """https?://[a-zA-Z\d!\?/+\-_~=;.,*&@#$%()'\[\]]+""",
        setOf(RegexOption.IGNORE_CASE, RegexOption.MULTILINE)
    )

    val list: MutableList<TextOrUrlString> = mutableListOf()
    var startIndex = 0
    while (true) {
        val matchResult = regex.find(text, startIndex = startIndex) ?: break
        if (matchResult.range.first != 0) {
            list.add(
                TextOrUrlString(
                    "TEXT",
                    text.substring(startIndex, matchResult.range.first)
                )
            )
        }
        list.add(TextOrUrlString("URL", matchResult.value))
        startIndex = matchResult.range.last + 1
    }
    if (startIndex < text.length) {
        list.add(TextOrUrlString("TEXT", text.substring(startIndex)))
    }

    return list
}

URLとURL以外に分割したテキストから、AnnotatedStringを作成します。

/**
 * @param parsedText URLとそれ以外に分割された文字列
 * @param color リンク文字列の色
 */
private fun createAnnotatedString(
    parsedText: List<TextOrUrlString>,
    color: androidx.compose.ui.graphics.Color
): AnnotatedString {
    return buildAnnotatedString {
        parsedText.forEach { word ->
            when (word.type) {
                "TEXT" -> {
                    append(word.text)
                }
                "URL" -> {
                    pushStringAnnotation(
                        tag = "URL",
                        annotation = word.text
                    )
                    withStyle(
                        style = SpanStyle(
                            color = color,
                            fontWeight = FontWeight.Bold
                        )
                    ) {
                        append(word.text)
                    }
                    pop()
                }
            }
        }
    }
}

タップされた位置を取得するためText関数の代わりに、ClickableText関数を使用します。
タップされた位置にあるURLを取得して、URLを開きます。

@Composable
fun AutoLinkText(text: String) {
    val parsedText = parseText(text)
    val annotatedString = createAnnotatedString(parsedText, MaterialTheme.colors.primary)

    val uriHandler = AndroidUriHandler(LocalContext.current)
    ClickableText(
        text = annotatedString,
        style = MaterialTheme.typography.body1,
        onClick = { offset ->
            annotatedString.getStringAnnotations(tag = "URL", start = offset, end = offset)
                .firstOrNull()?.let { annotation ->
                    if (annotation.tag == "URL") {
                        uriHandler.openUri(annotation.item)
                    }
                }
        }
    )
}

全体のソースコードは次のようになります。

import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.AndroidUriHandler
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.withStyle

@Composable
fun AutoLinkText(text: String) {
    val parsedText = parseText(text)
    val annotatedString = createAnnotatedString(parsedText, MaterialTheme.colors.primary)

    val uriHandler = AndroidUriHandler(LocalContext.current)
    ClickableText(
        text = annotatedString,
        style = MaterialTheme.typography.body1,
        onClick = { offset ->
            annotatedString.getStringAnnotations(tag = "URL", start = offset, end = offset)
                .firstOrNull()?.let { annotation ->
                    if (annotation.tag == "URL") {
                        uriHandler.openUri(annotation.item)
                    }
                }
        }
    )
}

/**
 * @param type URLのときは"URL"、URL以外のときは"TEXT
 * @param text 表示するテキスト
 */
data class TextOrUrlString(val type: String, val text: String)

/**
 * textをURLとそれ以外に分割する
 */
private fun parseText(text: String): List<TextOrUrlString> {
    val regex = Regex(
        """https?://[a-zA-Z\d!\?/+\-_~=;.,*&@#$%()'\[\]]+""",
        setOf(RegexOption.IGNORE_CASE, RegexOption.MULTILINE)
    )

    val list: MutableList<TextOrUrlString> = mutableListOf()
    var startIndex = 0
    while (true) {
        val matchResult = regex.find(text, startIndex = startIndex) ?: break
        if (matchResult.range.first != 0) {
            list.add(
                TextOrUrlString(
                    "TEXT",
                    text.substring(startIndex, matchResult.range.first)
                )
            )
        }
        list.add(TextOrUrlString("URL", matchResult.value))
        startIndex = matchResult.range.last + 1
    }
    if (startIndex < text.length) {
        list.add(TextOrUrlString("TEXT", text.substring(startIndex)))
    }

    return list
}

/**
 * @param parsedText URLとそれ以外に分割された文字列
 * @param color リンク文字列の色
 */
private fun createAnnotatedString(
    parsedText: List<TextOrUrlString>,
    color: androidx.compose.ui.graphics.Color
): AnnotatedString {
    return buildAnnotatedString {
        parsedText.forEach { word ->
            when (word.type) {
                "TEXT" -> {
                    append(word.text)
                }
                "URL" -> {
                    pushStringAnnotation(
                        tag = "URL",
                        annotation = word.text
                    )
                    withStyle(
                        style = SpanStyle(
                            color = color,
                            fontWeight = FontWeight.Bold
                        )
                    ) {
                        append(word.text)
                    }
                    pop()
                }
            }
        }
    }
}

使用例
AutoLinkTextのサンプル

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            AutoLinkTextForJetpackComposeTheme {
                // A surface container using the 'background' color from the theme
                Surface(color = MaterialTheme.colors.background) {
                    MainContent()
                }
            }
        }
    }
}

@Composable
fun MainContent() {
    val text = """
        ブログ「山本隆の開発日誌」はhttp://www.gesource.jp/weblog/です。
    """.trimIndent()
    AutoLinkText(text = text)
}

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    AutoLinkTextForJetpackComposeTheme {
        MainContent()
    }
}

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください