#前言
我们在Android的日常开发中经常会用到TextView,而在TextView的使用中有可能需要像下面这样使用。
Android 自定义Html标签_第1张图片
上面只用了一个TextView就可以实现,有人可能会想到使用Html.fromHtml("...")实现,但是Android原生的font标签是不支持size属性的。我们来看下源码,看下font标签到底支持哪些属性:

    //摘抄自Android API源码(Html类)    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));        }    }

可以看到原生只支持colorface两个属性。如果你需要它支持下划线加粗、以及字体大小等属性是不可能。如果你需要font标签支持这些属性就只能通过自定标签的方式实现了。我们可以看到startFont方法是被private void handleStartTag(String tag, Attributes attributes)方法调用的,如下:

    private void handleStartTag(String tag, Attributes attributes) {        //省略N行代码         if (tag.equalsIgnoreCase("font")) {            startFont(mSpannableStringBuilder, attributes);        } else if (mTagHandler != null) {            mTagHandler.handleTag(true, tag, mSpannableStringBuilder, mReader);        }    }

可以看到,如果我们的文本中有什么标签没有被Html类处理的话最终会调用mTagHandler.handleTag(true, tag, mSpannableStringBuilder, mReader);这段代码。而mTagHandler是可以在调用fromHtml方法是传入的,源码如下:

/**     * Returns displayable styled text from the provided HTML string with the legacy flags     * {@link #FROM_HTML_MODE_LEGACY}.     *     * @deprecated use {@link #fromHtml(String, int, ImageGetter, TagHandler)} instead.     */    @Deprecated    public static Spanned fromHtml(String source, ImageGetter imageGetter, TagHandler tagHandler) {        return fromHtml(source, FROM_HTML_MODE_LEGACY, imageGetter, tagHandler);    }    /**     * Returns displayable styled text from the provided HTML string. Any <img> tags in the     * HTML will use the specified ImageGetter to request a representation of the image (use null     * if you don't want this) and the specified TagHandler to handle unknown tags (specify null if     * you don't want this).     *     * 

This uses TagSoup to handle real HTML, including all of the brokenness found in the wild. */ public static Spanned fromHtml(String source, int flags, ImageGetter imageGetter, TagHandler tagHandler) { Parser parser = new Parser(); try { parser.setProperty(Parser.schemaProperty, HtmlParser.schema); } catch (org.xml.sax.SAXNotRecognizedException e) { // Should not happen. throw new RuntimeException(e); } catch (org.xml.sax.SAXNotSupportedException e) { // Should not happen. throw new RuntimeException(e); } HtmlToSpannedConverter converter = new HtmlToSpannedConverter(source, imageGetter, tagHandler, parser, flags); return converter.convert(); }

上面的方法在API24之后就淘汰了,所以API24之后要用下面的,API24之前要用上面的。每个参数的意义就不详细介绍了(不是本文重点),主要来看下TagHandler这个接口。他只有一个抽象法方法handleTag,如下:

public static interface TagHandler {    public void handleTag(boolean opening, String tag,Editable output, XMLReader xmlReader);}

#####参数说明:
opening:从Html的源码中可以看出,处理标签开始时该参数为true,处理结束时该参数为false。例如Holle当读取到时为开始,当读取到时为结束。

tag:标签名字,例如Hollefont就是tag参数的值。

output:已经被处理到的你的文本源。

xmlReader:封装了所有tag标签的参数,如Holle中color的值就从这个参数中读取。但不幸的是我们并不能直接从xmlReader读取我们的自定义参数,这里需要用到反射。核心代码如下:
####Kotlin

/** * 利用反射获取html标签的属性值。使用该方法获取自定义属性等号后面的值。 *  * @param xmlReader XMLReader对象。 * @param property 你的自定义属性,例如color。 */@Suppress("UNCHECKED_CAST")private fun getProperty(xmlReader: XMLReader, property: String): String? {    try {        val elementField = xmlReader.javaClass.getDeclaredField("theNewElement")        elementField.isAccessible = true        val element: Any = elementField.get(xmlReader)        val attsField = element.javaClass.getDeclaredField("theAtts")        attsField.isAccessible = true        val atts: Any = attsField.get(element)        val dataField = atts.javaClass.getDeclaredField("data")        dataField.isAccessible = true        val data = dataField.get(atts) as Array<String>        val lengthField = atts.javaClass.getDeclaredField("length")        lengthField.isAccessible = true        val len = lengthField.getInt(atts)        for (i in 0 until len) {            // 判断属性名            if (property == data[i * 5 + 1]) {                return data[i * 5 + 4]            }        }    } catch (e: Exception) {        e.printStackTrace()    }    return null}

好,既然参数都明白了那么就开始来实现吧。首先我们来定义一个CustomTagHandler并重写他的抽象方法handleTag,然后根据opening参数判断当前是处理tag标签的开始还是结束。代码如下:

override fun handleTag(opening: Boolean, tag: String, output: Editable, xmlReader: XMLReader) {    if (opening) {        handlerStartTAG(tag, output, xmlReader)    } else {        handlerEndTAG(tag, output)    }}

在上面的代码中我们根据opening参数判断当前是处理tag标签的开始还是结束,如果是开始就调用handlerStartTAG否者则调用handlerEndTAG方法。
下面在来看下这个两个方法的实现:

private fun handlerStartTAG(tag: String, output: Editable, xmlReader: XMLReader) {    if (tag.equals("kFont", ignoreCase = true)) {        handlerKFontStart(output, xmlReader)    }}private fun handlerEndTAG(tag: String, output: Editable) {    if (tag.equals("kFont", ignoreCase = true)) {        handlerKFontEnd(output)    }}

这两个方法的主要作用是区分我们的自定义标签,只要我们能识别的标签我们才去处理。这里在检测到是我们自定义的标签后分别调用了handlerKFontStarthandlerKFontEnd方法(其实这里你可以将你的自定义标签封装成类,然后所有的处理都在你自己的类中处理,这样的话方便以后的扩展。我们现在是demo,demo还是简单点儿好。),handlerKFontStart主要是记录我们标签开始的位置,以及获取我们所有的参数的值,例如我们的标签只有一个size属性和一个clickable属性。代码如下:

private fun handlerKFontStart(output: Editable, xmlReader: XMLReader) {    val index = output.length    val tagInfo = TagInfo(index)        val clickable = getProperty(xmlReader, "clickable")    if (!clickable.isNullOrEmpty()) {        tagInfo.clickable = clickable    }    val size = getProperty(xmlReader, "size")    if (!size.isNullOrEmpty()) {        tagInfo.size = when {            size.endsWith("sp", true) -> Integer.parseInt(size.replace("sp", "", true))            size.endsWith("px", true) -> {                tagInfo.hasUnderline = false                Integer.parseInt(size.replace("px", "", true))            }            else -> try {                Integer.parseInt(size)            } catch (e: Exception) {                20            }        }    }    currentTagInfo = tagInfo}

这里我们主要是定义一个实体类TagInfo用来记录我们自定义tag的各个属性以及所在位置。获取参数值的getProperty方法刚刚都已经贴出来了,直接使用即可。接下来就是处理标签结束的时候(handlerKFontEnd方法),具体代码如下:

private fun handlerKFontEnd(output: Editable) {    val tagInfo = currentTagInfo    if (tagInfo != null) {        val size = tagInfo.size        val clickable = tagInfo.clickable        val end = output.length        if (!clickable.isNullOrEmpty()) {            output.setSpan(KFontClickableSpan(clickable), tagInfo.startIndex, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)        }        if (size > 0) {            output.setSpan(                AbsoluteSizeSpan(size, tagInfo.sizeDip),                tagInfo.startIndex,                end,                Spanned.SPAN_EXCLUSIVE_EXCLUSIVE            )        }    }}

这个方法中主要就是获取我们记录下来的TagInfo然后根据这个属性设置自己的Span,这里只列举了两个简单的Span,有关更详细的SpannableString的用法请参考这篇博客。
一切完成之后在调用Html.fromHtml()方法的时候传入我们的CustomTagHandler就可以了,代码如下:

val source = "是一个小小小鸟"tvText.text = Html.fromHtml(source, null, CustomTagHandler())

好了,到这里基本就算结束了,下面是我封装的一个工具类,上面的代码都是摘抄自这个工具类,你可以直接把这个类拿去用(请叫我雷锋,哈哈)。

class KFontHandler private constructor(private var onTextClickListener: ((flag: String) -> Unit)? = null) :    Html.TagHandler {    companion object {        private const val TAG_K_FONT = "kFont"        /**         * 格式化html文本。         * @param source 要格式化的html文本,除了支持Google原生支持的标签外,还支持kFont标签。         * @param textClickListener 如果你给kFont标签添加了clickable属性则可以通过该参数设置点击监听,监听中的flag参数为clickable等号后面的内容,例如         * 你kFont标签中clickable属性的为`clickable=A`,那么flag的值就为A。如果你没有使用clickable属性则该参数可以不传。         *         * @return 返回格式化后的文本(CharSequence类型)。         */        fun format(source: String, textClickListener: ((flag: String) -> Unit)? = null): CharSequence {            return htmlFrom(source.replace("\n", "
"
, true), textClickListener) } /** * 格式化html文本。 * @param context 上下文参数,用来读取资源文件中的文本。 * @param resId 要格式化的html文本文件的ID,例如R.raw.html_text。除了支持Google原生支持的标签外,还支持kFont标签。 * @param textClickListener 如果你给kFont标签添加了clickable属性则可以通过该参数设置点击监听,监听中的flag参数为clickable等号后面的内容,例如 * 你kFont标签中clickable属性的为`clickable=A`,那么flag的值就为A。如果你没有使用clickable属性则该参数可以不传。 * * @return 返回格式化后的文本(CharSequence类型)。 */ fun loadResource( context: Context, @RawRes resId: Int, textClickListener: ((flag: String) -> Unit)? = null ): CharSequence { return htmlFrom(getStringFromStream(context.resources.openRawResource(resId)), textClickListener) } private fun htmlFrom(source: String, textClickListener: ((flag: String) -> Unit)? = null): CharSequence { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { Html.fromHtml( if (source.startsWith("")) source else "$source", Html.FROM_HTML_MODE_LEGACY, null, KFontHandler(textClickListener) ) } else { Html.fromHtml( if (source.startsWith("")) source else "$source", null, KFontHandler(textClickListener) ) } } private fun getStringFromStream(inputStream: InputStream): String { val inputStreamReader = InputStreamReader(inputStream, "UTF-8") val reader = BufferedReader(inputStreamReader) val sb = StringBuffer("") var line = reader.readLine() while (line != null) { sb.append(line) sb.append("
"
) line = reader.readLine() } return sb.toString() } } private var currentTagInfo: TagInfo? = null override fun handleTag(opening: Boolean, tag: String, output: Editable, xmlReader: XMLReader) { if (opening) { handlerStartTAG(tag, output, xmlReader) } else { handlerEndTAG(tag, output) } } private fun handlerStartTAG(tag: String, output: Editable, xmlReader: XMLReader) { if (tag.equals(TAG_K_FONT, ignoreCase = true)) { handlerKFontStart(output, xmlReader) } } private fun handlerEndTAG(tag: String, output: Editable) { if (tag.equals(TAG_K_FONT, ignoreCase = true)) { handlerKFontEnd(output) } } private fun handlerKFontStart(output: Editable, xmlReader: XMLReader) { val index = output.length val tagInfo = TagInfo(index) val style = getProperty(xmlReader, "style") if (!style.isNullOrEmpty()) { tagInfo.style = when (style) { "b", "bold" -> Typeface.BOLD "i", "italic" -> Typeface.ITALIC "b_i", "i_b", "bold_italic", "italic_bold" -> Typeface.BOLD_ITALIC "u", "underline" -> { tagInfo.hasUnderline = true Typeface.NORMAL } "i_u", "u_i", "italic_underline", "underline_italic" -> { tagInfo.hasUnderline = true Typeface.ITALIC } "b_u", "u_b", "bold_underline", "underline_bold" -> { tagInfo.hasUnderline = true Typeface.BOLD } "b_u_i", "b_i_u", "u_b_i", "u_i_b", "i_u_b", "i_b_u", "italic_bold_underline", "italic_underline_bold", "underline_italic_bold", "underline_bold_italic", "bold_underline_italic", "bold_italic_underline" -> { tagInfo.hasUnderline = true Typeface.BOLD_ITALIC } else -> Typeface.NORMAL } } val clickable = getProperty(xmlReader, "clickable") if (!clickable.isNullOrEmpty()) { tagInfo.clickable = clickable } val size = getProperty(xmlReader, "size") if (!size.isNullOrEmpty()) { tagInfo.size = when { size.endsWith("sp", true) -> Integer.parseInt(size.replace("sp", "", true)) size.endsWith("px", true) -> { tagInfo.hasUnderline = false Integer.parseInt(size.replace("px", "", true)) } else -> try { Integer.parseInt(size) } catch (e: Exception) { 20 } } } val color = getProperty(xmlReader, "color") if (!color.isNullOrEmpty()) { tagInfo.color = color } currentTagInfo = tagInfo } private fun handlerKFontEnd(output: Editable) { val tagInfo = currentTagInfo if (tagInfo != null) { val color = tagInfo.color val size = tagInfo.size val style = tagInfo.style val clickable = tagInfo.clickable val end = output.length if (!clickable.isNullOrEmpty()) { output.setSpan(KFontClickableSpan(clickable), tagInfo.startIndex, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) } if (!color.isNullOrEmpty()) { output.setSpan( ForegroundColorSpan(Color.parseColor(color)), tagInfo.startIndex, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE ) } if (size > 0) { output.setSpan( AbsoluteSizeSpan(size, tagInfo.sizeDip), tagInfo.startIndex, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE ) } if (style != Typeface.NORMAL) { output.setSpan(StyleSpan(style), tagInfo.startIndex, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) } if (tagInfo.hasUnderline) { output.setSpan(UnderlineSpan(), tagInfo.startIndex, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) } } } /** * 利用反射获取html标签的属性值 */ @Suppress("UNCHECKED_CAST") private fun getProperty(xmlReader: XMLReader, property: String): String? { try { val elementField = xmlReader.javaClass.getDeclaredField("theNewElement") elementField.isAccessible = true val element: Any = elementField.get(xmlReader) val attsField = element.javaClass.getDeclaredField("theAtts") attsField.isAccessible = true val atts: Any = attsField.get(element) val dataField = atts.javaClass.getDeclaredField("data") dataField.isAccessible = true val data = dataField.get(atts) as Array<String> val lengthField = atts.javaClass.getDeclaredField("length") lengthField.isAccessible = true val len = lengthField.getInt(atts) for (i in 0 until len) { // 判断属性名 if (property == data[i * 5 + 1]) { return data[i * 5 + 4] } } } catch (e: Exception) { e.printStackTrace() } return null } private inner class TagInfo internal constructor(val startIndex: Int) { internal var style: Int = Typeface.NORMAL internal var hasUnderline: Boolean = false internal var clickable: String? = null internal var color: String? = null internal var size: Int = 0 set(value) { if (value > 0) { field = value } } internal var sizeDip: Boolean = true } private inner class KFontClickableSpan(private val flag: String) : ClickableSpan() { override fun onClick(widget: View) { onTextClickListener?.invoke(flag) } override fun updateDrawState(ds: TextPaint) { } }}

如果你觉的本文对你有用还请不要吝啬你的赞哦,你的鼓励将会转换成我继续创作下去的勇气。再次感谢。


最后还是奉上Demo地址吧

更多相关文章

  1. Android控件TextView中ellipsize属性(设置当文字长度超过textview
  2. Android-UI——TextView各属性详解
  3. Android ListView,GridView,ScrollView,ProgressBar,SeekBar,Rel
  4. Android开发学习:ImageView的scaletype属性
  5. lua学习笔记 3 android调用Lua。Lua脚本使用LoadLib回调Java,并

随机推荐

  1. The Saygus VPhone V1 clears FCC, Will
  2. Android修改屏幕亮度
  3. Android(安卓)Textview 超出最多字数省略
  4. 原文:Android(安卓)Theme XML
  5. Android预定义样式
  6. android数据库操作(二)
  7. android 调节屏幕亮度
  8. 如何为ListView设置分割线
  9. Android实现体重测量仪的源码
  10. writing dumpstate to file android