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

先看效果圖

在這裏插入圖片描述

開發的時候,需要使用到富文本,如果用到了Html標籤,系統不支持字體大小和加粗樣式,那麼就需要自己解析寫.

使用例子

 String htmlStr2 =
                "<span style='color:#EE30A7;font-size:20px'>Html" +
                        "<font color='#EE2C2C' size='40px'>字體變大,色值變化</font>"
                        +
                        "<font color='#CD8500' size='60px'>字體變大,色值變化1</font>" +
                        "</span>";
        TextView htmlTv2 = findViewById(R.id.html_tv2);
        htmlTv2.setText(HtmlHelper.getHtmlSpanned(htmlStr2));


        String htmlStr3 =
                "<font color='#4F94CD' size='40px'>我已經完成</font>" +
                        "<font color='#FF0000' size='80px'>80%</font>" +
                        "<font color='#4F94CD' size='40px'>的暑假作業</font>";
        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屬性.

現在已經源碼爲什麼不支持了,那麼就需要自己來解析和編寫.

解析流程圖

在這裏插入圖片描述

流程解析

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<HtmlLabelRangeBean> 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<HtmlLabelBean> 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 <code>size</code> 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());
    }

這裏需要注意:
如果刪除的標籤,範圍包含了已經刪除的標籤,那麼則替換,並刪除其他的覆蓋的.

<span1>  
<font1></font1>
<span2> 
      <font2></font2>
</span2>
</span>

假如此時已經遍歷到了</span2>
已經刪除的集合中包含了<font1><font1> <fon2></font2>
此時要把<span2></span2>添加到刪除的集合.
因爲span2的範圍包含了font2.
將span2添加到刪除集合中,添加後的集合數據是:<font1><font1> <span2></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方法中設置對應的樣式.
在這裏插入圖片描述

完善

在轉化自定義htmlSpanned的時候,
1.判斷是否爲空,如果爲空,支持給默認值
2.轉化之前,判斷是否需要轉化,再轉化,以防做不必要的轉化
3.如果進行了轉化,則需要在最外層包一層"<span>" + source + "</span>"
否則這樣再遍歷的時候計算的角標位置會錯亂,因爲計算角標的時候,是按照整個 字符串進行計算的,也可以不包<span></span>,也可以是其他的標籤,只是因爲<span>標籤不會有什麼影響原來的樣式.
4.如果沒做轉化,如果包含了html標籤,那麼使用系統自帶支持的html就可以的就可以.
5.如果沒做轉化,如果不包含了標籤,那麼不需要使用html.
6.創建了一個HtmlTextView,這樣佈局中,或者代碼創建,也可以直接使用,或者直接使用HtmlHelper類中的getHtmlSpanned.

如果需要也可以下載源碼.源碼下載在頂部.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章