Android 文字全效果(颜色,背景,图文,大小,展开,跑马灯,虚化,点击,字体以及文字渐变色)

904 阅读13分钟

Android TextView Span简介<一>

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

image.png

image.png

Android 开发过程中文字作为页面展示的基础且主要的内容,在长期的开发过程中引发了各种各样的需求和效果.好多功能是做了就忘了.好记性不容烂笔头,也不想拾人牙慧了,所以总结一下文字展示的各种常规样式,方便自己也方便大家.

1.日常开发中各种文字效果梳理一遍

  • 字体更换
  • 跑马灯
  • 图文混合
    • Span方案
    • WebView+标签方案
  • 折叠/展开
  • 文字凸显
    • 字体颜色(纯色/渐变色)
    • 文字大小
    • 文字划线
    • 文字虚化
    • 文字点击

2.功能实现:

下面我就将日常用到的文字效果一一实现,以此记录.

2.1 TextView字体替换
val typeface = ResourcesCompat.getFont(this, R.font.alimama_shu_hei_ti_bold);
binding.tv01.text="设置字体"
binding.tv01.setTypeface(typeface)
2.2 跑马灯效果
1.自定义
/**
 * 跑马灯自定义
 */

public class MarqueeTextView extends AppCompatTextView {

    private boolean isFocused = false;

    public MarqueeTextView(Context context) {
        this(context, null);
    }

    public MarqueeTextView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MarqueeTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        setEllipsize(TextUtils.TruncateAt.MARQUEE);
    }

    @Override
    public boolean isFocused() {
        return isFocused;
    }

    public void setFocused(boolean isFocused) {
        this.isFocused = isFocused;
        setFocusableInTouchMode(isFocused);
        setFocusable(isFocused);
        setSelected(isFocused);
    }

    @Override
    protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
        if (focused) {
            super.onFocusChanged(focused, direction, previouslyFocusedRect);
        }
    }

    @Override
    public void onWindowFocusChanged(boolean hasWindowFocus) {
        if (hasWindowFocus) {
            super.onWindowFocusChanged(hasWindowFocus);
        }
    }
}

2.xml 引用


<com.wkq.ui.MarqueeTextView
    android:id="@+id/tv_6"
    android:textColor="@color/black"
    android:textSize="20dp"
    android:layout_width="160dp"
    android:layout_height="wrap_content"
    android:text="这是一个跑马灯展示"
    android:gravity="center_vertical"
    android:singleLine="true"
    android:ellipsize="marquee"
    android:marqueeRepeatLimit="marquee_forever"
    android:scrollHorizontally="true" />

3.代码设置

binding.tv6.setEllipsize(TextUtils.TruncateAt.MARQUEE)
//一次
 binding.tv6.setMarqueeRepeatLimit(1)
 binding.tv6.setMarqueeRepeatLimit(-1)
 binding.tv6.setFocused(true)



2.3 文字凸显

文字凸显的功能涉及的东西就很多了,涉及到颜色,背景,点击事件,字体渐变,图文混合,下划线,中划线,模糊效果,超文本等功能,其中功能又涉及到不同的实现方案.

2.3.1 自身属性
  • 设置字体 val typeface = ResourcesCompat.getFont(this, R.font.alimama_shu_hei_ti_bold); binding.tv.setTypeface(typeface)

  • 设置中划线下划线

    ```
      binding.tv4.text="展示中横线"
      binding.tv4.getPaint().setFlags(Paint.STRIKE_THRU_TEXT_FLAG)
    
      binding.tv5.text="展示下横线"
      binding.tv5.getPaint().setFlags(Paint.UNDERLINE_TEXT_FLAG)
      
    ```
    
  • 设置跑马灯

        binding.tv6.setEllipsize(TextUtils.TruncateAt.MARQUEE)
        binding.tv6.setMarqueeRepeatLimit(1)  //一次
        binding.tv6.setMarqueeRepeatLimit(-1)//无线
        binding.tv6.setFocused(true)
    
  • 设置省略号展示

    <TextView
        android:textColor="@color/black"
        android:layout_margin="10dp"
        android:layout_width="140dp"
        android:layout_height="wrap_content"
        android:singleLine="true"
        android:ellipsize="end"
        android:textSize="20dp"
        android:text="文字展示中横线打打 大大大爱滴阿文"/>
    
2.3.2 Span方式设置文字凸显
  • ForegroundColorSpan(实质是设置文字颜色) 设置部分字体颜色

    /**
     * 设置 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 会被处理为空字符串。
     \* @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
     }
    
  • 设置文字渐变色

     /**
      * 设置文字渐变色
      * @param startText String?
      * @param spanText String?
      * @param endText String?
      * @param startColor Int
      * @param endColor Int
      * @param isLeftToRight Boolean
      * @return SpannableString
      */
     fun setTextGradientColor(
         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 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 会被处理为空字符串。
 \* @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
     }
    
    
  • 设置文字中图片对其方式
    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
 }
  • 设置文字样式

      /**
       * 设置 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 的上标。
 /*\*
 \* 设置 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 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
 }

2.3 文字折叠/展开效果

111.png

222.png

思路:

自定义TextView 设置最多展示的文字数,当文字数超过设置的限制展示...然后尾部动态拼接一个可点击的全文 ClickableSpan ,点击这个文字绘制全部文字,然后尾部怕添加一个收起 ClickableSpan 点击收起收起文字.


class ExpandableTextView : AutoLinkTextView {

    private var mMaxCollapsedLength = 0

    //省略号
    private var mEllipsisHint: String = "..."

    //展开文字
    private var mExpandHint: String? = null

    //收起文字
    private var mCollapseHint: String? = null

    //展开/收起文字颜色
    var mHintColor: Int? = null

    //原始文本
    private var mOrigText: CharSequence? = null

    //截取后的文本
    private var mSubText: CharSequence? = null
    private var mCurrentState = 0

    //关闭状态
    val STATE_COLLAPSE = 0

    //展开状态
    val STATE_EXPAND = 1


    private var mOnExpandListener: OnExpandListener? = null

    private var copyText: String? = null

    init {
        mHintColor = R.color.color_auto_high_default
    }

    constructor(mContext: Context) : this(mContext, null)
    constructor(mContext: Context, mAttributeSet: AttributeSet?) : this(mContext, mAttributeSet, 0)
    constructor(mContext: Context, mAttributeSet: AttributeSet?, defStyleAttr: Int) : super(
        mContext, mAttributeSet, defStyleAttr
    ) {
        initAttributeSet(mAttributeSet!!)
        initTextView()
    }

    private fun initTextView() {
        mOrigText = text
        setTextInternal(getNewTextByConfig()!!, mBufferType)
    }


    //初始换数据
    @SuppressLint("ResourceAsColor")
    fun initAttributeSet(mAttributeSet: AttributeSet) {
        val array: TypedArray = context!!.obtainStyledAttributes(
            mAttributeSet, R.styleable.ExpandableTextView
        )
        mMaxCollapsedLength = array.getInt(
            R.styleable.ExpandableTextView_maxCollapsedLength, 100
        )
        mExpandHint = array.getString(R.styleable.ExpandableTextView_new_expandHint)
        mCollapseHint = array.getString(R.styleable.ExpandableTextView_new_collapseHint)
        mHintColor = array.getColor(
            R.styleable.ExpandableTextView_new_hintColor, R.color.color_auto_high_default
        )
        if (TextUtils.isEmpty(mExpandHint)) {
            mExpandHint = "全文"
        }
        if (TextUtils.isEmpty(mCollapseHint)) {
            mCollapseHint = "收起"
        }
        array.recycle()
        mCurrentState = STATE_COLLAPSE
    }


    fun setText(text: CharSequence?, state: Int) {
        mCurrentState = state
        mOrigText = text
        setText(getNewTextByConfig()!!)
    }

    private fun setTextInternal(text: CharSequence, type: BufferType) {
        super.setText(text, type)
    }

    //扩展的点击事件
    fun setExpandListener(listener: OnExpandListener) {
        mOnExpandListener = listener
    }

    private fun getNewTextByConfig(): CharSequence? {
        if (TextUtils.isEmpty(mOrigText)) return ""
        when (mCurrentState) {
            STATE_COLLAPSE -> {
                val lastBracket: Int
                if (mOrigText!!.length <= mMaxCollapsedLength) {
                    setClickableSpan(null, 0, 0)
                    return mOrigText
                }
                mSubText = mOrigText!!.subSequence(0, mMaxCollapsedLength)
                //自定义表情处理
                lastBracket = mSubText.toString().lastIndexOf("]")
                if (mMaxCollapsedLength - lastBracket <= 4) {
                    mMaxCollapsedLength = lastBracket + 1
                }
                //链接处理
                val links: ArrayList<TextViewLinkify.LinkSpec> =
                    TextViewLinkify.getGatherLinks(mAutoLinkMaskCompat, mOrigText.toString())
                if (links != null && links.size > 0) {
                    for (linkSpec in links) {
                        if (mMaxCollapsedLength < linkSpec.getEnd() && mMaxCollapsedLength > linkSpec.getStart()) {
                            mMaxCollapsedLength = linkSpec.getEnd()
                        }
                    }
                }
                mSubText = mOrigText!!.subSequence(0, mMaxCollapsedLength)
                var ssbCollapsed = SpannableStringBuilder(mSubText).append(mEllipsisHint) //省略号
                    .append(mExpandHint)
                setClickableSpan(
                    Clickable(OnClickListener { toggle() }), ssbCollapsed.length - 2,
                    ssbCollapsed.length
                )
                return ssbCollapsed
            }

            STATE_EXPAND -> {
                if (mOrigText!!.length <= mMaxCollapsedLength) {
                    setClickableSpan(null, 0, 0)
                    return mOrigText
                }
                val ssbExpand = SpannableStringBuilder(mOrigText).append(
                        """

                    $mCollapseHint
                    """.trimIndent()
                    )
                setClickableSpan(
                    Clickable(OnClickListener { toggle() }), ssbExpand.length - 2, ssbExpand.length
                )
                return ssbExpand
            }
        }
        return mOrigText
    }


    private fun toggle() {
        when (mCurrentState) {
            STATE_COLLAPSE -> mCurrentState = STATE_EXPAND
            STATE_EXPAND -> mCurrentState = STATE_COLLAPSE
        }
        if (mOnExpandListener != null) mOnExpandListener!!.onExpand(this)
        setTextInternal(getNewTextByConfig()!!, mBufferType)
    }

    inner class Clickable(
        private val mListener: OnClickListener
    ) : ClickableSpan(), OnClickListener, TouchableSpan {
        protected var mPressed = false

        override fun setPressed(pressed: Boolean) {
            mPressed = pressed
        }

        override fun onClick(v: View) {
            mListener.onClick(v)
        }

        override fun onLongClick(widget: View?) {

        }

        @SuppressLint("ResourceAsColor")
        override fun updateDrawState(ds: TextPaint) {
            ds.setColor(mHintColor!!)
            ds.isUnderlineText = false //去除超链接的下划线
        }
    }

    interface OnExpandListener {
        fun onExpand(view: ExpandableTextView?)
    }

}

源码地址,自行查看

2.4 WebView+html 实现图文混排,以及点击事件回调

思路:

不管自身属性和Span设置方式都是移动端拿到数据,做的定制化处理.当业务需求是页面展示需要客户定制化展示,以上的方案就不能满足业务需求了,这时候就需要Web+Html标签实现动态文字的需求了

要求:

  • 文字样式大小后台返回
  • 图文方式
  • 图片点击预览

实现流程:

后台返回标签数据---> 自己拼接成html数据(设置颜色大小宽高)-->js拦截对象,和js拦截方法-->注入html中

实现:

1.根据获取的标签数据组合到Html字符串中


val picUrl="<http://n.sinaimg.cn/ent/4_img/upload/1f0ce517/160/w1024h1536/20210413/f3cc-knqqqmv1022303.jpg>"

val pTagString = "<p>这是一个包含图片的段落:<img src="转存失败,建议直接上传图片文件 $picUrl" alt="示例图片转存失败,建议直接上传图片文件" style="width: 200px; height: 150px; object-fit: cover ;"> 图片展示结束。</p>"

String html = "<html><head><meta charset="UTF-8"><style type="text/css">html,body{padding:0px;margin:0px;font-size:" + fontSize + "px} img{background-size:contain|cover;width:100%;height: auto;} p{margin:0px;font-size:" + fontSize + "px}</style></head><body>" + pData + "</body></html>";

2.WebView加载Html

    webEleListener = webEleClickListener;
    String html = "<html><head><meta charset="UTF-8"><style type="text/css">html,body{padding:0px;margin:0px;font-size:" + fontSize + "px} img{background-size:contain|cover;width:100%;height: auto;} p{margin:0px;font-size:" + fontSize + "px}</style></head><body>" + pData + "</body></html>";
    webView.getSettings().setJavaScriptEnabled(true);

    webView.getSettings().setSupportZoom(false) ;// 禁止缩放
    webView.getSettings().setUseWideViewPort(false);// 不使用宽视口
    webView.getSettings().setLoadWithOverviewMode(false);// 不使用概述模式
    webView.addJavascriptInterface(new JsObject(), "injectedObject");
    webView.setWebViewClient(new WebViewClient() {
        @Override
        public void onPageFinished(WebView view, String url) {
            super.onPageFinished(view, url);
            String js = "javascript:(function(){" +
                    "var images = document.getElementsByTagName("img");" +
                    "var imageUrls = new Array(); " +
                    "for(var i=0; i<images.length; i++) {" +
                    "   imageUrls[i] = images[i].src; " +
                    "   images[i].pos = i; " +
                    "   images[i].onclick = function(){" +
                    "       window.injectedObject.openImage(this.src, this.pos);" +
                    "   }" +
                    "}" +
                    "window.injectedObject.setImageUrls(imageUrls);  " +
                    "})()";
            webView.loadUrl(js);
        }
    });
    webView.loadDataWithBaseURL(null, html, "text/html", "utf-8", null);

3:写js方法注入点击事件监听


    private static class JsObject {

        private String[] mImageUrls;

        @JavascriptInterface
        public void setImageUrls(String[] imageUrls) {
            this.mImageUrls = imageUrls;
        }

        @JavascriptInterface
        public void openImage(String src, int pos) {
            if (null != webEleListener) {
                webEleListener.onImageClick(src, pos, this.mImageUrls);
            }
            if (null != context) {
                if (null != src && !TextUtils.isEmpty(src) && null != mImageUrls) {
                    ArrayList<String> imgs = new ArrayList<>();
                    for (String imageUrl : mImageUrls) {
                        imgs.add(imageUrl);
                    }
                    if (null != imgs && imgs.size() > 0) {
                        previewImg(context, src, imgs);
                    }
                }
            }
        }
    }

注入 js对象
webView\.addJavascriptInterface(new JsObject(), "injectedObject");
注入 js监听方法
webView\.setWebViewClient(new WebViewClient() {
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
String js = "javascript:(function(){" +
"var images = document.getElementsByTagName("img");" +
"var imageUrls = new Array(); " +
"for(var i=0; i\<images.length; i++) {" +
"   imageUrls\[i] = images\[i].src; " +
"   images\[i].pos = i; " +
"   images\[i].onclick = function(){" +
"       window\.injectedObject.openImage(this.src, this.pos);" +
"   }" +
"}" +
"window\.injectedObject.setImageUrls(imageUrls);  " +
"})()";
webView\.loadUrl(js);
}
});

总结

本文围绕 Android 开发中 TextView 的文字效果展开,全面梳理了字体更换、跑马灯、图文混合、折叠 / 展开、文字凸显等常见需求。通过提供具体的代码示例,详细介绍了利用自身属性、Span 方式、自定义 View 等多种实现方案。开发者可依据实际需求参考这些方案,实现多样化的文字展示效果,同时给出的相关参考链接也为进一步深入学习提供了方向。

使用各种 Span 类可以实现丰富的文字样式效果。

  • ForegroundColorSpan:设置部分字体的颜色,通过传入起始文本、要设置样式的中间文本、结束文本和颜色值,创建 SpannableString 并应用 ForegroundColorSpan
  • BackgroundColorSpan:设置局部文字的背景色,原理与设置前景色类似。
  • 文字渐变色:通过自定义的 KtLinearGradientFontSpan 类,实现文字的渐变色效果。
  • 局部文字点击事件:使用 ClickableSpan 类,为指定文本设置点击事件,当点击该部分文本时触发相应的回调函数。
  • 文字高斯效果:使用 MaskFilterSpan 结合 BlurMaskFilter 或 EmbossMaskFilter,实现文字的模糊或浮雕效果。
  • 文字划线效果:分别使用 StrikethroughSpan 和 UnderlineSpan 实现文字的中划线和下划线效果。
  • 文字大小设置:通过 AbsoluteSizeSpan 和 RelativeSizeSpan 分别设置文字的绝对大小和相对大小。
  • 文字中图片对齐方式:使用 DynamicDrawableSpan 类,设置图片与文字的对齐方式,如基线对齐或底部对齐。
  • 图文混合:使用 ImageSpan

相关参考链接