Compose 可点击文本:ClickableText Compose 中的 ClickableSpan

在Compose中如何实现一段文字,多种显示效果,多种点击区域?在xml时代我们可以用ClickableSpan,在Compose时代我们用ClickableText 。

今天实现的效果如下图所示:

1、准备工作,引入相关资源文件(可跳过)

1.1、colors.xml

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<resources>
      <color name="welcome_privacy_title">#333333</color>
      <color name="welcome_privacy_content">#333333</color>
      <color name="welcome_privacy_tip">#333333</color>
      <color name="welcome_privacy_disagree">#999999</color>
</resources>

1.2、strings.xml

xml 复制代码
<resources>
    <string name="welcome_app_name">module_welcome</string>
    <string name="welcome_privacy_title">用户协议与隐私政策</string>
    <string name="welcome_privacy_content">亲爱的用户,欢迎来到wanandroid,
        我们深知个人信息对您的重要性,我们将按照法律法规的规定,保护您的个人信息及隐私安全。\n
        1、为了你能安全的使用wanandroid,我们会想你收集设备信息;\n
        2、未经你的同意,我们不会从第三方获取墩号共享或对外提供你的信息;\n
        3、你可以访问、更正、删除你的个人信息,我们也将提供注销,投诉方式;\n
        4、如果您同意请点击下面的按钮以接收我们的服务。</string>
    <string name="welcome_privacy_tip">你可以阅读完整版《用户协议》、《隐私政策》、《ShareSdk隐私政策》、《百度地图Sdk隐私政策》来了解详细信息</string>
    <string name="welcome_agree_privacy">同意</string>
    <string name="welcome_disagree_privacy">不同意退出</string>
    <string name="user_agrreement">《用户协议》</string>
    <string name="privacy_policy">《隐私政策》</string>
    <string name="share_sdk">《ShareSdk隐私政策》</string>
    <string name="baidu_map_sdk">《百度地图Sdk隐私政策》</string>
</resources>

2、创建model

kotlin 复制代码
data class ClickableTextPart (
    val text:String,
    val tag:String,
    val destination: NavigationDestination
)

3、创建 AnnotatedString AnnotatedString是具有多种样式的文本基本数据结构。可通过Builder构造,我们通过 buildAnnotatedString 来创建。

我们的需求是4个协议,需要不同的样式显示,并且每个协议可以单独点击。

我们分两步来处理

3.1、先显示

显示效果的整体逻辑比较简单,就是定位字符串的起始位置和结束位置,然后将起始位置和结束位置之间的字符串处理效果就行了。

举个例子:你可以阅读完整版《用户协议》、《隐私政策》、《ShareSdk隐私政策》、《百度地图Sdk隐私政策》来了解详细信息;你可以阅读完整版、、、来了解详细信息 ;这些字符串按照默认的样式来显示。《用户协议》、《隐私政策》、《ShareSdk隐私政策》、《百度地图Sdk隐私政策》 这些字符串显示指定的效果。

引入基本数据

vbscript 复制代码
val context = LocalContext.current
val clickableParts = listOf(
    ClickableTextPart(
        context.getString(R.string.user_agrreement),
        "user_agreement",
        NavigationDestination.UserAgreement
    ),
    ClickableTextPart(
        context.getString(R.string.privacy_policy),
        "privacy_policy",
        NavigationDestination.PrivacyPolicy
    ),
    ClickableTextPart(
        context.getString(R.string.share_sdk),
        "share_sdk",
        NavigationDestination.SharedSdkPrivacy
    ),
    ClickableTextPart(
        context.getString(R.string.baidu_map_sdk),
        "baidu_map_sdk",
        NavigationDestination.BaiduMapSdkPrivacy
    )
)

val baseText = context.getString(R.string.welcome_privacy_tip)
ini 复制代码
val annotatedString = buildAnnotatedString {

    var currentIndex = 0
    clickableParts.forEach { part ->
        val startIndex = baseText.indexOf(part.text, currentIndex)
        //普通文本直接添加,例如第一次进入 startIndex=8 currentIndex=0 那么就需要添加第一个默认字符串
        if (startIndex > currentIndex) {
            val stringInfo=baseText.substring(currentIndex, startIndex)
            append(stringInfo)
        }
        // 添加可点击文本(带样式)
        pushStringAnnotation(tag = part.tag, annotation = part.tag)
        withStyle(
            style = SpanStyle(
                color = Color.Blue,
                fontWeight = FontWeight.Bold
            )
        ) {
            //《用户协议》 《隐私政策》 《ShareSdk隐私政策》 《百度地图Sdk隐私政策》
            append(part.text)
        }
        pop()

        currentIndex = startIndex + part.text.length
    }

    //最后再添加文本末尾那一句
    if (currentIndex < baseText.length) {
        append(baseText.substring(currentIndex))
    }

}

上面的代码注释写的很详细很好理解的,唯一可能有点陌生的是 pushStringAnnotation 和 pop这两个方法, 这个我们稍后再说。到此样式已经显示完毕了,点击效果呢?

点击效果非常简单,下面贴出完整的代码:

ini 复制代码
@Composable
fun ClickablePrivacyText(viewModel: WellcomeViewModel, modifier: Modifier = Modifier) {

    val context = LocalContext.current
    val clickableParts = listOf(
        ClickableTextPart(
            context.getString(R.string.user_agrreement),
            "user_agreement",
            NavigationDestination.UserAgreement
        ),
        ClickableTextPart(
            context.getString(R.string.privacy_policy),
            "privacy_policy",
            NavigationDestination.PrivacyPolicy
        ),
        ClickableTextPart(
            context.getString(R.string.share_sdk),
            "share_sdk",
            NavigationDestination.SharedSdkPrivacy
        ),
        ClickableTextPart(
            context.getString(R.string.baidu_map_sdk),
            "baidu_map_sdk",
            NavigationDestination.BaiduMapSdkPrivacy
        )
    )

    val baseText = context.getString(R.string.welcome_privacy_tip)


    val annotatedString = buildAnnotatedString {

        var currentIndex = 0
        clickableParts.forEach { part ->
            val startIndex = baseText.indexOf(part.text, currentIndex)
            //普通文本直接添加,例如第一次进入 startIndex=8 currentIndex=0 那么就需要添加第一个默认字符串
            if (startIndex > currentIndex) {
                val stringInfo=baseText.substring(currentIndex, startIndex)
                append(stringInfo)
            }
            // 添加可点击文本(带样式)
            pushStringAnnotation(tag = part.tag, annotation = part.tag)
            withStyle(
                style = SpanStyle(
                    color = Color.Blue,
                    fontWeight = FontWeight.Bold
                )
            ) {
                //《用户协议》 《隐私政策》 《ShareSdk隐私政策》 《百度地图Sdk隐私政策》
                append(part.text)
            }
            pop()

            currentIndex = startIndex + part.text.length
        }

        //最后再添加文本末尾那一句
        if (currentIndex < baseText.length) {
            append(baseText.substring(currentIndex))
        }

    }



    ClickableText(
        text = annotatedString,
        modifier = modifier
            .padding(start = 16.dp, top = 20.dp, end = 16.dp),
        style = TextStyle(
            fontSize = 16.sp,
            color = colorResource(R.color.welcome_privacy_tip)
        ),
        onClick = { offset->
            Log.e("clickOffset","$offset")
           annotatedString.getStringAnnotations(start = offset, end = offset)
               .firstOrNull()
               ?.let { annotation->
                 Toast.makeText(context,annotation.tag,Toast.LENGTH_LONG).show()
               }
        }
    )


}

在点击效果的实现中,程序比较好理解,可能最主要就annotatedString.getStringAnnotations有点点陌生。

4、下面就结合上面的疑问一起来说说。

4.1、首先 pushStringAnnotation 和 pop 方法总是成对的出现的。

pushStringAnnotation(tag: String, annotation: String) 这个方法用于在当前构建位置,压入一个字符串,它需要两个参数,tag和annotation;tag 和 annotation 可以理解为键值对(key,value模型);pop表示已经压入完毕,可以结束了。这样我们再来理解这段程序就非常简单了。

scss 复制代码
// 添加可点击文本(带样式)
//准备开始压入自定义样式的字符串了,tag就是key annotation就是value
pushStringAnnotation(tag = part.tag, annotation = part.text)
withStyle(
    style = SpanStyle(
        color = Color.Blue,
        fontWeight = FontWeight.Bold
    )
) {
    //《用户协议》 《隐私政策》 《ShareSdk隐私政策》 《百度地图Sdk隐私政策》
    append(part.text)
}
//压入的字符串已经压入完毕了,可以弹出压入了
pop()

getStringAnnotations 看名字就是用来检索这些注解的方法,它接收两个参数start开始检索的区域(包含),end停止检索的区域(不包含)它返回一个List类型,绝大绝大多数情况下至多只有1个。

ini 复制代码
ClickableText(
    text = annotatedString,
    modifier = modifier
        .padding(start = 16.dp, top = 20.dp, end = 16.dp),
    style = TextStyle(
        fontSize = 16.sp,
        color = colorResource(R.color.welcome_privacy_tip)
    ),
    onClick = { offset->
        Log.e("clickOffset","$offset")
       annotatedString.getStringAnnotations(start = offset, end = offset)
           .firstOrNull()
           ?.let { annotation->
              Log.e("clickOffset",annotation.tag+"****"+annotation.item)
           }
    }
)

举个例子:

makefile 复制代码
文本: "C l i c k   H E R E"
索引:  0 1 2 3 4 5 6 7 8 9

假入我们 HERE 使用了pushStringAnnotation 压入,那么当点击E的时候,getStringAnnotations(start=7,end=7)他就返回了一个List集合,就能找到HERE对象,并通过tag 和 item 拿到我们存入的信息。

为什么是tag和Item我们刚刚不是传入了tag和annotation吗?( pushStringAnnotation(tag = part.tag, annotation = part.tag))问得好,我们看看源码

kotlin 复制代码
/**
 * Attach the given [annotation] to any appended text until a corresponding [pop]
 * is called.
 *
 * @sample androidx.compose.ui.text.samples.AnnotatedStringBuilderPushStringAnnotationSample
 *
 * @param tag the tag used to distinguish annotations
 * @param annotation the string annotation attached on this AnnotatedString
 * @see getStringAnnotations
 * @see Range
 */
fun pushStringAnnotation(tag: String, annotation: String): Int {
    MutableRange(item = annotation, start = text.length, tag = tag).also {
        styleStack.add(it)
        annotations.add(it)
    }
    return styleStack.size - 1
}

金光闪闪的item=annotation

总结:

1、 pushStringAnnotation 和 pop 方法总是成对的出现的 ,pushStringAnnotation表示开始压入需要处理的字符串,pop方法表示压入字符串操作已经结束了;getStringAnnotations就是查询我们刚刚压入的字符串。

2、pushStringAnnotation(tag: String, annotation: String) tag和annotation就是一个键值对,可以在后面的getStringAnnotations中查询出来,其中annotation在getStringAnnotations中用item表示。

3、最后我们创建的 AnnotatedString需要在ClickableText中显示,才能发挥效果。

相关推荐
Digitally2 小时前
如何在安卓手机/平板上找到下载文件?
android·智能手机·电脑
硬件学长森哥5 小时前
Android影像基础--cameraAPI2核心流程
android·计算机视觉
前行的小黑炭9 小时前
Android 协程的使用:结合一个环境噪音检查功能的例子来玩玩
android·java·kotlin
阿华的代码王国9 小时前
【Android】内外部存储的读写
android·内外存储的读写
inmK113 小时前
蓝奏云官方版不好用?蓝云最后一版实测:轻量化 + 不限速(避更新坑) 蓝云、蓝奏云第三方安卓版、蓝云最后一版、蓝奏云无广告管理工具、安卓网盘轻量化 APP
android·工具·网盘工具
giaoho13 小时前
Android 热点开发的相关api总结
android
咖啡の猫15 小时前
Android开发-常用布局
android·gitee
程序员老刘15 小时前
Google突然“变脸“,2026年要给全球开发者上“紧箍咒“?
android·flutter·客户端
Tans515 小时前
Androidx Lifecycle 源码阅读笔记
android·android jetpack·源码阅读
雨白16 小时前
实现双向滑动的 ScalableImageView(下)
android