Fork me on GitHub

Android全文关键字高亮与关键字点击

思路

因为自己看过很多杂七杂八的东西,所以知道 TextView 本身可以解析一些 html 的标签,而且 Span String 可以花式实现各种样式。这里采用 Span String 的方式实现。实现这个需求的难点在哪呢?刚开始在我看来就是实现关键字匹配,刚开始想用 spilt
方法直接分割自己添加 keyword 取巧,后来细想不行,不是所有的输入都能完成,多个关键字这种方式太复杂。没办法,想起以前学过的 kmp 算法,打开了个网页准备开撸了,忽然想到搜一下全文关键字匹配,很容易的就搜到了 Java 中对应的 API ,搜索也是需要技巧的……不能太过于想当然了。效果图:

图

实现

实现过程还算顺利,不多 BB 直接放代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
package com.xiasuhuei321.forspanstring;

import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextPaint;
import android.text.TextUtils;
import android.text.style.ClickableSpan;
import android.text.style.ForegroundColorSpan;
import android.view.View;

import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* Created by xiasuhuei321 on 2017/10/26.
* author:luo
* e-mail:xiasuhuei321@163.com
*
* desc:本类提供了几种给字符串添加颜色、点击事件的方法
*/

public class RichTextUtil {

/**
* 获取带颜色的文本,设定关键字颜色,这里只接受一个关键字,并且没有点击事件
*
* @param originText 原始文本
* @param keyword 需要颜色的文字
* @param color 颜色
* @return CharSequence 处理后的文字
*/
public static CharSequence getColorString(String originText, String keyword, int color) {
return getColorString(originText, keyword, color, null);
}

/**
* 获取带颜色的文本,将给定的元是字符串
*
* @param originText 原始文本
* @param keyword 关键字
* @param color 颜色
* @param listener 点击关键字的监听回调,可空
* @return
*/
public static CharSequence getColorString(String originText, String keyword, int color,
final View.OnClickListener listener) {
SpannableString s = new SpannableString(originText);
Pattern p = Pattern.compile(keyword);
Matcher m = p.matcher(s);

while (m.find()) {
int start = m.start();
int end = m.end();
s.setSpan(new ForegroundColorSpan(color), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

if (listener != null) {
s.setSpan(new ClickableSpan() {
@Override
public void onClick(View widget) {
listener.onClick(widget);
}

@Override
public void updateDrawState(TextPaint ds) {
ds.setColor(color);
ds.setUnderlineText(false);
}
}, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}

return s;
}

public static CharSequence getColorString(String originText, List<String> keywords,
Map<String, Integer> colorMap) {
return getColorString(originText, keywords, colorMap, null);
}

public static CharSequence getColorString(String originText, List<String> keywords,
final Map<String, Integer> colorMap, Map<String, View.OnClickListener> listenerMap) {
SpannableString s = new SpannableString(originText);

for (int i = 0; i < keywords.size(); i++) {
final String keyword = keywords.get(i);
Pattern p = Pattern.compile(keyword);
Matcher m = p.matcher(s);

while (m.find()) {
int start = m.start();
int end = m.end();

s.setSpan(new ForegroundColorSpan(colorMap.get(keyword)), start, end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

if (listenerMap != null) {
final View.OnClickListener listener = listenerMap.get(keyword);
if (listener != null) {
s.setSpan(new ClickableSpan() {
@Override
public void onClick(View widget) {
listener.onClick(widget);
}

@Override
public void updateDrawState(TextPaint ds) {
ds.setColor(colorMap.get(keyword));
ds.setUnderlineText(false);
}
}, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
}
}

return s;
}

private SpannableStringBuilder builder = null;

/**
* 拼接出特殊的文字,可以选择加入颜色和点击事件
*
* @param str 加入的文字
* @return 本对象
*/
public RichTextUtil append(String str) {
return append(str, -1, null);
}

/**
* 拼接出特殊的文字,可以选择加入颜色和点击事件
*
* @param str 加入的文字
* @param color 颜色
* @return 本对象
*/
public RichTextUtil append(String str, int color) {
return append(str, color, null);
}

/**
* 拼接出特殊的文字,可以选择加入颜色和点击事件
*
* @param str 加入的文字
* @param color 颜色
* @param listener 点击事件
* @return 本对象
*/
public RichTextUtil append(String str, int color, View.OnClickListener listener) {
if (TextUtils.isEmpty(builder)) {
builder = new SpannableStringBuilder();
}

if (TextUtils.isEmpty(str)) {
BlackgagaLogger.debug("传入的str为 null!请检查!");
return null;
}

if (color == -1) {
builder.append(str);
return this;
}

SpannableString span = new SpannableString(str);
span.setSpan(new ForegroundColorSpan(color), 0, str.length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

if (listener != null) {
span.setSpan(new ClickableSpan() {
@Override
public void onClick(View widget) {
listener.onClick(widget);
}

@Override
public void updateDrawState(TextPaint ds) {
ds.setColor(color);
ds.setUnderlineText(false);
}
}, 0, str.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}

builder.append(span);
return this;
}

public SpannableStringBuilder finish() {
return builder;
}

}

上面用了 View.OnClickListener 来传递事件,因为我发现同一个 ClickableSpan 给多个 Span 设置的时候只有第一个点击事件是有效的。又不想自己多加一个接口,就用了 Android 里现成的接口了。最后要注意一下,一定要设置:

1
textView.setMovementMethod(LinkMovementMethod.getInstance());

不然无法响应点击事件。

主要提供了两大类 api,一类是直接传入全文和所有的关键字,以及关键字对应的点击事件和颜色值。这类使用起来的构造也是比较麻烦的,而且如果碰到指定部分关键字高亮,而另外一些匹配的不高亮(比如匹配数字,很有可能一些数字中包含了你的数字关键字,但问题是你不想让这部分高亮),实现起来就更麻烦了。所以又添加了一类 api 类似 StringBuilder 的方法,自己从零开始构造一个 SpannableStringBuilder,而这个类是 CharSequence 的子类,TextView 可以直接设置。