#前言
我們在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));
}
}
可以看到原生只支持color
和face
兩個屬性。如果你需要它支持下劃線
、加粗
、以及字體大小
等屬性是不可能。如果你需要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).
*
* <p>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。例如<font>Holle</font>
當讀取到<font>
時爲開始,當讀取到</font>
時爲結束。
tag:標籤名字,例如<font>Holle</font>
中font
就是tag參數的值。
output:已經被處理到的你的文本源。
xmlReader:封裝了所有tag標籤的參數,如<font color=#000000>Holle</font>
中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)
}
}
這兩個方法的主要作用是區分我們的自定義標籤,只要我們能識別的標籤我們纔去處理。這裏在檢測到是我們自定義的標籤後分別調用了handlerKFontStart
和handlerKFontEnd
方法(其實這裏你可以將你的自定義標籤封裝成類,然後所有的處理都在你自己的類中處理,這樣的話方便以後的擴展。我們現在是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 = "<kFont color=#F0F000 size=34>我</kFont>是一個<kFont color=#00FF00 size=14>小小小</kFont>鳥"
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", "<br/>", 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("<html>")) source else "<html>$source</html>",
Html.FROM_HTML_MODE_LEGACY,
null,
KFontHandler(textClickListener)
)
} else {
Html.fromHtml(
if (source.startsWith("<html>")) source else "<html>$source</html>",
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("<br/>")
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地址吧