最近接到个需求,在一段英文中,将给定的单词高亮。写了近1天,考虑了各种情况,终于写完了对应算法。我将其封装成一个:MySpanTextView
源码在下面,直接复制,拿到项目中用就行。继承自普通TextView,仅仅对文字匹配做了修改,其他属性不变。复制代码,可以直接用
需求:给定一段英文,和某些关键字,在英文中,将关键字高亮。
要求:将给定的关键字视为独立的单词,当其出现在其他单词内时,不可以高亮。
如:
关键字中有单独字母 a ,则 and、teacher等单词中的 a 不可以高亮
英文中有一个单词是 haha ,关键字中有一个是 ha,则 haha 中,ha 不可以高亮
效果图如下:
相关的注释,全部在代码中了
/**
* 展示高亮关键字的TextView
*/
class MySpanTextView : TextView {
constructor(context: Context?) : this(context, null)
constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
/**
* 设置文字前景色
*
* content 要展示的文字
* highlight 要处理为高亮的所有关键字
*/
fun setTextForegColor(content: String?, highlight: String?, tip: String?) {
try {
//如果文本为空,就 setText=""
if (content.isNullOrEmpty()) {
text = ""
return
}
//如果文本为空,就 setText=content
if (highlight.isNullOrEmpty()) {
text = content
return
}
/**
* 整体思路
*
* 1、将全部的高亮关键字(keys),进行切分,拿到单个的关键字(key)
* 2、用每个key,去原始文本中进行遍历,找到出现的位置(每次出现的位置)
* 3、对该位置的前后进行判断,是否是有效的单词分隔符,如:空格、逗号、句号等
* 4、如果符合条件,就是最终,要高亮的位置(有效位),这个位置上的单词,就是要高亮的单词
*
* 如:
* 原始文本:abc is a Teacher.
* 高亮单词:a
* 结果:abc、Teacher 中的"a",不能高亮,只有单独的那个"a",才能高亮,单独的"a"出现的位置,才是真正的有效位
* 理由:abc中,虽然可以找到a,但是a后面,是"b",不是单词分隔符,所有不能算有效位
*
* 关于位置的处理思路:
* 目前,找到后,先存起来(封装成一个bean(index:出现位置,key:key的内容),最后整体遍历一遍;
* 也可以找到有效位,就高亮变色
*
*/
//所有要高亮的单词
var keys = highlight.trim().split(";", ";")
if (keys.isNullOrEmpty().not()) {
//高亮关键字集合不为空
//创建一个 spannableString
var spannableString: SpannableString = SpannableString(content)
//最后要集中处理的高亮位置的单词
var keyBeanList: MutableList<HighKeyBean> = mutableListOf()
//视为分隔符的 字符 的集合
var separatedChars: MutableList<Char> = mutableListOf(
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
'u', 'v', 'w', 'x', 'y', 'z'
)
//遍历keys中,每个高亮关键字,并对其进行处理
keys.forEach {
var index = 0
//截取原始文本的开始位置
var subStartIndex = 0
var subContent = content!!
while (subContent.contains(it)) {
//找到当前的这个关键字,在剩余文本中第一次出现的位置(不一定是有效位)
var indexOf = subContent.indexOf(it)
//设置、记录下一次,要截取的位置的开始
subStartIndex = indexOf + it.length
index = index + indexOf
//剩余文本中,找到了这个单词,判断其有效位
if (indexOf == 0) {
//单词出现的位置,是剩余文本的起点处
/**
* 特别说明
*
* 这里要检查一下标记的前一位,是否是有效位
*
* 因为会出现特殊情况
*
* 如:abc haha def
* key = ha
*
* 后续处理时
* 第一次:
* abc haha def
*
* indexOf = 4
* subStartIndex = indexOf + it.length = 4 + ha.length = 4+2 = 6
* index = 0 + 4 = 4
*
* 会走到 indexOf != 0 情况中的一种,判定结果为:haha中,前半部分的"ha",不能高亮
*
* 第二次:从 haha 的第二个 h 处开始截取
* 子串为:ha def
* key = ha
* 这个时候,这个 ha 还是不能高亮,因为它前面还紧跟字母,只是因为截取看不到
*
* 所以要对它的前一位做判断,同时,为了避免繁琐的判断,直接用 try...catch...
*/
var checkIndex = true
try {
if (separatedChars.contains(content[index - 1].toLowerCase())) {
checkIndex = false
}
} catch (e: Exception) {
}
if (checkIndex) {
if (it.length == subContent.length) {
/**
* 当前剩余文本,都是关键字
* 如:剩余文本:abc
* key=abc
*/
//是有效位
keyBeanList.add(
HighKeyBean(
index,
it
)
)
} else if (!separatedChars.contains(subContent[it.length].toLowerCase())) {
/**
* 单词的下一个位置的字符,是分隔类型字符
*
* 如:剩余文本:ab cd
* key = ab
* 则:
* indexOf = 0
* keyLength = 2
* indexOf + it.length = 2
*
* subContent[it.length] = subContent[2] = ' '(空格)
*
* separatedChars中包含空格
*/
//是有效位
keyBeanList.add(
HighKeyBean(
index,
it
)
)
}
}
} else {
//出现位置,不再文本开始处
if (indexOf + it.length == subContent.length && !separatedChars.contains(subContent[indexOf - 1].toLowerCase())) {
/**
* 如:剩余文本为:ab cde
* key = cde
*
* indexOf = 3
* keyLength = 3
* indexOf + it.length =6
* subContent.length = 6
*
* subContent[indexOf -1] = subContent[2] = ' '(空格)
*/
//是有效位
keyBeanList.add(
HighKeyBean(
index,
it
)
)
} else {
if (!separatedChars.contains(subContent[indexOf - 1].toLowerCase())
&& !separatedChars.contains(subContent[indexOf + it.length].toLowerCase()
)
) {
keyBeanList.add(
HighKeyBean(
index,
it
)
)
}
}
}
//下一次要处理的子串
subContent = subContent.substring(subStartIndex)
//避免错位,要进行对应的移位
index += it.length
}
}
Log.e("keyBeanList = ", "$keyBeanList")
//集中处理需要高亮的单词
if (keyBeanList.isNotEmpty()) {
keyBeanList.forEach {
spannableString.setSpan(
ForegroundColorSpan(Color.parseColor("#ffd234")),
it.index,
it.index + it.key.length,
Spanned.SPAN_INCLUSIVE_EXCLUSIVE
)
}
if (tip.isNullOrEmpty().not()) {
var t= tip!!.trim()
val tipSpan = ForegroundColorSpan(Color.parseColor("#ff999999"))
spannableString.setSpan(
tipSpan,
content.indexOf(t),
content.indexOf(t) + t.length,
Spanned.SPAN_INCLUSIVE_EXCLUSIVE
)
}
text = spannableString
} else {
text = content
}
} else {
//高亮关键字集合为空。直接展示原始文本
text = content
}
} catch (e: Exception) {
if (content.isNullOrEmpty()) {
text = ""
}else{
text = content
}
}
}
}
其中 HighKeyBean 为
data class HighKeyBean(
var index: Int = 0,
var key: String = ""
) {
}
使用:
布局文件中:
<com.demo.newkotlindemo.MySpanTextView
android:id="@+id/my_tv"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
//文本内容
var content: String? =
"If you procrastinate when faced with a big e e haha difficult if the problem... break and the problem if into ha parts, and handle one part at a time time."
//需要高亮的单词
var highlight: String? = "If;you;a;the;and;time;if;e;ha"
my_tv.setTextForegColor(content,highlight)