#前言
我们在Android的日常开发中经常会用到TextView,而在TextView的使用中有可能需要像下面这样使用。

上面只用了一个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-UI——TextView各属性详解
  2. Android布局文件属性笔记
  3. Android三种动画详解
  4. Android布局——Linearlayout线性布局
  5. Android布局文件的属性值解析
  6. Android控件TextView中ellipsize属性(设置当文字长度超过textview
  7. Android中的windowSoftInputMode属性详解
  8. mybatisplus的坑 insert标签insert into select无参数问题的解决
  9. Python技巧匿名函数、回调函数和高阶函数

随机推荐

  1. android中随手指拖动滑屏
  2. 有关Android国际化的一点积累
  3. Android(安卓)内存溢出解决方案(OOM) 整理
  4. android点滴(29) android中设置用户自定
  5. 谷安: Google 44 号楼“变脸”!
  6. Android(安卓)Studio中导入现有Eclipse项
  7. Android(安卓)线程间通信机制(ITC详解)
  8. Android(安卓)1.5和Android(安卓)2.1在相
  9. Android高级应用2----ContentProvider(访
  10. Android之 RecyclerView,CardView 详解和