3

Android TextView加载Html自定义标签实现富文本效果

 2 years ago
source link: http://www.androidchina.net/12589.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Android实现TextView加载Html标签的逻辑

关于Android富文本的实现,之前的文章也有写到过几种方式。那么在国际化前提下的富文本实现,最好的方式是通过Html来指定实现富文本了。

由于英文,马来语,印地语它们的语法顺序都和中文不同,如果按Span的方法来指定索引变色来实现,几乎是不现实的。例如:

HR from Custom Company has viewed your resume.

Custom Company的人事专员查看了你的简历。

同样的话,我们需要把公司的字段变色方法,替换字体,如果用富文本的实现方法,我们要获取到当前系统语言,根据语言if else 来写不同的substring的方法去找索引替换Span。

其实我们用Html一样可以实现富文本的实现,我们Android的TextView的 Html.fromHtml 方法是可以解析部分标签的。

9f8cc89541a64a2e9f3035bd13f22624~tplv-k3u1fbpfcp-zoom-in-crop-mark:1304:0:0:0.awebp

如果不支持的标签我们可以自定义实现,还可以自定义标签实现原本Html实现不了的效果。

一、单标签的实现

自定义字体的工具库

/**
 * 系统原生的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);
            }
        }
    }

}

实现的方式:

  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))));
    }

9f659004df1041a195c805c56ae7d217~tplv-k3u1fbpfcp-zoom-in-crop-mark:1304:0:0:0.awebp

二、多标签的实现

这样确实是可以实现一个简单的Html的标签解析了,但是这种方式只能解析一个自定义的Tag,如果我们想支持多个自定义Tag就会出现start end 索引的冲突问题,我们需要使用一个栈的数据结构来保存不同tag的索引。

工具类如下:


/**
 * 支持的标签为
 * <del>xxx</del>  中划线
 * <size value='16'>xxx</size>  自定义大小文本
 * <face>xxx</face>       自定义字体
 */
public class CustomerLableHandler implements Html.TagHandler {

    private Typeface typeface;
    private int imgRes;

    public CustomerLableHandler(Typeface typeface, int imgRes) {
        this.typeface = typeface;
        this.imgRes = imgRes;
    }

    /**
     * html 标签的开始下标,为了支持多个标签,使用栈管理开始下标
     */
    private Stack<Integer> startIndex;

    /**
     * html的标签的属性值
     */
    private Stack<String> propertyValue;

    @Override
    public void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader) {
        if (opening) {
            handlerStartTAG(tag, output, xmlReader);
        } else {
            handlerEndTAG(tag, output);
        }
    }

    /**
     * 处理开始的标签位
     */
    private void handlerStartTAG(String tag, Editable output, XMLReader xmlReader) {
        if (tag.equalsIgnoreCase("del")) {
            handlerStartDEL(output);
        } else if (tag.equalsIgnoreCase("size")) {
            handlerStartSIZE(output, xmlReader);
        }else if (tag.equalsIgnoreCase("face")){
            handleStartFACE(output);
        }else if (tag.equalsIgnoreCase("icon")){
            handleStartICON(output);
        }
    }

    /**
     * 处理结尾的标签位
     */
    private void handlerEndTAG(String tag, Editable output) {
        if (tag.equalsIgnoreCase("del")) {
            handlerEndDEL(output);
        } else if (tag.equalsIgnoreCase("size")) {
            handlerEndSIZE(output);
        }else if (tag.equalsIgnoreCase("face")){
            handleEndFACE(output);
        }else if (tag.equalsIgnoreCase("icon")){
            handleEndICON(output);
        }
    }

    // =======================  自定义Icon begin ↓ =========================

    private void handleStartICON(Editable output) {
        if (startIndex == null) {
            startIndex = new Stack<>();
        }
        startIndex.push(output.length());
    }

    private void handleEndICON(Editable output) {

        Drawable drawable = CommUtils.getDrawable(imgRes);
        drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
        MiddleIMarginImageSpan imageSpan = new MiddleIMarginImageSpan(drawable, 4, 0, 0);

        output.setSpan(imageSpan, startIndex.pop(), output.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    }

    // =======================  自定义Icon end ↑ =========================

    // =======================  自定义字体 begin ↓ =========================

    private void handleStartFACE(Editable output) {
        if (startIndex == null) {
            startIndex = new Stack<>();
        }
        startIndex.push(output.length());
    }

    private void handleEndFACE(Editable output) {
        output.setSpan(new CustomTypefaceSpan(typeface), startIndex.pop(), output.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    }

    // =======================  自定义字体 end ↑ =========================

    // =======================  中划线处理 begin ↓ =========================

    private void handlerStartDEL(Editable output) {
        if (startIndex == null) {
            startIndex = new Stack<>();
        }
        startIndex.push(output.length());
    }

    //中划线的Span
    private void handlerEndDEL(Editable output) {
        output.setSpan(new StrikethroughSpan(), startIndex.pop(), output.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    }

    // =======================  中划线处理 end ↑ =========================

    // =======================  文本大小设置 begin ↓ =========================

    private void handlerStartSIZE(Editable output, XMLReader xmlReader) {
        if (startIndex == null) {
            startIndex = new Stack<>();
        }
        startIndex.push(output.length());

        if (propertyValue == null) {
            propertyValue = new Stack<>();
        }
        //获取自定义标签内部的属性值
        propertyValue.push(getProperty(xmlReader, "value"));
    }

    private void handlerEndSIZE(Editable output) {

        if (!propertyValue.isEmpty()) {
            try {
                int value = Integer.parseInt(propertyValue.pop());
                output.setSpan(new AbsoluteSizeSpan(CommUtils.dip2px(value)), startIndex.pop(), output.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    // =======================  文本大小设置 end ↑ =========================

    /**
     * 利用反射获取html标签的属性值
     */
    private String getProperty(XMLReader xmlReader, String property) {
        try {
            Field elementField = xmlReader.getClass().getDeclaredField("theNewElement");
            elementField.setAccessible(true);
            Object element = elementField.get(xmlReader);
            Field attsField = element.getClass().getDeclaredField("theAtts");
            attsField.setAccessible(true);
            Object atts = attsField.get(element);
            Field dataField = atts.getClass().getDeclaredField("data");
            dataField.setAccessible(true);
            String[] data = (String[]) dataField.get(atts);
            Field lengthField = atts.getClass().getDeclaredField("length");
            lengthField.setAccessible(true);
            int len = (Integer) lengthField.get(atts);

            for (int i = 0; i < len; i++) {
                // 这边的property换成你自己的属性名就可以了
                if (property.equals(data[i * 5 + 1])) {
                    return data[i * 5 + 4];
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

}

内部的ImageSpan是自行封装的可以居中,居下,设置margin的Span,实现如下:

public class MiddleIMarginImageSpan extends AlignMiddleImageSpan {

    private int mSpanMarginLeft = 0;
    private int mSpanMarginRight = 0;
    private int mOffsetY = 0;

    public MiddleIMarginImageSpan(Drawable d, int verticalAlignment, int marginLeft, int marginRight) {
        this(d, verticalAlignment, marginLeft, marginRight, 0);
    }

    public MiddleIMarginImageSpan(Drawable d, int verticalAlignment, int marginLeft, int marginRight, int offsetY) {
        super(d, verticalAlignment);
        mSpanMarginLeft = marginLeft;
        mSpanMarginRight = marginRight;
        mOffsetY = offsetY;
    }

    @Override
    public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
        if (mSpanMarginLeft != 0 || mSpanMarginRight != 0) {
            super.getSize(paint, text, start, end, fm);
            Drawable d = getDrawable();
            return d.getIntrinsicWidth() + mSpanMarginLeft + mSpanMarginRight;
        } else {
            return super.getSize(paint, text, start, end, fm);
        }
    }

    @Override
    public void draw(Canvas canvas, CharSequence text, int start, int end,
                     float x, int top, int y, int bottom, Paint paint) {
        canvas.save();
        canvas.translate(0, mOffsetY);
        // marginRight不用专门处理,只靠getSize()中改变即可
        super.draw(canvas, text, start, end, x + mSpanMarginLeft, top, y, bottom, paint);
        canvas.restore();
    }
}

public class AlignMiddleImageSpan extends ImageSpan {

    public static final int ALIGN_MIDDLE = 4; // 默认垂直居中

    /**
     * 规定这个Span占几个字的宽度
     */
    private float mFontWidthMultiple = -1f;

    /**
     * 是否避免父类修改FontMetrics,如果为 false 则会走父类的逻辑, 会导致FontMetrics被更改
     */
    private boolean mAvoidSuperChangeFontMetrics = false;

    @SuppressWarnings("FieldCanBeLocal")
    private int mWidth;
    private Drawable mDrawable;
    private int mDrawableTintColorAttr;

    /**
     * @param d                 作为 span 的 Drawable
     * @param verticalAlignment 垂直对齐方式, 如果要垂直居中, 则使用 {@link #ALIGN_MIDDLE}
     */
    public AlignMiddleImageSpan(Drawable d, int verticalAlignment) {
        this(d, verticalAlignment, 0);
    }

    /**
     * @param d                 作为 span 的 Drawable
     * @param verticalAlignment 垂直对齐方式, 如果要垂直居中, 则使用 {@link #ALIGN_MIDDLE}
     * @param fontWidthMultiple 设置这个Span占几个中文字的宽度, 当该值 > 0 时, span 的宽度为该值*一个中文字的宽度; 当该值 <= 0 时, span 的宽度由 {@link #mAvoidSuperChangeFontMetrics} 决定
     */
    public AlignMiddleImageSpan(@NonNull Drawable d, int verticalAlignment, float fontWidthMultiple) {
        super(d.mutate(), verticalAlignment);
        mDrawable = getDrawable();
        if (fontWidthMultiple >= 0) {
            mFontWidthMultiple = fontWidthMultiple;
        }
    }

    @Override
    public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
        if (mAvoidSuperChangeFontMetrics) {
            Drawable d = getDrawable();
            Rect rect = d.getBounds();
            mWidth = rect.right;
        } else {
            mWidth = super.getSize(paint, text, start, end, fm);
        }
        if (mFontWidthMultiple > 0) {
            mWidth = (int) (paint.measureText("子") * mFontWidthMultiple);
        }
        return mWidth;
    }

    @Override
    public void draw(Canvas canvas, CharSequence text, int start, int end,
                     float x, int top, int y, int bottom, Paint paint) {
        if (mVerticalAlignment == ALIGN_MIDDLE) {
            Drawable d = mDrawable;
            canvas.save();

            Paint.FontMetricsInt fontMetricsInt = paint.getFontMetricsInt();
            int fontTop = y + fontMetricsInt.top;
            int fontMetricsHeight = fontMetricsInt.bottom - fontMetricsInt.top;
            int iconHeight = d.getBounds().bottom - d.getBounds().top;
            int iconTop = fontTop + (fontMetricsHeight - iconHeight) / 2;
            canvas.translate(x, iconTop);
            d.draw(canvas);
            canvas.restore();
        } else {
            super.draw(canvas, text, start, end, x, top, y, bottom, paint);
        }
    }

    /**
     * 是否避免父类修改FontMetrics,如果为 false 则会走父类的逻辑, 会导致FontMetrics被更改
     */
    public void setAvoidSuperChangeFontMetrics(boolean avoidSuperChangeFontMetrics) {
        mAvoidSuperChangeFontMetrics = avoidSuperChangeFontMetrics;
    }

}

到处就完成定义啦,这里我只定义了Drawable的加载 字体的替换,字体大小设置,中划线的实现,还有其他的效果,大家可以自行实现的,注释很详细。

英文的string:

    <string name="hr_view_resume"> <![CDATA[ <font color=\"#000000\">HR from </font>
    <face><font color=\"#0689FB\">%1$s</font></face>
    <font color=\"#000000\"> has viewed your resume.</font>
    <font><icon>1</icon> from Company</font>
    <font color=\"#ff6c00\"><size value=\"25\">1500/day</size></font> <del><font color=\"#808080\"><size value=\"18\">org:20000</size></font></del>

]]> </string>

中文的string:

  <string name="hr_view_resume"> <![CDATA[ <face><font color=\"#0689FB\">%1$s</font></face>
    <font color=\"#000000\">的人事专员</font>
    <font color=\"#000000\">查看了你的简历</font>
    <font>来自公司的<icon>1</icon></font>
    <font color=\"#ff6c00\"><size value=\"25\">1500/天</size></font> <del><font color=\"#808080\"><size value=\"18\">原价:20000元</size></font></del>

     ]]> </string>

Activity中的调用


      val content = String.format(getString(R.string.hr_view_resume), "Custom Company")

        //Html的文本展示
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
            mBinding.tvHtmlText.text =
                Html.fromHtml(content, Html.FROM_HTML_MODE_LEGACY, null,
                    CustomerLableHandler(
                        TypefaceUtil.getSFFlower(mContext),
                        R.mipmap.iv_me_red_packet
                    )
                )
        } else {
            mBinding.tvHtmlText.text = Html.fromHtml(content, null,
                CustomerLableHandler(
                    TypefaceUtil.getSFFlower(mContext),
                    R.mipmap.iv_me_red_packet
                )
            )
        }

复制代码

中英文实现的效果如下:

01e7c6285fe24605b8abbdff9f35b206~tplv-k3u1fbpfcp-zoom-in-crop-mark:1304:0:0:0.awebp
72a6b694b17f4fe39ec0ee3e724d3d9c~tplv-k3u1fbpfcp-zoom-in-crop-mark:1304:0:0:0.awebp

单标签的自定义和多标签的自定义讲到这来就完成了,是不是很方便呢?常用的一些Span已经给大家封装好了,有需要的也可以看一下源码,跑一下代码。

感觉大家看到这里,如果觉得不错还请点赞支持!

如有错漏与不同意见也请评论指出!

ftd2

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK