Android TextView 基于Span的各种文字样式实现<二>

微信图片_20250212171111.png

在 Android 开发中,Span 是一种用于对 Spannable 文本进行样式设置和交互处理的机制。SpannableCharSequence 的一个子接口,它允许你在文本的特定区域应用各种样式和行为,而不是对整个文本应用相同的属性。以下是关于 Android Span 的具体功能实现

Span说明:

  • 1、BackgroundColorSpan 背景色
  • 2、ClickableSpan 文本可点击,有点击事件
  • 3、ForegroundColorSpan 文本颜色(前景色)
  • 4、MaskFilterSpan 修饰效果,如模糊(BlurMaskFilter)、浮雕(EmbossMaskFilter)
  • 5、MetricAffectingSpan 父类,一般不用
  • 6、RasterizerSpan 光栅效果
  • 7、StrikethroughSpan 删除线(中划线)
  • 8、SuggestionSpan 相当于占位符
  • 9、UnderlineSpan 下划线
  • 10、AbsoluteSizeSpan 绝对大小(文本字体)
  • 11、DynamicDrawableSpan 设置图片,基于文本基线或底部对齐。
  • 12、ImageSpan 图片
  • 13、RelativeSizeSpan 相对大小(文本字体)
  • 14、ReplacementSpan 父类,一般不用
  • 15、ScaleXSpan 基于x轴缩放
  • 16、StyleSpan 字体样式:粗体、斜体等
  • 17、SubscriptSpan 下标(数学公式会用到)
  • 18、SuperscriptSpan 上标(数学公式会用到)
  • 19、TextAppearanceSpan 文本外貌(包括字体、大小、样式和颜色)
  • 20、TypefaceSpan 文本字体
  • 21、URLSpan 文本超链接

1:工具类


package com.wkq.ui.util.span

/**
*
*@Author: wkq
*
*@Time: 2025/2/12 14:23
*
*@Desc:
*/

import android.content.Context
import android.graphics.BlurMaskFilter
import android.graphics.Color
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.os.Build
import android.text.Spannable
import android.text.SpannableString
import android.text.SpannableStringBuilder
import android.text.style.AbsoluteSizeSpan
import android.text.style.BackgroundColorSpan
import android.text.style.ClickableSpan
import android.text.style.DynamicDrawableSpan
import android.text.style.ForegroundColorSpan
import android.text.style.ImageSpan
import android.text.style.MaskFilterSpan
import android.text.style.RelativeSizeSpan
import android.text.style.ScaleXSpan
import android.text.style.StrikethroughSpan
import android.text.style.StyleSpan
import android.text.style.SubscriptSpan
import android.text.style.SuperscriptSpan
import android.text.style.TextAppearanceSpan
import android.text.style.TypefaceSpan
import android.text.style.URLSpan
import android.text.style.UnderlineSpan
import android.widget.TextView
import androidx.annotation.RequiresApi
import com.wkq.ui.view.KtLinearGradientFontSpan

/**
 * 用于处理 Android 中 SpannableString 的各种样式设置的工具类。
 * 提供了丰富的方法来对文本进行多样化的样式定制,
 * 最后还能将处理好的 SpannableString 追加到 TextView 中。
 */
class SpanUtils private constructor(private val context: Context) {
    companion object {
        @Volatile
        private var instance: SpanUtils? = null

        /**
         * 获取 SpanUtils 的单例实例。
         *
         * @param context 应用程序上下文,用于访问资源等操作。
         * @return SpanUtils 的单例实例。
         */
        fun getInstance(context: Context): SpanUtils {
            return instance ?: synchronized(this) {
                instance ?: SpanUtils(context).also { instance = it }
            }
        }
    }

    /**
     * 处理可能为 null 的文本,若文本为 null 则返回空字符串。
     *
     * @param text 待处理的文本,可能为 null。
     * @return 处理后的非空文本,若原文本为 null 则返回 ""。
     */
    private fun handleNullText(text: String?): String {
        return text ?: ""
    }



    fun setTextTypeface(
        startText: String?,
        spanText: String?,
        endText: String?,
        startColor: Int = Color.GREEN,
        endColor: Int = Color.GREEN,
        isLeftToRight:Boolean=true
    ): SpannableString {
        val combinedText =
            handleNullText(startText) + handleNullText(spanText) + handleNullText(endText)
        val startIndex = handleNullText(startText).length
        val endIndex = startIndex + handleNullText(spanText).length
        val spannable = SpannableString(combinedText)
        val span = KtLinearGradientFontSpan(startColor, endColor, isLeftToRight)
        spannable.setSpan(
            span, startIndex, endIndex, Spannable.SPAN_INCLUSIVE_EXCLUSIVE
        )
        return spannable
    }

    /**
     * 设置 spanText 的背景色。
     *
     * @param startText 起始文本,若为 null 会被处理为空字符串。
     * @param spanText 要设置样式的中间文本,若为 null 会被处理为空字符串。
     * @param endText 结束文本,若为 null 会被处理为空字符串。
     * @param color 背景色,默认为绿色(Color.GREEN)。
     * @return 应用了背景色样式的 SpannableString。
     */
    fun setBackgroundColorSpan(
        startText: String?,
        spanText: String?,
        endText: String?,
        color: Int = Color.GREEN
    ): SpannableString {
        val combinedText =
            handleNullText(startText) + handleNullText(spanText) + handleNullText(endText)
        val startIndex = handleNullText(startText).length
        val endIndex = startIndex + handleNullText(spanText).length
        val spannable = SpannableString(combinedText)
        spannable.setSpan(
            BackgroundColorSpan(color), startIndex, endIndex, Spannable.SPAN_INCLUSIVE_EXCLUSIVE
        )
        return spannable
    }

    /**
     * 设置 spanText 可点击及点击事件。
     *
     * @param startText 起始文本,若为 null 会被处理为空字符串。
     * @param spanText 要设置样式的中间文本,若为 null 会被处理为空字符串。
     * @param endText 结束文本,若为 null 会被处理为空字符串。
     * @param clickListener 点击 spanText 时触发的回调函数。
     * @return 应用了可点击样式的 SpannableString。
     */
    fun setClickableSpan(
        startText: String?,
        spanText: String?,
        endText: String?,
        clickListener: (content:String) -> Unit
    ): SpannableString {
        val combinedText =
            handleNullText(startText) + handleNullText(spanText) + handleNullText(endText)
        val startIndex = handleNullText(startText).length
        val endIndex = startIndex + handleNullText(spanText).length
        val spannable = SpannableString(combinedText)
        val clickableSpan = object : ClickableSpan() {
            override fun onClick(widget: android.view.View) {
                clickListener(handleNullText(spanText))
            }
        }
        spannable.setSpan(clickableSpan, startIndex, endIndex, Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
        return spannable
    }

    /**
     * 设置 spanText 的文本颜色(前景色)。
     *
     * @param startText 起始文本,若为 null 会被处理为空字符串。
     * @param spanText 要设置样式的中间文本,若为 null 会被处理为空字符串。
     * @param endText 结束文本,若为 null 会被处理为空字符串。
     * @param color 前景色,默认为蓝色(Color.BLUE)。
     * @return 应用了前景色样式的 SpannableString。
     */
    fun setForegroundColorSpan(
        startText: String?,
        spanText: String?,
        endText: String?,
        color: Int = Color.BLUE
    ): SpannableString {
        val combinedText =
            handleNullText(startText) + handleNullText(spanText) + handleNullText(endText)
        val startIndex = handleNullText(startText).length
        val endIndex = startIndex + handleNullText(spanText).length
        val spannable = SpannableString(combinedText)
        spannable.setSpan(
            ForegroundColorSpan(color), startIndex, endIndex, Spannable.SPAN_INCLUSIVE_EXCLUSIVE
        )
        return spannable
    }

    /**
     * 设置 spanText 的修饰效果,如模糊、浮雕。
     *
     * @param startText 起始文本,若为 null 会被处理为空字符串。
     * @param spanText 要设置样式的中间文本,若为 null 会被处理为空字符串。
     * @param endText 结束文本,若为 null 会被处理为空字符串。
     * @return 应用了模糊和浮雕修饰效果的 SpannableString。
     */
    fun setMaskFilterSpan(
        startText: String?,
        spanText: String?,
        endText: String?
    ): SpannableString {
        val combinedText =
            handleNullText(startText) + handleNullText(spanText) + handleNullText(endText)
        val startIndex = handleNullText(startText).length
        val endIndex = startIndex + handleNullText(spanText).length
        val spannable = SpannableString(combinedText)
        // 模糊(BlurMaskFilter)

        val blurMaskFilterSpan = MaskFilterSpan(BlurMaskFilter(3f, BlurMaskFilter.Blur.OUTER))
        spannable.setSpan(
            blurMaskFilterSpan, startIndex, endIndex, Spannable.SPAN_INCLUSIVE_EXCLUSIVE
        )
        // 浮雕(EmbossMaskFilter)
//        val embossMaskFilterSpan =
//            MaskFilterSpan(EmbossMaskFilter(floatArrayOf(1f, 1f, 1f), 0.2f, 28f, 5f))
//        spannable.setSpan(
//            embossMaskFilterSpan, startIndex, endIndex, Spannable.SPAN_INCLUSIVE_EXCLUSIVE
//        )
        return spannable
    }

    /**
     * 设置 spanText 的删除线(中划线)。
     *
     * @param startText 起始文本,若为 null 会被处理为空字符串。
     * @param spanText 要设置样式的中间文本,若为 null 会被处理为空字符串。
     * @param endText 结束文本,若为 null 会被处理为空字符串。
     * @return 应用了删除线样式的 SpannableString。
     */
    fun setStrikethroughSpan(
        startText: String?,
        spanText: String?,
        endText: String?
    ): SpannableString {
        val combinedText =
            handleNullText(startText) + handleNullText(spanText) + handleNullText(endText)
        val startIndex = handleNullText(startText).length
        val endIndex = startIndex + handleNullText(spanText).length
        val spannable = SpannableString(combinedText)
        spannable.setSpan(
            StrikethroughSpan(), startIndex, endIndex, Spannable.SPAN_INCLUSIVE_EXCLUSIVE
        )
        return spannable
    }

    /**
     * 设置 spanText 的下划线。
     *
     * @param startText 起始文本,若为 null 会被处理为空字符串。
     * @param spanText 要设置样式的中间文本,若为 null 会被处理为空字符串。
     * @param endText 结束文本,若为 null 会被处理为空字符串。
     * @return 应用了下划线样式的 SpannableString。
     */
    fun setUnderlineSpan(
        startText: String?,
        spanText: String?,
        endText: String?
    ): SpannableString {
        val combinedText =
            handleNullText(startText) + handleNullText(spanText) + handleNullText(endText)
        val startIndex = handleNullText(startText).length
        val endIndex = startIndex + handleNullText(spanText).length
        val spannable = SpannableString(combinedText)
        spannable.setSpan(UnderlineSpan(), startIndex, endIndex, Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
        return spannable
    }

    /**
     * 设置 spanText 的绝对大小(文本字体)。
     *
     * @param startText 起始文本,若为 null 会被处理为空字符串。
     * @param spanText 要设置样式的中间文本,若为 null 会被处理为空字符串。
     * @param endText 结束文本,若为 null 会被处理为空字符串。
     * @param size 字体的绝对大小。
     * @param dip 若为 true,表示大小以 dip 为单位;若为 false,表示大小以像素为单位,默认为 true。
     * @return 应用了绝对大小样式的 SpannableString。
     */
    fun setAbsoluteSizeSpan(
        startText: String?,
        spanText: String?,
        endText: String?,
        size: Int,
        dip: Boolean = true
    ): SpannableString {
        val combinedText =
            handleNullText(startText) + handleNullText(spanText) + handleNullText(endText)
        val startIndex = handleNullText(startText).length
        val endIndex = startIndex + handleNullText(spanText).length
        val spannable = SpannableString(combinedText)
        spannable.setSpan(
            AbsoluteSizeSpan(size, dip), startIndex, endIndex, Spannable.SPAN_INCLUSIVE_EXCLUSIVE
        )
        return spannable
    }

    /**
     * 设置 spanText 中的图片,基于文本基线或底部对齐。
     *
     * @param startText 起始文本,若为 null 会被处理为空字符串。
     * @param spanText 要设置样式的中间文本,若为 null 会被处理为空字符串。
     * @param endText 结束文本,若为 null 会被处理为空字符串。
     * @param drawableResId 图片资源的 ID。
     * @param width 图片的宽度。
     * @param height 图片的高度。
     * @return 应用了图片样式(基线和底部对齐)的 SpannableString。
     */
    fun setDynamicDrawableSpan(
        startText: String?,
        spanText: String?,
        endText: String?,
        drawableResId: Int,
        width: Int,
        height: Int
    ): SpannableString {
        val combinedText =
            handleNullText(startText) + handleNullText(spanText) + handleNullText(endText)
        val startIndex = handleNullText(startText).length
        val endIndex = startIndex + handleNullText(spanText).length
        val spannable = SpannableString(combinedText)
        val drawableSpanBaseline =
            object : DynamicDrawableSpan(DynamicDrawableSpan.ALIGN_BASELINE) {
                override fun getDrawable(): Drawable {
                    val d = context.resources.getDrawable(drawableResId)
                    d.setBounds(0, 0, width, height)
                    return d
                }
            }
        spannable.setSpan(
            drawableSpanBaseline, startIndex, endIndex, Spannable.SPAN_INCLUSIVE_EXCLUSIVE
        )
        val drawableSpanBottom = object : DynamicDrawableSpan(DynamicDrawableSpan.ALIGN_BOTTOM) {
            override fun getDrawable(): Drawable {
                val d = context.resources.getDrawable(drawableResId)
                d.setBounds(0, 0, width, height)
                return d
            }
        }
        spannable.setSpan(
            drawableSpanBottom, startIndex, endIndex, Spannable.SPAN_INCLUSIVE_EXCLUSIVE
        )
        return spannable
    }

    /**
     * 设置 spanText 中的图片。
     *
     * @param startText 起始文本,若为 null 会被处理为空字符串。
     * @param spanText 要设置样式的中间文本,若为 null 会被处理为空字符串。
     * @param endText 结束文本,若为 null 会被处理为空字符串。
     * @param drawableResId 图片资源的 ID。
     * @param width 图片的宽度。
     * @param height 图片的高度。
     * @return 应用了图片样式的 SpannableString。
     */
    fun setImageSpan(
        startText: String?,
        spanText: String?,
        endText: String?,
        drawableResId: Int,
        width: Int,
        height: Int
    ): SpannableString {
        val combinedText =
            handleNullText(startText) + handleNullText(spanText) + handleNullText(endText)
        val startIndex = handleNullText(startText).length
        val endIndex = startIndex + handleNullText(spanText).length
        val spannable = SpannableString(combinedText)
        val d = context.resources.getDrawable(drawableResId)
        d.setBounds(0, 0, width, height)
        spannable.setSpan(ImageSpan(d), startIndex, endIndex, Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
        return spannable
    }

    /**
     * 设置 spanText 的相对大小(文本字体)。
     *
     * @param startText 起始文本,若为 null 会被处理为空字符串。
     * @param spanText 要设置样式的中间文本,若为 null 会被处理为空字符串。
     * @param endText 结束文本,若为 null 会被处理为空字符串。
     * @param proportion 字体的相对大小比例。
     * @return 应用了相对大小样式的 SpannableString。
     */
    fun setRelativeSizeSpan(
        startText: String?,
        spanText: String?,
        endText: String?,
        proportion: Float
    ): SpannableString {
        val combinedText =
            handleNullText(startText) + handleNullText(spanText) + handleNullText(endText)
        val startIndex = handleNullText(startText).length
        val endIndex = startIndex + handleNullText(spanText).length
        val spannable = SpannableString(combinedText)
        spannable.setSpan(
            RelativeSizeSpan(proportion), startIndex, endIndex, Spannable.SPAN_INCLUSIVE_EXCLUSIVE
        )
        return spannable
    }

    /**
     * 基于 x 轴缩放 spanText。
     *
     * @param startText 起始文本,若为 null 会被处理为空字符串。
     * @param spanText 要设置样式的中间文本,若为 null 会被处理为空字符串。
     * @param endText 结束文本,若为 null 会被处理为空字符串。
     * @param proportion x 轴的缩放比例。
     * @return 应用了 x 轴缩放样式的 SpannableString。
     */
    fun setScaleXSpan(
        startText: String?,
        spanText: String?,
        endText: String?,
        proportion: Float
    ): SpannableString {
        val combinedText =
            handleNullText(startText) + handleNullText(spanText) + handleNullText(endText)
        val startIndex = handleNullText(startText).length
        val endIndex = startIndex + handleNullText(spanText).length
        val spannable = SpannableString(combinedText)
        spannable.setSpan(
            ScaleXSpan(proportion), startIndex, endIndex, Spannable.SPAN_INCLUSIVE_EXCLUSIVE
        )
        return spannable
    }

    /**
     * 设置 spanText 的字体样式:粗体、斜体等。
     *
     * @param startText 起始文本,若为 null 会被处理为空字符串。
     * @param spanText 要设置样式的中间文本,若为 null 会被处理为空字符串。
     * @param endText 结束文本,若为 null 会被处理为空字符串。
     * @param style 字体样式,默认为粗斜体(Typeface.BOLD_ITALIC)。
     * @return 应用了字体样式的 SpannableString。
     */
    fun setStyleSpan(
        startText: String?,
        spanText: String?,
        endText: String?,
        style: Int = Typeface.BOLD_ITALIC
    ): SpannableString {
        val combinedText =
            handleNullText(startText) + handleNullText(spanText) + handleNullText(endText)
        val startIndex = handleNullText(startText).length
        val endIndex = startIndex + handleNullText(spanText).length
        val spannable = SpannableString(combinedText)
        spannable.setSpan(
            StyleSpan(style), startIndex, endIndex, Spannable.SPAN_INCLUSIVE_EXCLUSIVE
        )
        return spannable
    }


    /**
     * 设置 spanText 的下标。
     *
     * @param startText 起始文本,若为 null 会被处理为空字符串。
     * @param spanText 要设置样式的中间文本,若为 null 会被处理为空字符串。
     * @param endText 结束文本,若为 null 会被处理为空字符串。
     * @return 应用了下标样式的 SpannableString。
     */
    fun setSubscriptSpan(
        startText: String?,
        spanText: String?,
        endText: String?
    ): SpannableString {
        val combinedText = handleNullText(startText) + handleNullText(spanText) + handleNullText(endText)
        val startIndex = handleNullText(startText).length
        val endIndex = startIndex + handleNullText(spanText).length
        val spannable = SpannableString(combinedText)
        spannable.setSpan(SubscriptSpan(), startIndex, endIndex, Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
        return spannable
    }
    /**
     * 设置 spanText 的上标。
     *
     * @param startText 起始文本,若为 null 会被处理为空字符串。
     * @param spanText 要设置样式的中间文本,若为 null 会被处理为空字符串。
     * @param endText 结束文本,若为 null 会被处理为空字符串。
     * @return 应用了上标样式的 SpannableString。
     */
    fun setSuperscriptSpan(
        startText: String?,
        spanText: String?,
        endText: String?
    ): SpannableString {
        val combinedText = handleNullText(startText) + handleNullText(spanText) + handleNullText(endText)
        val startIndex = handleNullText(startText).length
        val endIndex = startIndex + handleNullText(spanText).length
        val spannable = SpannableString(combinedText)
        spannable.setSpan(SuperscriptSpan(), startIndex, endIndex, Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
        return spannable
    }
    /**
     * 设置 spanText 的外貌(包括字体、大小、样式和颜色)。
     *
     * @param startText 起始文本,若为 null 会被处理为空字符串。
     * @param spanText 要设置样式的中间文本,若为 null 会被处理为空字符串。
     * @param endText 结束文本,若为 null 会被处理为空字符串。
     * @param appearanceStyle 文本外貌的样式资源 ID,默认为 android.R.style.TextAppearance_Medium。
     * @return 应用了文本外貌样式的 SpannableString。
     */
    fun setTextAppearanceSpan(
        startText: String?,
        spanText: String?,
        endText: String?,
        appearanceStyle: Int = android.R.style.TextAppearance_Medium
    ): SpannableString {
        val combinedText = handleNullText(startText) + handleNullText(spanText) + handleNullText(endText)
        val startIndex = handleNullText(startText).length
        val endIndex = startIndex + handleNullText(spanText).length
        val spannable = SpannableString(combinedText)
        spannable.setSpan(
            TextAppearanceSpan(context, appearanceStyle),
            startIndex,
            endIndex,
            Spannable.SPAN_INCLUSIVE_EXCLUSIVE
        )
        return spannable
    }
    /**
     * 设置 spanText 的字体。
     *
     * @param startText 起始文本,若为 null 会被处理为空字符串。
     * @param spanText 要设置样式的中间文本,若为 null 会被处理为空字符串。
     * @param endText 结束文本,若为 null 会被处理为空字符串。
     * @param typeface 字体名称,默认为 "monospace"。
     * @return 应用了指定字体样式的 SpannableString。
     */

    @RequiresApi(Build.VERSION_CODES.P)
    fun setTypefaceSpan(
        startText: String?,
        spanText: String?,
        endText: String?,
        typeface: Typeface
    ): SpannableString {
        val combinedText = handleNullText(startText) + handleNullText(spanText) + handleNullText(endText)
        val startIndex = handleNullText(startText).length
        val endIndex = startIndex + handleNullText(spanText).length
        val spannable = SpannableString(combinedText)
        spannable.setSpan(
            TypefaceSpan(typeface),
            startIndex,
            endIndex,
            Spannable.SPAN_INCLUSIVE_EXCLUSIVE
        )
        return spannable
    }

    fun setTypefaceSpan(
        startText: String?,
        spanText: String?,
        endText: String?,
        typeface: String = "monospace"
    ): SpannableString {
        val combinedText = handleNullText(startText) + handleNullText(spanText) + handleNullText(endText)
        val startIndex = handleNullText(startText).length
        val endIndex = startIndex + handleNullText(spanText).length
        val spannable = SpannableString(combinedText)
        spannable.setSpan(
            TypefaceSpan(typeface),
            startIndex,
            endIndex,
            Spannable.SPAN_INCLUSIVE_EXCLUSIVE
        )
        return spannable
    }


    /**
     * 设置 spanText 的超链接。
     *
     * @param startText 起始文本,若为 null 会被处理为空字符串。
     * @param spanText 要设置样式的中间文本,若为 null 会被处理为空字符串。
     * @param endText 结束文本,若为 null 会被处理为空字符串。
     * @param url 超链接的 URL 地址。
     * @return 应用了超链接样式的 SpannableString。
     */
    fun setURLSpan(
        startText: String?,
        spanText: String?,
        endText: String?,
        url: String
    ): SpannableString {
        val combinedText = handleNullText(startText) + handleNullText(spanText) + handleNullText(endText)
        val startIndex = handleNullText(startText).length
        val endIndex = startIndex + handleNullText(spanText).length
        val spannable = SpannableString(combinedText)
        spannable.setSpan(URLSpan(url), startIndex, endIndex, Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
        return spannable
    }
    /**
     * 将 SpannableString 追加到 TextView 并设置可点击(针对 URLSpan 等)。
     *
     * @param textView 要追加文本的 TextView。
     * @param spannable 要追加的 SpannableString。
     */
    fun appendSpannableToTextView(textView: TextView, spannable: SpannableString) {
        val builder = SpannableStringBuilder(textView.text)
        builder.append("\n")
        builder.append(spannable)
        textView.text = builder
        textView.movementMethod = android.text.method.LinkMovementMethod.getInstance()
    }
}

2.调用

package com.wkq.tools.ui

import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.graphics.Paint
import android.graphics.drawable.Drawable
import android.os.Build
import android.os.Bundle
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.TextUtils
import android.view.LayoutInflater
import android.view.View
import android.widget.TextView
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.res.ResourcesCompat
import com.wkq.tools.R
import com.wkq.tools.databinding.ActivityUiSpanBinding
import com.wkq.tools.databinding.ActivityUiTextviewBinding
import com.wkq.ui.util.WebViewUtil
import com.wkq.ui.util.span.CustomClickableSpan
import com.wkq.ui.util.span.SpanBuilder
import com.wkq.ui.util.span.SpanUtils
import com.wkq.ui.view.KtLinearGradientFontSpan

/**
 *
 *@Author: wkq
 *
 *@Time: 2025/2/11 15:10
 *
 *@Desc:
 *
 *  * 1、BackgroundColorSpan 背景色
 *  * 2、ClickableSpan 文本可点击,有点击事件
 *  * 3、ForegroundColorSpan 文本颜色(前景色)
 *  * 4、MaskFilterSpan 修饰效果,如模糊(BlurMaskFilter)、浮雕(EmbossMaskFilter)
 *  * 5、MetricAffectingSpan 父类,一般不用
 *  * 6、RasterizerSpan 光栅效果
 *  * 7、StrikethroughSpan 删除线(中划线)
 *  * 8、SuggestionSpan 相当于占位符
 *  * 9、UnderlineSpan 下划线
 *  * 10、AbsoluteSizeSpan 绝对大小(文本字体)
 *  * 11、DynamicDrawableSpan 设置图片,基于文本基线或底部对齐。
 *  * 12、ImageSpan 图片
 *  * 13、RelativeSizeSpan 相对大小(文本字体)
 *  * 14、ReplacementSpan 父类,一般不用
 *  * 15、ScaleXSpan 基于x轴缩放
 *  * 16、StyleSpan 字体样式:粗体、斜体等
 *  * 17、SubscriptSpan 下标(数学公式会用到)
 *  * 18、SuperscriptSpan 上标(数学公式会用到)
 *  * 19、TextAppearanceSpan 文本外貌(包括字体、大小、样式和颜色)
 *  * 20、TypefaceSpan 文本字体
 *  * 21、URLSpan 文本超链接
 *  *
 *
 */
class UISpanActivity : AppCompatActivity() {

    companion object {
        fun startActivity(context: Context) {
            context.startActivity(Intent(context, UISpanActivity::class.java))
        }
    }

    val binding: ActivityUiSpanBinding by lazy {
        ActivityUiSpanBinding.inflate(LayoutInflater.from(this))
    }


    private fun getGradientSpan(content: String, startColor: Int, endColor: Int, isLeftToRight: Boolean): SpannableStringBuilder {
        val spannableStringBuilder = SpannableStringBuilder(content)
        val span = KtLinearGradientFontSpan(startColor, endColor, isLeftToRight)
        spannableStringBuilder.setSpan(span, 0, spannableStringBuilder.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)

        // 若有需要可以在这里用SpanString系列的其他类,给文本添加下划线、超链接、删除线...等等效果
        return spannableStringBuilder
    }

    @RequiresApi(Build.VERSION_CODES.P)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)

        binding.btFinish.setOnClickListener {
            finish()
        }
        // 自定义颜色
        val customColor = Color.parseColor("#FF5722")
        // 自定义图片资源,假设你在 res/drawable 目录下有一张名为 ic_example 的图片
        val customDrawableResId = android.R.drawable.star_big_on
        val typeface = ResourcesCompat.getFont(this, R.font.alimama_shu_hei_ti_bold);
        val spanUtils = SpanUtils.getInstance(this)
        binding.tv01.text="设置字体"
        binding.tv01.setTypeface(typeface)

        binding.tv02.setTypeface(typeface)
        binding.tv02.setText(
            getGradientSpan(
                "修改字体渐变色", getColor(R.color.color_start), getColor(R.color.color_end),
                true
            ), TextView.BufferType.SPANNABLE
        )


        spanUtils.appendSpannableToTextView(binding.tv1, spanUtils.setTextTypeface( "起始文本 ",
            "设置字体渐变色",
            " 结束文本",getColor(R.color.color_start), getColor(R.color.color_end),true))

        // 1. 设置背景色
        val backgroundColorSpannable = spanUtils.setBackgroundColorSpan(
            "起始文本 ",
            "背景色文本",
            " 结束文本",
            customColor
        )
        spanUtils.appendSpannableToTextView(binding.tv1, backgroundColorSpannable)

        // 2. 设置可点击文本
        val clickableSpannable = spanUtils.setClickableSpan(
            "起始文本",
            "点击我弹出吐司",
            "起始文本",
            { content ->
                Toast.makeText(this@UISpanActivity, content, Toast.LENGTH_SHORT).show()
            }
        )
        spanUtils.appendSpannableToTextView(binding.tv2, clickableSpannable)
        // 开启 TextView 的可点击和可触摸事件
        binding.tv2.isClickable = true
        binding.tv2.isFocusable = true
        // 设置 MovementMethod 以处理点击事件
        binding.tv2.movementMethod = android.text.method.LinkMovementMethod.getInstance()

        // 3. 设置前景色
        val foregroundColorSpannable = spanUtils.setForegroundColorSpan(
            "起始文本 ",
            "前景色文本",
            " 结束文本",
            customColor
        )
        spanUtils.appendSpannableToTextView(binding.tv3, foregroundColorSpannable)

        // 4. 设置修饰效果(模糊和浮雕)
        binding.tv4.setLayerType(TextView.LAYER_TYPE_SOFTWARE, null)
        val maskFilterSpannable = spanUtils.setMaskFilterSpan(
            "起始文本 ",
            "修饰效果文本",
            " 结束文本"
        )
        spanUtils.appendSpannableToTextView(binding.tv4, maskFilterSpannable)

        // 5. 设置删除线
        val strikethroughSpannable = spanUtils.setStrikethroughSpan(
            "起始文本 ",
            "删除线文本",
            " 结束文本"
        )
        spanUtils.appendSpannableToTextView(binding.tv5, strikethroughSpannable)

        // 6. 设置下划线
        val underlineSpannable = spanUtils.setUnderlineSpan(
            "起始文本 ",
            "下划线文本",
            " 结束文本"
        )
        spanUtils.appendSpannableToTextView(binding.tv6, underlineSpannable)

        // 7. 设置绝对大小
        val absoluteSizeSpannable = spanUtils.setAbsoluteSizeSpan(
            "起始文本 ",
            "绝对大小文本",
            " 结束文本",
            24
        )
        spanUtils.appendSpannableToTextView(binding.tv7, absoluteSizeSpannable)

        // 8. 设置动态图片(基于文本基线或底部对齐)
        val dynamicDrawableSpannable = spanUtils.setDynamicDrawableSpan(
            "起始文本 ",
            "动态图片文本",
            " 结束文本",
            customDrawableResId,
            100,
            100
        )
        spanUtils.appendSpannableToTextView(binding.tv8, dynamicDrawableSpannable)

        // 9. 设置图片
        val imageSpannable = spanUtils.setImageSpan(
            "起始文本 ",
            "图片文本",
            " 结束文本",
            customDrawableResId,
            100,
            100
        )
        spanUtils.appendSpannableToTextView(binding.tv9, imageSpannable)

        // 10. 设置相对大小
        val relativeSizeSpannable = spanUtils.setRelativeSizeSpan(
            "起始文本 ",
            "相对大小文本",
            " 结束文本",
            1.5f
        )
        spanUtils.appendSpannableToTextView(binding.tv10, relativeSizeSpannable)

        // 11. 基于 x 轴缩放
        val scaleXSpannable = spanUtils.setScaleXSpan(
            "起始文本 ",
            "x 轴缩放文本",
            " 结束文本",
            2f
        )
        spanUtils.appendSpannableToTextView(binding.tv11, scaleXSpannable)

        // 12. 设置字体样式
        val styleSpannable = spanUtils.setStyleSpan(
            "起始文本 ",
            "字体样式文本",
            " 结束文本"
        )
        spanUtils.appendSpannableToTextView(binding.tv12, styleSpannable)

        // 13. 设置下标
        val subscriptSpannable = spanUtils.setSubscriptSpan(
            "起始文本 ",
            "下标文本",
            " 结束文本"
        )
        spanUtils.appendSpannableToTextView(binding.tv13, subscriptSpannable)

        // 14. 设置上标
        val superscriptSpannable = spanUtils.setSuperscriptSpan(
            "起始文本 ",
            "上标文本",
            " 结束文本"
        )
        spanUtils.appendSpannableToTextView(binding.tv14, superscriptSpannable)

        // 15. 设置文本外貌
        val textAppearanceSpannable = spanUtils.setTextAppearanceSpan(
            "起始文本 ",
            "文本外貌文本",
            " 结束文本"
        )
        spanUtils.appendSpannableToTextView(binding.tv15, textAppearanceSpannable)
      val tp=  ResourcesCompat.getFont(this, R.font.alimama_shu_hei_ti_bold)
        if (tp!=null){
            // 16. 设置字体
            val typefaceSpannable = spanUtils.setTypefaceSpan(
                "起始文本 ",
                "字体文本",
                " 结束文本",tp
            )
            spanUtils.appendSpannableToTextView(binding.tv16, typefaceSpannable)
        }


        // 17. 设置超链接
        val urlSpannable = spanUtils.setURLSpan(
            "起始文本 ",
            "超链接文本",
            " 结束文本",
            "https://www.example.com"
        )
        spanUtils.appendSpannableToTextView(binding.tv17, urlSpannable)


        binding.tv1.movementMethod = android.text.method.LinkMovementMethod.getInstance()
        binding.tv2.movementMethod = android.text.method.LinkMovementMethod.getInstance()
        binding.tv3.movementMethod = android.text.method.LinkMovementMethod.getInstance()
//        binding.tv4.movementMethod = android.text.method.LinkMovementMethod.getInstance()
        binding.tv5.movementMethod = android.text.method.LinkMovementMethod.getInstance()
        binding.tv6.movementMethod = android.text.method.LinkMovementMethod.getInstance()
        binding.tv7.movementMethod = android.text.method.LinkMovementMethod.getInstance()
        binding.tv8.movementMethod = android.text.method.LinkMovementMethod.getInstance()
        binding.tv9.movementMethod = android.text.method.LinkMovementMethod.getInstance()
        binding.tv10.movementMethod = android.text.method.LinkMovementMethod.getInstance()
        binding.tv11.movementMethod = android.text.method.LinkMovementMethod.getInstance()
        binding.tv12.movementMethod = android.text.method.LinkMovementMethod.getInstance()
        binding.tv13.movementMethod = android.text.method.LinkMovementMethod.getInstance()
        binding.tv14.movementMethod = android.text.method.LinkMovementMethod.getInstance()
        binding.tv15.movementMethod = android.text.method.LinkMovementMethod.getInstance()
        binding.tv16.movementMethod = android.text.method.LinkMovementMethod.getInstance()
        binding.tv17.movementMethod = android.text.method.LinkMovementMethod.getInstance()
        binding.tv18.movementMethod = android.text.method.LinkMovementMethod.getInstance()
        binding.tv19.movementMethod = android.text.method.LinkMovementMethod.getInstance()
    }

    }

总结

Android 提供了强大的Span 支持对TextView文本进行操作,这里对设置颜色,背景,点击事件,字体渐变,图文混合,下划线,中划线,模糊效果,超文本等效果一一实现,以此记录方便后续copy.

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,063评论 6 510
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,805评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,403评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,110评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,130评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,877评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,533评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,429评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,947评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,078评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,204评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,894评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,546评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,086评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,195评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,519评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,198评论 2 357

推荐阅读更多精彩内容