Android Html.fromHtml支持字体大小和加粗(可扩展)

先看效果图

Android Html.fromHtml支持字体大小和加粗(可扩展)_第1张图片

开发的时候,需要使用到富文本,如果用到了Html标签,系统不支持字体大小和加粗样式,那么就需要自己解析写.

使用例子

 String htmlStr2 =
                "Html" +
                        "字体变大,色值变化"
                        +
                        "字体变大,色值变化1" +
                        "";
        TextView htmlTv2 = findViewById(R.id.html_tv2);
        htmlTv2.setText(HtmlHelper.getHtmlSpanned(htmlStr2));


        String htmlStr3 =
                "我已经完成" +
                        "80%" +
                        "的暑假作业";
        HtmlTextView htmlTv3 = findViewById(R.id.html_tv3);
        htmlTv3.setHtmlColorSize(htmlStr3);

SDK源码:

font标签:

private void startFont(Editable text, Attributes attributes) {
        String color = attributes.getValue("", "color");
        String face = attributes.getValue("", "face");

        if (!TextUtils.isEmpty(color)) {
            int c = getHtmlColor(color);
            if (c != -1) {
                start(text, new Foreground(c | 0xFF000000));
            }
        }

        if (!TextUtils.isEmpty(face)) {
            start(text, new Font(face));
        }
    }

这里font标签,只支持color属性和face,不支持size属性.

span标签:

private void startCssStyle(Editable text, Attributes attributes) {
        String style = attributes.getValue("", "style");
        if (style != null) {
            Matcher m = getForegroundColorPattern().matcher(style);
            if (m.find()) {
                int c = getHtmlColor(m.group(1));
                if (c != -1) {
                    start(text, new Foreground(c | 0xFF000000));
                }
            }

            m = getBackgroundColorPattern().matcher(style);
            if (m.find()) {
                int c = getHtmlColor(m.group(1));
                if (c != -1) {
                    start(text, new Background(c | 0xFF000000));
                }
            }

            m = getTextDecorationPattern().matcher(style);
            if (m.find()) {
                String textDecoration = m.group(1);
                if (textDecoration.equalsIgnoreCase("line-through")) {
                    start(text, new Strikethrough());
                }
            }
        }
    }

    private static Pattern getForegroundColorPattern() {
        if (sForegroundColorPattern == null) {
            sForegroundColorPattern = Pattern.compile(
                    "(?:\\s+|\\A)color\\s*:\\s*(\\S*)\\b");
        }
        return sForegroundColorPattern;
    }
     private static Pattern getBackgroundColorPattern() {
        if (sBackgroundColorPattern == null) {
            sBackgroundColorPattern = Pattern.compile(
                    "(?:\\s+|\\A)background(?:-color)?\\s*:\\s*(\\S*)\\b");
        }
        return sBackgroundColorPattern;
    }
    private static Pattern getTextDecorationPattern() {
        if (sTextDecorationPattern == null) {
            sTextDecorationPattern = Pattern.compile(
                    "(?:\\s+|\\A)text-decoration\\s*:\\s*(\\S*)\\b");
        }
        return sTextDecorationPattern;
    }

系统的Span标签只支持:color,background,background-color,text-decoration属性,而不支持font-size,font-weight属性.

现在已经源码为什么不支持了,那么就需要自己来解析和编写.

解析流程图

Android Html.fromHtml支持字体大小和加粗(可扩展)_第2张图片

流程解析

1.获取到了html标签字符串

//标签
    public static final String NEW_FONT = "myfont";
    public static final String HTML_FONT = "font";

    public static final String NEW_SPAN = "myspan";
    public static final String HTML_SPAN = "span";

用到的类

public class HtmlLabelBean {
    public String tag;//当前Tag
    public int startIndex;//tag开始角标
    public int endIndex;//tag结束的角标
    public int size;//字体大小
    @ColorInt
    public int color;//字体颜色

    public String fontWeight;//字体样式,目前只是判断了是否加粗


    public List ranges;

    /**
     * 是否加粗
     */
    public boolean isBold() {
        return "bold".equalsIgnoreCase(fontWeight);
    }
}

2.将制定的标签更改为自定义的标签

if (source.contains("<" + HtmlCustomTagHandler.HTML_FONT)) {
            isTransform = true;
            //转化font标签
            source = source.replaceAll("<" + HtmlCustomTagHandler.HTML_FONT, "<" + HtmlCustomTagHandler.NEW_FONT);
            source = source.replaceAll("/" + HtmlCustomTagHandler.HTML_FONT + ">", "/" + HtmlCustomTagHandler.NEW_FONT + ">");
            Log.d(HtmlCustomTagHandler.TAG, "font->myfont");
        }
        if (source.contains("<" + HtmlCustomTagHandler.HTML_SPAN)) {
            isTransform = true;
            //转化span标签
            source = source.replaceAll("<" + HtmlCustomTagHandler.HTML_SPAN, "<" + HtmlCustomTagHandler.NEW_SPAN);
            source = source.replaceAll("/" + HtmlCustomTagHandler.HTML_SPAN + ">", "/" + HtmlCustomTagHandler.NEW_SPAN + ">");
            Log.d(HtmlCustomTagHandler.TAG, "span->myspan");
        }

这里在转化之前,最后判断一下元html是否包含font和span标签,如果包含则替换,避免不必要的转化.

3.顺序遍历字符串

顺序遍历字符串的时候,用到了SDK中的Html.TagHandler,需要创建一个类继承它,Html转化支持传递自定义的.系统的方法如下:

 public static Spanned fromHtml(String source, ImageGetter imageGetter, TagHandler tagHandler) {
        return fromHtml(source, FROM_HTML_MODE_LEGACY, imageGetter, tagHandler);
    }

3.1:查看开标签,然后获取其属性,将此标签顺序存储到开标签集合(OPEN_LIST)
private List labelBeanList;//顺序添加的Bean
public void startFont(String tag, Editable output, XMLReader xmlReader) {
        int startIndex = output.length();
        HtmlLabelBean bean = new HtmlLabelBean();
        bean.startIndex = startIndex;
        bean.tag = tag;

        String color = null;
        String size = null;
        //字体加粗的值CSS font-weight属性:,normal,bold,bolder,lighter,也可以指定的值(100-900,其中400是normal)
        //说这么多,这里只支持bold,如果是bold则加粗,否则就不加粗
        String fontWeight = null;
        if (NEW_FONT.equals(tag)) {
            color = attributes.get("color");
            size = attributes.get("size");

        } else if (NEW_SPAN.equals(tag)) {
            String style = attributes.get("style");
            if (!TextUtils.isEmpty(style)) {
                String[] styles = style.split(";");
                for (String str : styles) {
                    if (!TextUtils.isEmpty(str)) {
                        String[] value = str.split(":");
                        if (value[0].equals("color")) {
                            color = value[1];
                        } else if (value[0].equals("font-size")) {
                            size = value[1];
                        } else if (value[0].equals("font-weight")) {
                            fontWeight = value[1];
                        }
                    }
                }
            }
        }
        try {
            if (!TextUtils.isEmpty(color)) {
                int colorInt = Color.parseColor(color);
                bean.color = colorInt;
            } else {
                bean.color = -1;
            }
        } catch (Exception e) {
            bean.color = -1;
        }

        try {
            if (!TextUtils.isEmpty(size)) {
                //这里用[A-Za-z]+)?,是为了假如单位不是px,dp,sp的话,或者无单位的话,那么还可以取出数值,给出一个默认的单位
                Pattern compile = Pattern.compile("^(\\d+)([A-Za-z]+)?$");
                Matcher matcher = compile.matcher(size);
                if (matcher.matches()) {
                    String group1 = matcher.group(1);//12--数值
                    String group2 = matcher.group(2);//px/sp/dp/无--单位-默认是px
                    if ("sp".equalsIgnoreCase(group2)) {
                        bean.size = sp2px(Integer.parseInt(group1));
                    } else if ("dp".equalsIgnoreCase(group2)) {
                        bean.size = dp2px(Integer.parseInt(group1));
                    } else if ("px".equalsIgnoreCase(group2)) {
                        bean.size = Integer.parseInt(group1);
                    } else {
                        bean.size = Integer.parseInt(group1);
                    }
                } else {
                    bean.size = -1;
                }
            } else {
                bean.size = -1;
            }
        } catch (Exception e) {
            bean.size = -1;
        }
        //设置字体粗细
        bean.fontWeight = fontWeight;

        labelBeanList.add(bean);
        Log.d(TAG, "opening:开" + "tag:<" + tag + " startIndex:" + startIndex + " 当前遍历的开的集合长度:" + labelBeanList.size());
    }

注意:
再获取font-size和size属性,要判断后面的单位,因为AbsoluteSizeSpan传递的字体单位是px,所以需要把单位都要转化为px.

源代码:

/**
     * Set the text size to size physical pixels.
     */
    public AbsoluteSizeSpan(int size) {
        this(size, false);
    }

这里是做转化的逻辑部分代码

if ("sp".equalsIgnoreCase(group2)) {
                        bean.size = sp2px(Integer.parseInt(group1));
                    } else if ("dp".equalsIgnoreCase(group2)) {
                        bean.size = dp2px(Integer.parseInt(group1));
                    } else if ("px".equalsIgnoreCase(group2)) {
                        bean.size = Integer.parseInt(group1);
                    } else {
                        bean.size = Integer.parseInt(group1);
                    }


 /**
     * sp-->px
     *
     * @param sp
     * @return
     */
    private static int sp2px(int sp) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp,
                MyApp.app().getResources().getDisplayMetrics());
    }

    /**
     * dp-->px
     *
     * @param dp
     * @return
     */
    private static int dp2px(int dp) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,
                MyApp.app().getResources().getDisplayMetrics());
    }


3.2:查询到闭标签,则从OPEN_LIST中逆向查找与该标签匹配的开标签.
/**
     * 获取最后一个与当前tag匹配的Bean的位置
     * 从后往前找
     *
     * @param tag
     * @return
     */
    private int getLastLabelByTag(String tag) {
        for (int size = labelBeanList.size(), i = size - 1; i >= 0; i--) {
            if (!TextUtils.isEmpty(tag) &&
                    !TextUtils.isEmpty(labelBeanList.get(i).tag) &&
                    labelBeanList.get(i).tag.equals(tag)) {
                return i;
            }
        }

        return -1;
    }

3.3:计算该完整标签影响的范围(这里用到了DELETED_LIST集合)
/**
     * 计算影响的范围
     *
     * @param bean
     */
    private void optBeanRange(HtmlLabelBean bean) {

        if (bean.ranges == null) {
            bean.ranges = new ArrayList<>();
        }

        if (tempRemoveLabelList.size() == 0) {
            HtmlLabelRangeBean range = new HtmlLabelRangeBean();
            range.start = bean.startIndex;
            range.end = bean.endIndex;
            bean.ranges.add(range);
        } else {
            int size = tempRemoveLabelList.size();
            //逆向找到  第一个结束位置<=当前结束位置
            //逆向找到最后一个开始位置>=当前开始位置
            int endRangePosition = -1;
            int startRangePosition = -1;
            for (int i = size - 1; i >= 0; i--) {
                HtmlLabelBean bean1 = tempRemoveLabelList.get(i);
                if (bean1.endIndex <= bean.endIndex) {
                    //找第一个
                    if (endRangePosition == -1)
                        endRangePosition = i;
                }
                if (bean1.startIndex >= bean.startIndex) {
                    //找最后一个,符合条件的都覆盖之前的
                    startRangePosition = i;
                }
            }
            if (startRangePosition != -1 && endRangePosition != -1) {
                HtmlLabelBean lastBean = null;
                //有包含关系
                for (int i = startRangePosition; i <= endRangePosition; i++) {
                    HtmlLabelBean removeBean = tempRemoveLabelList.get(i);
                    lastBean = removeBean;
                    HtmlLabelRangeBean range;
                    if (i == startRangePosition) {
                        range = new HtmlLabelRangeBean();
                        range.start = bean.startIndex;
                        range.end = removeBean.startIndex;
                        bean.ranges.add(range);
                    } else {
                        range = new HtmlLabelRangeBean();
                        HtmlLabelBean bean1 = tempRemoveLabelList.get(i - 1);
                        range.start = bean1.endIndex;
                        range.end = removeBean.startIndex;
                        bean.ranges.add(range);
                    }
                }
                HtmlLabelRangeBean range = new HtmlLabelRangeBean();
                range.start = lastBean.endIndex;
                range.end = bean.endIndex;
                bean.ranges.add(range);
            } else {
                //表示将要并列添加,那么影响的范围就是自己的角标范围
                HtmlLabelRangeBean range = new HtmlLabelRangeBean();
                range.start = bean.startIndex;
                range.end = bean.endIndex;
                bean.ranges.add(range);
            }
        }
    }

计算出的范围存储在Bean类中的ranges属性中,这里也考虑了嵌套的标签查找逻辑,所以传递进来的标签也可以是嵌套类型的.

3.4:设置拼接影响范围的字体和颜色.
for (HtmlLabelRangeBean range : bean.ranges) {
                //设置字体颜色
                if (bean.color != -1)
                    output.setSpan(new ForegroundColorSpan(bean.color), range.start, range.end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
                //设置字体大小
                // 这里AbsoluteSizeSpan默所以是px
                if (bean.size != -1) {
                    output.setSpan(new AbsoluteSizeSpan(bean.size), range.start, range.end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
                }
                //设置是否加粗
                if (bean.isBold()) {
                    output.setSpan(new StyleSpan(Typeface.BOLD), range.start, range.end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
                }
            }
3.5:从开始标签中删除该闭标签对应的开始标签

在集合中labelBeanList删除此标签Bean

3.6:把删除的完整标签存储到已删除完成标签集合中(DELETED_LIST)
 /**
     * 操作删除的Bean,将其添加到删除的队列中
     *
     * @param removeBean
     */
    private void optRemoveByAddBean(HtmlLabelBean removeBean) {
        int isAdd = 0;
        for (int size = tempRemoveLabelList.size(), i = size - 1; i >= 0; i--) {
            HtmlLabelBean bean = tempRemoveLabelList.get(i);
            if (removeBean.startIndex <= bean.startIndex && removeBean.endIndex >= bean.endIndex) {
                if (isAdd == 0) {
                    tempRemoveLabelList.set(i, removeBean);
                    isAdd = 1;
                } else {
                    //表示已经把isAdd = 1;当前删除的bean,添加到了删除队列中,如果再次找到了可以removeBean可以替代的bean,则删除
                    tempRemoveLabelList.remove(i);
                }

            }
        }
        if (isAdd == 0) {
            tempRemoveLabelList.add(removeBean);
        }

        Log.d(TAG, "已经删除的完整开关结点的集合长度:" + tempRemoveLabelList.size());
    }

这里需要注意:
如果删除的标签,范围包含了已经删除的标签,那么则替换,并删除其他的覆盖的.

  

 
      



假如此时已经遍历到了
已经删除的集合中包含了 
此时要把添加到删除的集合.
因为span2的范围包含了font2.
将span2添加到删除集合中,添加后的集合数据是: .
如果不这样处理的话,那么计算影响范围的会比较复杂.

完整的结束标签的处理代码


    public void endFont(String tag, Editable output, XMLReader xmlReader) {
        int stopIndex = output.length();
        Log.d(TAG, "opening:关" + "tag:" + tag + "/> endIndex:" + stopIndex);
        int lastLabelByTag = getLastLabelByTag(tag);
        if (lastLabelByTag != -1) {
            HtmlLabelBean bean = labelBeanList.get(lastLabelByTag);
            bean.endIndex = stopIndex;
            optBeanRange(bean);
            Log.d(TAG, "完整的TagBean解析完成:" + bean.toString());

            for (HtmlLabelRangeBean range : bean.ranges) {
                //设置字体颜色
                if (bean.color != -1)
                    output.setSpan(new ForegroundColorSpan(bean.color), range.start, range.end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
                //设置字体大小
                // 这里AbsoluteSizeSpan默所以是px
                if (bean.size != -1) {
                    output.setSpan(new AbsoluteSizeSpan(bean.size), range.start, range.end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
                }
                //设置是否加粗
                if (bean.isBold()) {
                    output.setSpan(new StyleSpan(Typeface.BOLD), range.start, range.end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
                }
            }
            //从顺序添加的集合中删除已经遍历完结束标签
            labelBeanList.remove(lastLabelByTag);
            optRemoveByAddBean(bean);
        }
    }

如何扩展

扩展的内容是要Spanned可以设置的样式
1.在HtmlLabelBean中增加对应的属性
2.在startFont方法中解析属性就可以,设置给Bean
3.在endFont方法中设置对应的样式.
Android Html.fromHtml支持字体大小和加粗(可扩展)_第3张图片

完善

在转化自定义htmlSpanned的时候,
1.判断是否为空,如果为空,支持给默认值
2.转化之前,判断是否需要转化,再转化,以防做不必要的转化
3.如果进行了转化,则需要在最外层包一层"" + source + ""
否则这样再遍历的时候计算的角标位置会错乱,因为计算角标的时候,是按照整个 字符串进行计算的,也可以不包,也可以是其他的标签,只是因为标签不会有什么影响原来的样式.
4.如果没做转化,如果包含了html标签,那么使用系统自带支持的html就可以的就可以.
5.如果没做转化,如果不包含了标签,那么不需要使用html.
6.创建了一个HtmlTextView,这样布局中,或者代码创建,也可以直接使用,或者直接使用HtmlHelper类中的getHtmlSpanned.

如果需要也可以下载源码.源码下载在顶部.

你可能感兴趣的:(Android工具)