在做Wandroid
项目时有一个搜索功能,要在搜索结果中将匹配到的关键词高亮显示。但是 玩安卓API并没有提供颜色的高亮,只有字体斜体,效果看起来并不明显,并且昵称也参与了搜索,但并没有增加HTML
标签返回,这就有点美中不足了。因此我们自己动手来做一个。
API返回结果
{
...
"title": "微信在Github开源了Hard<em class='highlight'>coder</em>,对Android开发者有什么影响?",
...
}
预期效果
实现步骤
因为是支持多关键词搜索(空格分割),所以需要将关键词根据空格分割成一个至多个关键词,只要实现了单关键词的高亮,那么多关键词的高亮自然就不是问题了。
先来看看单关键词的实现步骤:
- 通过
indexOf
定位搜索关键词的索引,存在则返回关键词首个字符在整个字符串中的位置,不存在则返回-1
- 找出索引后截取出包含的
关键词
以及关键词之前的词+关键词拼接的字符串
- 将
关键词
以及关键词之前的词+关键词拼接的字符串
保存在MutableList<Pair<String, String>>
中 - 遍历集合,将匹配的词使用带有
HTML
标签的词替换 -
HtmlCompat.fromHtml
让TextView
可识别HTML
标签
看上面的实现步骤可能还是会有点懵,没关系,下面来一个一个的梳理清楚。先来看看代码
尽管是单关键词,搜索结果中还是有可能会有包含多个关键词的,比如Wandroid项目采用Kotlin语言编写,kotlin语言真好用
(此处的两个kotlin
的首字母大小写不一致,是为了验证后面要忽略大小写)就包含2个Kotlin语言
。
拿上面的字符串Wandroid项目采用Kotlin语言编写,kotlin语言真好用
来举例,搜索单关键词Kotlin语言
,使其高亮显示。
// CharSequenceExt.kt
const val emStart = "<em class='highlight'>" // 斜体
const val emEnd = "</em>"
const val fontStart = "<font color=\"red\">" // 字体红色
const val fontEnd = "</font>"
fun CharSequence?.appendHtmlTags(key: String): CharSequence {
val str = "Wandroid项目采用Kotlin语言编写,kotlin语言真好用"
val key = "Kotlin语言" // 需要将后面的"kotlin语言真好用"中的kotlin语言也匹配出来
// Pair<str的子字符串, str中包含的关键词,字符串一致,大小写不一定一致>
val textArr: MutableList<Pair<String, String>> = mutableListOf()
var searchIndex = 0 // 标记已经匹配过的位置,while循环中的indexOf需要从searchIndex开始,表示不重复匹配已经匹配过的字符串
// 因为有2处可匹配到关键词,所以这里用while循环遍历
while(searchIndex < str.length) { // 匹配到最后一个字符就跳出循环
val index = str.indexOf(key, searchIndex, true) // true表示忽略大小写
if(index != -1) {
// 匹配到了关键词
val keyword = str.substring(index, index + key.length) // 和key一样,只是大小写不一定一样
val text = str.substring(searchIndex, index + key.length)
searchIndex = index + key.length
textArr.add(text to keyword)
} else {
if(searchIndex != str.length) {
// 没匹配到关键词就把str和搜索关键词添加到textArr集合中
textArr.add(str.subString(searchIndex) to key)
}
}
}
val builder = StringBuilder()
textArr.forEach {
builder.append(
if (!it.first.contains(it.second, true)) {
it.first
} else
it.first.replace(
it.second,
fontStart + emStart + it.second + emEnd + fontEnd
)
)
}
return builder.toString()
}
在while
第一次循环中:
val str = "Wandroid项目采用Kotlin语言编写,kotlin语言真好用" // length = 34
val key = "Kotlin语言"
val index = str.indexOf(key, 0, true) // index = 12
得到关键词key
在str
的第12个位置开始,index
不为-1,说明匹配到了关键词,则进入以下代码
val keyword = str.substring(index, index + key.length) // str.substring(12, 12 + 8)
val text = str.substring(searchIndex, index + key.length) // str.substring(0, 12 + 8)
searchIndex = index + key.length // 12 + 8
textArr.add(text to keyword)
以上代码得出:
keyword = Kotlin语言
text = Wandroid项目采用Kotlin语言
searchIndex = 20
textArr = [(Wandroid项目采用Kotlin语言, Kotlin语言)]
在while
第二次循环中:
val str = "Wandroid项目采用Kotlin语言编写,kotlin语言真好用"
val key = "Kotlin语言"
val index = str.indexOf(key, 20, true) // index = 23
这次定位索引是从字符串str
的第二十的位置开始,得出index
为23,则进入以下代码
val keyword = str.substring(index, index + key.length) // str.substring(23, 23 + 8)
val text = str.substring(searchIndex, index + key.length) // str.substring(20, 23 + 8)
searchIndex = index + key.length // 23 + 8
textArr.add(text to keyword)
以上代码得出:
keyword = kotlin语言 // 小写k
text = 编写,kotlin语言
searchIndex = 31
textArr = [(Wandroid项目采用Kotlin语言, Kotlin语言), (编写,kotlin语言, kotlin语言)]
目前searchIndex = 31
,还没有超出str.length = 34
的范围,所以还会进入第三次循环
在while
第三次循环中:
val str = "Wandroid项目采用Kotlin语言编写,kotlin语言真好用"
val key = "Kotlin语言"
val index = str.indexOf(key, 31, true) // index = -1
因为str
从第31位开始就只有真好用
这三个字了,所以是匹配不到关键词了,index
返回-1
,进入了else
的代码块中
if(searchIndex != str.length) { // 31 != 34 = true
// 没匹配到关键词就把str和搜索关键词添加到textArr集合中
textArr.add(str.subString(searchIndex) to key) // textArr.add("真好用" to "Kotlin语言")
}
执行完后到break
处跳出循环。
到目前为止,textArr
的值为:
[("Wandroid项目采用Kotlin语言", "Kotlin语言"), ("编写,kotlin语言", "kotlin语言"), ("真好用", "Kotlin语言")]
从上面textArr
的结果中可以看出分词规则是这样的:
-
从
searchIndex
到index + key.length
截取的字符串作为Pair.first
,一个分词片段 -
从
index
到index + key.length
截取的字符串作为Pair.second
,保存这个是为了要还原原字符串的大小写,避免原字符串中是大写,而搜索关键词是小写,造成最后replace
的时候把原字符串中的大写替换成了小写。
既然得出了textArr
,接下来就要遍历textArr
,然后依次给匹配的关键词增加HTML
标签
val builder = StringBuilder()
textArr.forEach {
builder.append(
// it是textArr的每一个元素,类型是Pair<String, String>
// 判断每一个分词片段是否包含关键词
if (!it.first.contains(it.second, true)) {
// 不包含关键词,直接将分词片段返回拼接
it.first
} else
// 包含关键词,将分词片段中的关键词用增加了标签的字符串替换,保持了原有字符串中的大小写
it.first.replace(
it.second,
fontStart + emStart + it.second + emEnd + fontEnd
)
)
}
想要斜体+红色字体效果,我们就要像以下格式在关键词前后添加标签
<font color='red'><em class='highlight'>这是高亮文字</em></font>
再来捋一遍,textArr
中有三个元素,遍历会执行三次forEach
代码块中的代码
第一次
it = ("Wandroid项目采用Kotlin语言", "Kotlin语言")
,it.first = "Wandroid项目采用Kotlin语言"
中包含it.second = "Kotlin语言"
,会执行以下代码
it.first.replace(it.second,fontStart + emStart + it.second + emEnd + fontEnd)
即,将Wandroid项目采用Kotlin语言
替换成Wandroid项目采用<font color='red'><em class='highlight'>Kotlin语言</em></font>
此时的builder
为Wandroid项目采用<font color='red'><em class='highlight'>Kotlin语言</em></font>
;
第二次
it = ("编写,kotlin语言", "kotlin语言")
,it.first = "编写,kotlin语言"
中包含it.second = "kotlin语言"
,会执行以下代码
it.first.replace(it.second,fontStart + emStart + it.second + emEnd + fontEnd)
即,将编写,kotlin语言
替换成编写,<font color='red'><em class='highlight'>kotlin语言</em></font>
与第一次得到的builder
拼接,此时的builder
为Wandroid项目采用<font color='red'><em class='highlight'>Kotlin语言</em></font>编写,<font color='red'><em class='highlight'>kotlin语言</em></font>
;
第三次
it = ("真好用", "Kotlin语言")
,it.first = "真好用"
中不包含it.second = "Kotlin语言"
,会直接将it.first
返回与builder
拼接
即,将真好用
拼接在builder
后面。
此时的builder
为:
Wandroid项目采用<font color='red'><em class='highlight'>Kotlin语言</em></font>编写,<font color='red'><em class='highlight'>kotlin语言</em></font>真好用
最后
textView.text = HtmlCompat.fromHtml(builder.toString(), HtmlCompat.FROM_HTML_MODE_LEGACY)
就可以显示出高亮效果了。
多关键词高亮
到目前为止,显示高亮效果的还只是单关键词,那么如何实现多关键词高亮效果呢?单关键词的思路已经有了,其实多关键词高亮只需要将多关键词分割成一个个的单关键词就好了。
假设多关键词用空格分开,我们就需要用key.split(" ")
将一个关键词字符串分割成多个关键词,避免一些不规范的输入,如前后有空格或者中间有不止一个空格,那就要消除这些不规范。
// 替换所有空格为一个空格
fun String?.replaceAllEmptyToOne(): String {
if (this.isNullOrEmpty()) return ""
val pattern = Pattern.compile("\\s+")
val matcher = pattern.matcher(this)
return matcher.replaceAll(" ")
}
val keys = key.trim() // 去除首尾空格
.toLowerCase(Locale.getDefault()) // 全部转成小写,忽略大小写
.replaceAllEmptyToOne() // 将多个连续的空格替换成一个
.splt(" ") // 分割关键词
.toSet() // 去除重复的关键词
通过一系列的操作得到的keys
是一个比较规范的关键词组,通过遍历依次对每个单关键词进行高亮操作即可。
// CharSequenceExt.kt
// 多关键词高亮
fun CharSequence?.makeTextHighlightForMultiKeys(key: String): CharSequence {
if (this.isNullOrEmpty()) return ""
val keys = key.trim().toLowerCase(Locale.getDefault()).replaceAllEmptyToOne().split(" ").toSet()
var result: CharSequence = this
keys.forEach {
result = result.appendHtmlTags(it) // 对每个单关键词添加HTML标签
}
return HtmlCompat.fromHtml(result.toString(), HtmlCompat.FROM_HTML_MODE_LEGACY)
}
// MainActivity.kt
val text = "Wandroid项目采用Kotlin语言编写,kotlin语言真好用"
val result = text.makeTextHighlightForMultiKeys("Wandroid Kotlin")
tvText.text = result
到此,已经全部完成了,下面附上完整代码
import androidx.core.text.HtmlCompat
import java.util.*
import java.util.regex.Pattern
const val emStart = "<em class='highlight'>"
const val emEnd = "</em>"
const val fontStart = "<font color='red'>"
const val fontEnd = "</font>"
// 多关键词高亮
fun CharSequence?.makeTextHighlightForMultiKeys(key: String): CharSequence {
if (this.isNullOrEmpty()) return ""
val keys = key.trim().toLowerCase(Locale.getDefault()).replaceAllEmptyToOne().split(" ").toSet()
var result: CharSequence = this
keys.forEach {
result = result.appendHtmlTags(it)
}
return HtmlCompat.fromHtml(result.toString(), HtmlCompat.FROM_HTML_MODE_LEGACY)
}
// 搜索到的标题文本标红处理,返回的文本带有<em>标签
fun String?.toSearchTitleColorString(): CharSequence {
return if (this.isNullOrEmpty()) "" else HtmlCompat.fromHtml(
if (this.contains(emStart)) {
this.replace(emStart, fontStart + emStart)
.replace(emEnd, emEnd + fontEnd)
} else {
this
},
HtmlCompat.FROM_HTML_MODE_LEGACY
)
}
fun CharSequence?.appendHtmlTags(key: String): CharSequence {
if (this.isNullOrEmpty()) return ""
if (!this.contains(key, true)) return this
// 解析出整个字符串中所有包含key的位置
val textArr: MutableList<Pair<String, String>> = mutableListOf()
var searchIndex = 0
while (searchIndex < this.length) {
val index = this.indexOf(key, searchIndex, true)
if (index != -1) {
// 能匹到
val keyword = this.substring(index, index + key.length) // 和key一样,只是大小写不一定一样
val text = this.substring(searchIndex, index + key.length)
searchIndex = index + key.length
textArr.add(text to keyword)
} else {
if (searchIndex != length) {
// 还有字符串
textArr.add(substring(searchIndex) to key)
}
break
}
}
val builder = StringBuilder()
textArr.forEach {
builder.append(
if (!it.first.contains(it.second, true)) {
it.first
} else
it.first.replace(
it.second,
fontStart + emStart + it.second + emEnd + fontEnd
)
)
}
return builder.toString()
}
// 替换所有空格为一个空格
fun String?.replaceAllEmptyToOne(): String {
if (this.isNullOrEmpty()) return ""
val pattern = Pattern.compile("\\s+")
val matcher = pattern.matcher(this)
return matcher.replaceAll(" ")
}
it.first.replace(
it.second,
fontStart + emStart + it.second + emEnd + fontEnd
)
)
}
return builder.toString()
}
// 替换所有空格为一个空格
fun String?.replaceAllEmptyToOne(): String {
if (this.isNullOrEmpty()) return ""
val pattern = Pattern.compile("\\s+")
val matcher = pattern.matcher(this)
return matcher.replaceAll(" ")
}
网友评论