美文网首页
Android富文本的实现的几种方式

Android富文本的实现的几种方式

作者: BlueSocks | 来源:发表于2022-11-03 21:59 被阅读0次

    在Android开发过程中,最常见的富文本场景一般都是变色,点击跳转,或者局部变大,而我们实现的方式通常分为两种。
    一种是Html的方式定义在string中,通过html标签变色,变大,通过占位符填充数据。一般常用于有国际化的需求。
    另一种是CharSequence的setSpan设置自定义Span。功能更强大,细读也更细,便于精准操作。一般用于没有国际化需求的地方。
    为什么有国际化相关的要求,是因为一般setSpan的方式都是添加或者根据索引替换对应的文本,如果国际化之后中英马等语言的顺序都变了,自然效果就不同了。当然也可以通过判断语言进行不同的操作。

    一,Html的方式实现

    1.1 占位符的处理

    先看看string xml中如何处理占位符
    %N代表第N个参数,如%3代表的是第三个参数;$是结束符;

    <string name="string_test_1">学号:%1$d ;姓名:%2$s ;成绩:%3$.2f</string>
    

    使用的时候:

    String testStr = getResources().getString(R.string.string_test_1);
    String result = String.format(testStr,1001,"张三",9.235);
    System.out.println(result);
    

    1.2 Html的占位符

    和上面的差不多:

        <string name="purchase_points"><![CDATA[ <font color="#767676">Purchase with</font> 
        <font color="#FF5E75">%s</font><font color="#767676"> points?</font>]]></string>
    

    使用:

        String formatPoints = PointFormatUtils.formatPoints(points);
        String result = String.format(getResources().getString(R.string.purchase_points),formatPoints);
        tv_message.setText(Html.fromHtml(result));
    

    结论:
    能实现变色,简单的变大等简单功能,由于TextView不能解析更多的Html标签,由此还出现了一些库,让TextView支持更多标签。
    如果有一些自定义的需求,我们可以使用自定义标签+自定义标签的功能,例如Html中的自定义字体。

    1.3 自定义Html标签

    先定义自定义字体的Span类

    /**
     * 系统原生的TypefaceSpan只能使用原生的默认字体
     * 如果使用自定义的字体,通过这个来实现
     */
    public class MyTypefaceSpan extends MetricAffectingSpan {
    
        private final Typeface typeface;
    
        public MyTypefaceSpan(final Typeface typeface) {
            this.typeface = typeface;
        }
    
        @Override
        public void updateDrawState(final TextPaint drawState) {
            apply(drawState);
        }
    
        @Override
        public void updateMeasureState(final TextPaint paint) {
            apply(paint);
        }
    
        private void apply(final Paint paint) {
            final Typeface oldTypeface = paint.getTypeface();
            final int oldStyle = oldTypeface != null ? oldTypeface.getStyle() : 0;
            int fakeStyle = oldStyle & ~typeface.getStyle();
            if ((fakeStyle & Typeface.BOLD) != 0) {
                paint.setFakeBoldText(true);
            }
            if ((fakeStyle & Typeface.ITALIC) != 0) {
                paint.setTextSkewX(-0.25f);
            }
            paint.setTypeface(typeface);
        }
    
    }
    

    自定义标签:

    /**
     * Html的TextView标签解释
     * <face></face>
     */
    public class TypeFaceLabel implements Html.TagHandler {
        private Typeface typeface;
        private int startIndex = 0;
        private int stopIndex = 0;
    
        public TypeFaceLabel(Typeface typeface) {
            this.typeface = typeface;
        }
    
        @Override
        public void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader) {
            if (tag.toLowerCase().equals("face")) {
                if (opening) {
                    startIndex = output.length();
                } else {
                    stopIndex = output.length();
                    //使用的是自定义的字体来实现
                    output.setSpan(new MyTypefaceSpan(typeface), startIndex, stopIndex, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
                }
            }
        }
    
    }
    

    定义Xml并使用,注意自定义face标签

      String content = "<font color=\"#000000\">HR from </font>" +
                        "<face><font color=\"#0689FB\">" + item.employer_name + "</font></face>" +
                        "<font color=\"#000000\"> has viewed your resume.</font>";
    
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
              tv_resume_log_content.setText(Html.fromHtml(content, Html.FROM_HTML_MODE_LEGACY, null, new TypeFaceLabel(TypefaceUtil.getSFSemobold(mContext))));
            } else {
              tv_resume_log_content.setText(Html.fromHtml(content, null, new TypeFaceLabel(TypefaceUtil.getSFSemobold(mContext))));
        }
    

    如果想实现其他的变大 下划线 中划线等Span效果,都可以通过自定义的Html标签+自定义Span实现相应的效果。

    二,Span的几种实现方式

    虽然通过Html的方式可以实现各种效果,但是定义的时候也太过复杂,各种定义Span 定义标签之类的,有没有更简单和直接的?
    有,我们直接封装Span就行了。

    2.1 java - SpanUtil

    在Java中我们可以封装工具类一个如下:

    /**
     * String字符串通过区间来改变颜色,大小,字体,下划线等
     */
    public class SpanUtils {
    
        private static final SpanUtils ourInstance = new SpanUtils();
    
        public static SpanUtils getInstance() {
            return ourInstance;
        }
    
        private SpanUtils() {
        }
    
        /**
         * 变大变小
         */
        public CharSequence toSizeSpan(CharSequence charSequence, int start, int end, float scale) {
    
            SpannableString spannableString = new SpannableString(charSequence);
    
            spannableString.setSpan(
                    new RelativeSizeSpan(scale),
                    start,
                    end,
                    Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
    
            return spannableString;
        }
    
        /**
         * 变色
         */
        public CharSequence toColorSpan(CharSequence charSequence, int start, int end, int color) {
    
            SpannableString spannableString = new SpannableString(charSequence);
    
            spannableString.setSpan(
                    new ForegroundColorSpan(color),
                    start,
                    end,
                    Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
    
            return spannableString;
        }
    
        /**
         * 变背景色
         */
        public CharSequence toBackgroundColorSpan(CharSequence charSequence, int start, int end, int color) {
    
            SpannableString spannableString = new SpannableString(charSequence);
    
            spannableString.setSpan(
                    new BackgroundColorSpan(color),
                    start,
                    end,
                    Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
    
            return spannableString;
        }
    
        private long mLastClickTime = 0;
        public static final int TIME_INTERVAL = 1000;
    
        /**
         * 可点击-带下划线
         */
        public CharSequence toClickSpan(CharSequence charSequence, int start, int end, int color, boolean needUnderLine, OnSpanClickListener listener) {
    
            SpannableString spannableString = new SpannableString(charSequence);
    
            ClickableSpan clickableSpan = new ClickableSpan() {
                @Override
                public void onClick(@NonNull View widget) {
                    if (listener != null) {
                        //防止重复点击
                        if (System.currentTimeMillis() - mLastClickTime >= TIME_INTERVAL) {
                            //to do
                            listener.onClick(charSequence.subSequence(start, end));
    
                            mLastClickTime = System.currentTimeMillis();
                        }
    
                    }
                }
    
                @Override
                public void updateDrawState(@NonNull TextPaint ds) {
                    ds.setColor(color);
                    ds.setUnderlineText(needUnderLine);
                }
            };
    
            spannableString.setSpan(
                    clickableSpan,
                    start,
                    end,
                    Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
    
            return spannableString;
        }
    
        public interface OnSpanClickListener {
            void onClick(CharSequence charSequence);
        }
    
    
        /**
         * 变成自定义的字体
         */
        public CharSequence toCustomTypeFaceSpan(CharSequence charSequence, int start, int end, Typeface typeface) {
    
            SpannableString spannableString = new SpannableString(charSequence);
    
            spannableString.setSpan(
                    new MyTypefaceSpan(typeface),
                    start,
                    end,
                    Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
    
            return spannableString;
        }
    
    }
    

    2.2 kotlin扩展

    /**
     * 将一段文字中指定range的文字改变大小
     * @param range 要改变大小的文字的范围
     * @param scale 缩放值,大于1,则比其他文字大;小于1,则比其他文字小;默认是1.5
     */
    fun CharSequence.toSizeSpan(range: IntRange, scale: Float = 1.5f): CharSequence {
        return SpannableString(this).apply {
            setSpan(
                RelativeSizeSpan(scale),
                range.start,
                range.endInclusive,
                Spannable.SPAN_INCLUSIVE_EXCLUSIVE
            )
        }
    }
    
    /**
     * 将一段文字中指定range的文字改变前景色
     * @param range 要改变前景色的文字的范围
     * @param color 要改变的颜色,默认是红色
     */
    fun CharSequence.toColorSpan(range: IntRange, color: Int = Color.RED): CharSequence {
        return SpannableString(this).apply {
            setSpan(
                ForegroundColorSpan(color),
                range.start,
                range.endInclusive,
                Spannable.SPAN_INCLUSIVE_EXCLUSIVE
            )
        }
    }
    
    /**
     * 将一段文字中指定range的文字改变背景色
     * @param range 要改变背景色的文字的范围
     * @param color 要改变的颜色,默认是红色
     */
    fun CharSequence.toBackgroundColorSpan(range: IntRange, color: Int = Color.RED): CharSequence {
        return SpannableString(this).apply {
            setSpan(
                BackgroundColorSpan(color),
                range.start,
                range.endInclusive,
                Spannable.SPAN_INCLUSIVE_EXCLUSIVE
            )
        }
    }
    
    /**
     * 将一段文字中指定range的文字添加删除线
     * @param range 要添加删除线的文字的范围
     */
    fun CharSequence.toStrikeThrougthSpan(range: IntRange): CharSequence {
        return SpannableString(this).apply {
            setSpan(
                StrikethroughSpan(),
                range.start,
                range.endInclusive,
                Spannable.SPAN_INCLUSIVE_EXCLUSIVE
            )
        }
    }
    
    /**
     * 将一段文字中指定range的文字添加颜色和点击事件
     * @param range 目标文字的范围
     */
    fun CharSequence.toClickSpan(
        range: IntRange,
        color: Int = Color.RED,
        isUnderlineText: Boolean = false,
        clickAction: (() -> Unit)?
    ): CharSequence {
        return SpannableString(this).apply {
            val clickableSpan = object : ClickableSpan() {
                override fun onClick(widget: View) {
                    clickAction?.invoke()
                }
    
                override fun updateDrawState(ds: TextPaint) {
                    ds.color = color
                    ds.isUnderlineText = isUnderlineText
                }
            }
            setSpan(clickableSpan, range.start, range.endInclusive, Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
        }
    }
    
    /**
     * 将一段文字中指定range的文字添加style效果
     * @param range 要添加删除线的文字的范围
     */
    fun CharSequence.toStyleSpan(style: Int = Typeface.BOLD, range: IntRange): CharSequence {
        return SpannableString(this).apply {
            setSpan(
                StyleSpan(style),
                range.start,
                range.endInclusive,
                Spannable.SPAN_INCLUSIVE_EXCLUSIVE
            )
        }
    }
    
    /**
     * 将一段文字中指定range的文字添加自定义效果
     * @param range 要添加删除线的文字的范围
     */
    fun CharSequence.toCustomTypeFaceSpan(typeface: Typeface, range: IntRange): CharSequence {
        return SpannableString(this).apply {
            setSpan(
                CustomTypefaceSpan(typeface),
                range.start,
                range.endInclusive,
                Spannable.SPAN_INCLUSIVE_EXCLUSIVE
            )
        }
    }
    
    
    /**
     * 将一段文字中指定range的文字添加自定义效果,可以设置对齐方式,可以设置margin
     * @param range
     */
    fun CharSequence.toImageSpan(
        imageRes: Int,
        range: IntRange,
        verticalAlignment: Int = 0,  //默认底部  4是垂直居中
        maginLeft: Int = 0,
        marginRight: Int = 0,
        width: Int = 0,
        height: Int = 0
    ): CharSequence {
        return SpannableString(this).apply {
            setSpan(
                MiddleIMarginImageSpan(
                    CommUtils.getDrawable(imageRes)
                        .apply {
                            setBounds(0, 0, if (width == 0) getIntrinsicWidth() else width, if (height == 0) getIntrinsicHeight() else height)
                        },
                    verticalAlignment,
                    maginLeft,
                    marginRight
                ),
                range.start,
                range.endInclusive,
                Spannable.SPAN_INCLUSIVE_EXCLUSIVE
            )
        }
    }
    

    扩展方法的使用

      mBinding.tvTextSpan1.text = "演示一下appendXX方法的用法\n"
            mBinding.tvTextSpan1.appendSizeSpan("变大变大", 1.5f)
                .appendColorSpan("我要变色", color = Color.parseColor("#f0aafc"))
                .appendBackgroundColorSpan("我是有底色的", color = Color.parseColor("#cacee0"))
                .appendStrikeThrougthSpan("添加删除线哦哦哦哦")
                .appendClickSpan("来点我一下试试啊", isUnderlineText = true, clickAction = {
                    toast("哎呀,您点到我了呢,嘿嘿")
                })
                .appendImageSpan(R.mipmap.ic_launcher)  //默认的大图什么都不加 默认在底部对齐
                .appendStyleSpan("我是粗体的") //可以是默认粗体 斜体等
                .appendImageSpan(R.mipmap.ic_launcher_round, 4, width = dp2px(35f), height = dp2px(35f))//4是居中的,限制Drawable
                .appendCustomTypeFaceSpan("Xiao mi Hua wei", TypefaceUtil.getSFFlower(mActivity))  //自定义字体文件
                //默认底部对齐,加左右margin
                .appendImageSpan(R.mipmap.iv_me_red_packet, maginLeft = dp2px(10f), marginRight = dp2px(10f))
                //添加删除线
                .appendStrikeThrougthSpan("添加删除线哦哦哦哦添加删除线哦哦哦哦")
    

    2.3 kotlin DSL方式

    如果是使用Kotlin的语言开发,那么还有更简单的DSL封装方式:

    第一层的DSL接口

    interface DslSpannableStringBuilder {
        //增加一段文字
        fun addText(text: String, method: (DslSpanBuilder.() -> Unit)? = null)
    
        //添加一个图标
        fun addImage(imageRes: Int, verticalAlignment: Int = 0, maginLeft: Int = 0, marginRight: Int = 0, width: Int = 0, height: Int = 0)
    }
    

    第一层的DSL接口实现

    class DslSpannableStringBuilderImpl : DslSpannableStringBuilder {
    
        private val builder = SpannableStringBuilder()
    
        //添加文本
        override fun addText(text: String, method: (DslSpanBuilder.() -> Unit)?) {
    
            val spanBuilder = DslSpanBuilderImpl()
            method?.let { spanBuilder.it() }
    
            var charSeq: CharSequence = text
    
            spanBuilder.apply {
                if (issetColor) {
                    charSeq = charSeq.toColorSpan(0..text.length, textColor)
                }
                if (issetBackground) {
                    charSeq = charSeq.toBackgroundColorSpan(0..text.length, textBackgroundColor)
                }
                if (issetScale) {
                    charSeq = charSeq.toSizeSpan(0..text.length, scaleSize)
                }
                if (isonClick) {
                    charSeq = charSeq.toClickSpan(0..text.length, textColor, isuseUnderLine, onClick)
                }
                if (issetTypeface) {
                    charSeq = charSeq.toCustomTypeFaceSpan(typefaces, 0..text.length)
                }
                if (issetStrikethrough) {
                    charSeq = charSeq.toStrikeThrougthSpan(0..text.length)
                }
    
                builder.append(charSeq)
            }
        }
    
        //添加图标
        override fun addImage(imageRes: Int, verticalAlignment: Int, maginLeft: Int, marginRight: Int, width: Int, height: Int) {
            var charSeq: CharSequence = "1"
            charSeq = charSeq.toImageSpan(imageRes, 0..1, verticalAlignment, maginLeft, marginRight, width, height)
            builder.append(charSeq)
        }
    
        fun build(): SpannableStringBuilder {
            return builder
        }
    
    }
    

    第二层Text的DSL接口

    interface DslSpanBuilder {
        //设置文字颜色
        fun setColor(color: Int = 0)
    
        //设置点击事件
        fun setClick(useUnderLine: Boolean = true, onClick: (() -> Unit)?)
    
        //设置缩放大小
        fun setScale(scale: Float = 1.0f)
    
        //设置自定义字体
        fun setTypeface(typeface: Typeface)
    
        //是否需要中划线
        fun setStrikethrough(isStrikethrough: Boolean = false)
    
        //设置背景
        fun setBackground(color: Int = Color.TRANSPARENT)
    
    }
    

    第二层Text的DSL接口实现

    class DslSpanBuilderImpl : DslSpanBuilder {
        var issetColor = false
        var textColor: Int = Color.BLACK
    
        var isonClick = false
        var isuseUnderLine = false
        var onClick: (() -> Unit)? = null
    
        var issetScale = false
        var scaleSize = 1.0f
    
        var issetTypeface = false
        var typefaces: Typeface = Typeface.DEFAULT
    
        var issetStrikethrough = false
    
        var issetBackground = false
        var textBackgroundColor = 0
    
        override fun setColor(color: Int) {
            issetColor = true
            textColor = color
        }
    
        override fun setClick(useUnderLine: Boolean, onClick: (() -> Unit)?) {
            isonClick = true
            isuseUnderLine = useUnderLine
            this.onClick = onClick
        }
    
        override fun setScale(scale: Float) {
            issetScale = true
            scaleSize = scale
        }
    
        override fun setTypeface(typeface: Typeface) {
            issetTypeface = true
            typefaces = typeface
        }
    
        override fun setStrikethrough(isStrikethrough: Boolean) {
            issetStrikethrough = isStrikethrough
        }
    
        override fun setBackground(color: Int) {
            issetBackground = true
            textBackgroundColor = color
        }
    
    }
    

    创建TextVuew的扩展入口

    //为 TextView 创建扩展函数,其参数为接口的扩展函数
    fun TextView.buildSpannableString(init: DslSpannableStringBuilder.() -> Unit) {
        //具体实现类
        val spanStringBuilderImpl = DslSpannableStringBuilderImpl()
        spanStringBuilderImpl.init()
        movementMethod = LinkMovementMethod.getInstance()
        //通过实现类返回SpannableStringBuilder
        text = spanStringBuilderImpl.build()
    }
    

    使用:

        mBinding.tvTextSpan4.buildSpannableString {
                addText("我已详细阅读并同意")
                addText("测试红色的文字颜色") {
                    setColor(Color.RED)
                }
                addText("测试白色文字加上灰色背景") {
                    setColor(Color.WHITE)
                    setBackground(Color.GRAY)
                }
                addText("测试文本变大了") {
                    setColor(Color.DKGRAY)
                    setScale(1.5f)
                }
                addImage(R.mipmap.ic_launcher)
                addText("测试可以点击的文本") {
                    setClick(true) {
                        toast("点击文本拉啦啦")
                    }
                }
                addImage(R.mipmap.ic_launcher_round, 5, dp2px(10f), dp2px(10f), dp2px(35f), dp2px(35f))
                addText("Test Custom Typeface Font is't Success?") {
                    setTypeface(TypefaceUtil.getSFFlower(mActivity))
                }
                addText("测试中划线是否生效") {
                    setStrikethrough(true)
                }
            }
    

    总结

    如果是顺序固定,效果复杂,那么可以用Span的方式。
    如果顺序不固定(如国际化)那么可以使用Html的方式。

    来自:https://juejin.cn/post/7100761278141956133

    相关文章

      网友评论

          本文标题:Android富文本的实现的几种方式

          本文链接:https://www.haomeiwen.com/subject/vwaetdtx.html