Android TextView对URL识别

IM开发过程中,对文本消息中的超练级进行点击处理,使用系统的tv.setAutoLinkMask(Linkify.PHONE_NUMBERS | Linkify.WEB_URLS);方法:

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
/**
* 拦截超链接
*/
public static void interceptHyperLink(TextView tv, ChatContext chatContext, int msg_type,
long msg_id, String send_ucid) {
tv.setAutoLinkMask(Linkify.PHONE_NUMBERS | Linkify.WEB_URLS);
tv.setMovementMethod(LinkMovementMethod.getInstance());
CharSequence text = tv.getText();
if (text instanceof Spannable) {
int end = text.length();
Spannable spannable = (Spannable) tv.getText();
URLSpan[] urlSpans = spannable.getSpans(0, end, URLSpan.class);
if (urlSpans.length == 0) {
return;
}

SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(text);
for (URLSpan uri : urlSpans) {
String url = uri.getURL();
CustomURLSpan custom = new CustomURLSpan(url, chatContext, msg_type, msg_id, send_ucid);
spannableStringBuilder.setSpan(custom, spannableStringBuilder.getSpanStart(uri),
spannableStringBuilder.getSpanEnd(uri), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
tv.setText(spannableStringBuilder);
}
}

Android自带的表达式(android.util.Patterns),在不同的ROM上表现形式是不一样的,在一些比较诡异的case上基本识别不出来,比如对于http://lianjia.com/xxx 啊啊啊这种连接,华为手机正常识别了,三星手机把后面的汉字也一起识别了,手机兼容性问题,最后只能自己写正则去匹配:

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
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
public class LinkifySpannableUtils {

public static LinkifySpannableUtils mInstance;

private Context mContext;
private TextView mTextView;
private SpannableStringBuilder mSpannableStringBuilder;

private LinkifySpannableUtils() {
}

public static LinkifySpannableUtils getInstance() {
if (mInstance == null) {
mInstance = new LinkifySpannableUtils();
}
return mInstance;
}

public void setSpan(Context context, TextView textView) {
this.mContext = context;
this.mTextView = textView;
addLinks();
}

private void addLinks() {
Linkify.addLinks(mTextView, WEB_URL, null);
Linkify.addLinks(mTextView, EMAIL_ADDRESS, null);
Linkify.addLinks(mTextView, PHONE, null);

CharSequence cSequence = mTextView.getText();
if (cSequence instanceof Spannable) {
int end = mTextView.getText().length();
Spannable sp = (Spannable) mTextView.getText();
URLSpan[] urls = sp.getSpans(0, end, URLSpan.class);
mSpannableStringBuilder = new SpannableStringBuilder(sp);
mSpannableStringBuilder.clearSpans();

for (URLSpan url : urls) {
String urlString = url.getURL();
PatternURLSpan patternURLSpan = new PatternURLSpan(urlString);
if (urlString != null && urlString.length() > 0) {
int _start = sp.getSpanStart(url);
int _end = sp.getSpanEnd(url);
try {
mSpannableStringBuilder.setSpan(patternURLSpan, _start, _end, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
} catch (Exception e) {
e.printStackTrace();
}
}
}
mTextView.setLinkTextColor(ColorStateList.valueOf(Color.BLUE));
mTextView.setHighlightColor(Color.parseColor("#AAAAAA"));
mTextView.setText(mSpannableStringBuilder);
}
}

private class PatternURLSpan extends ClickableSpan {

private String mString;

PatternURLSpan(String str) {
this.mString = str;
}

@Override
public void onClick(View widget) {
if (EMAIL_ADDRESS.matcher(mString).find()) {
sendEmail(mString);
} else if (WEB_URL.matcher(mString).find()) {
openUrl(mString);
} else if (PHONE.matcher(mString).find()) {
dialNum(mString);
} else {
if (mString.contains(".")) {
if (mString.startsWith("http")) {
openUrl(mString);
} else {
openUrl("http://" + mString);
}
}
}
}
}


/**
* 打开系统浏览器
* @param url
*/
private void openUrl(String url) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(url));
intent.setClassName("com.android.browser",
"com.android.browser.BrowserActivity");
mContext.startActivity(intent);
}


/**
* 拨打电话
* @param num
*/
private void dialNum(final String num) {
if (num != null && num.length() > 0) {
call(num, mContext);
}
}

/**
* 调用邮箱
* @param address
*/
private void sendEmail(String address) {
String[] receive = new String[]{address};
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("plain/text");
intent.putExtra(Intent.EXTRA_EMAIL, receive);
mContext.startActivity(Intent.createChooser(intent, ""));
}


private void call(final String mobile, final Context activity) {
if (mobile == null || mobile.length() == 0) {
Toast.makeText(activity, "电话号码为空", Toast.LENGTH_SHORT).show();
return;
}
String phone = mobile.toLowerCase();
if (!phone.startsWith("tel:")) {
phone = "tel:" + mobile;
}
final String callMobile = phone;

//适配6.0系统,申请权限
if (ActivityCompat.checkSelfPermission(activity, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {

ActivityCompat.requestPermissions((Activity) activity,
new String[]{Manifest.permission.CALL_PHONE},
MainActivity.REQUESTCODE);
}else {
callPhone(activity,callMobile);
}


}

public static void callPhone(Context activity, String callMobile) {
Intent intent = new Intent(Intent.ACTION_CALL, Uri.parse(callMobile));
if (ActivityCompat.checkSelfPermission(activity, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
return;
}
activity.startActivity(intent);
}


public final String TOP_LEVEL_DOMAIN_STR_FOR_WEB_URL =
"(?:"
+ "(?:aero|arpa|asia|a[cdefgilmnoqrstuwxz])"
+ "|(?:biz|b[abdefghijmnorstvwyz])"
+ "|(?:cat|com|coop|c[acdfghiklmnoruvxyz])"
+ "|d[ejkmoz]"
+ "|(?:edu|e[cegrstu])"
+ "|f[ijkmor]"
+ "|(?:gov|g[abdefghilmnpqrstuwy])"
+ "|h[kmnrtu]"
+ "|(?:info|int|i[delmnoqrst])"
+ "|(?:jobs|j[emop])"
+ "|k[eghimnprwyz]"
+ "|l[abcikrstuvy]"
+ "|(?:mil|mobi|museum|m[acdeghklmnopqrstuvwxyz])"
+ "|(?:name|net|n[acefgilopruz])"
+ "|(?:org|om)"
+ "|(?:pro|p[aefghklmnrstwy])"
+ "|qa"
+ "|r[eosuw]"
+ "|s[abcdeghijklmnortuvyz]"
+ "|(?:tel|travel|t[cdfghjklmnoprtvwz])"
+ "|u[agksyz]"
+ "|v[aceginu]"
+ "|w[fs]"
+ "|(?:\u03b4\u03bf\u03ba\u03b9\u03bc\u03ae|\u0438\u0441\u043f\u044b\u0442\u0430\u043d\u0438\u0435|\u0440\u0444|\u0441\u0440\u0431|\u05d8\u05e2\u05e1\u05d8|\u0622\u0632\u0645\u0627\u06cc\u0634\u06cc|\u0625\u062e\u062a\u0628\u0627\u0631|\u0627\u0644\u0627\u0631\u062f\u0646|\u0627\u0644\u062c\u0632\u0627\u0626\u0631|\u0627\u0644\u0633\u0639\u0648\u062f\u064a\u0629|\u0627\u0644\u0645\u063a\u0631\u0628|\u0627\u0645\u0627\u0631\u0627\u062a|\u0628\u06be\u0627\u0631\u062a|\u062a\u0648\u0646\u0633|\u0633\u0648\u0631\u064a\u0629|\u0641\u0644\u0633\u0637\u064a\u0646|\u0642\u0637\u0631|\u0645\u0635\u0631|\u092a\u0930\u0940\u0915\u094d\u0937\u093e|\u092d\u093e\u0930\u0924|\u09ad\u09be\u09b0\u09a4|\u0a2d\u0a3e\u0a30\u0a24|\u0aad\u0abe\u0ab0\u0aa4|\u0b87\u0ba8\u0bcd\u0ba4\u0bbf\u0baf\u0bbe|\u0b87\u0bb2\u0b99\u0bcd\u0b95\u0bc8|\u0b9a\u0bbf\u0b99\u0bcd\u0b95\u0baa\u0bcd\u0baa\u0bc2\u0bb0\u0bcd|\u0baa\u0bb0\u0bbf\u0b9f\u0bcd\u0b9a\u0bc8|\u0c2d\u0c3e\u0c30\u0c24\u0c4d|\u0dbd\u0d82\u0d9a\u0dcf|\u0e44\u0e17\u0e22|\u30c6\u30b9\u30c8|\u4e2d\u56fd|\u4e2d\u570b|\u53f0\u6e7e|\u53f0\u7063|\u65b0\u52a0\u5761|\u6d4b\u8bd5|\u6e2c\u8a66|\u9999\u6e2f|\ud14c\uc2a4\ud2b8|\ud55c\uad6d|xn\\-\\-0zwm56d|xn\\-\\-11b5bs3a9aj6g|xn\\-\\-3e0b707e|xn\\-\\-45brj9c|xn\\-\\-80akhbyknj4f|xn\\-\\-90a3ac|xn\\-\\-9t4b11yi5a|xn\\-\\-clchc0ea0b2g2a9gcd|xn\\-\\-deba0ad|xn\\-\\-fiqs8s|xn\\-\\-fiqz9s|xn\\-\\-fpcrj9c3d|xn\\-\\-fzc2c9e2c|xn\\-\\-g6w251d|xn\\-\\-gecrj9c|xn\\-\\-h2brj9c|xn\\-\\-hgbk6aj7f53bba|xn\\-\\-hlcj6aya9esc7a|xn\\-\\-j6w193g|xn\\-\\-jxalpdlp|xn\\-\\-kgbechtv|xn\\-\\-kprw13d|xn\\-\\-kpry57d|xn\\-\\-lgbbat1ad8j|xn\\-\\-mgbaam7a8h|xn\\-\\-mgbayh7gpa|xn\\-\\-mgbbh1a71e|xn\\-\\-mgbc0a9azcg|xn\\-\\-mgberp4a5d4ar|xn\\-\\-o3cw4h|xn\\-\\-ogbpf8fl|xn\\-\\-p1ai|xn\\-\\-pgbs0dh|xn\\-\\-s9brj9c|xn\\-\\-wgbh1c|xn\\-\\-wgbl6a|xn\\-\\-xkc2al3hye2a|xn\\-\\-xkc2dl3a5ee0h|xn\\-\\-yfro4i67o|xn\\-\\-ygbi2ammx|xn\\-\\-zckzah|xxx)"
+ "|y[et]" + "|z[amw]))";

public final String GOOD_IRI_CHAR = "a-zA-Z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF";


public final Pattern WEB_URL = Pattern
.compile("((?:(http|https|Http|Https|rtsp|Rtsp):\\/\\/(?:(?:[a-zA-Z0-9\\$\\-\\_\\.\\+\\!\\*\\'\\(\\)"
+ "\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,64}(?:\\:(?:[a-zA-Z0-9\\$\\-\\_"
+ "\\.\\+\\!\\*\\'\\(\\)\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,25})?\\@)?)?"
+ "((?:(?:["
+ GOOD_IRI_CHAR
+ "]["
+ GOOD_IRI_CHAR
+ "\\-]{0,64}\\.)+" // named host
+ TOP_LEVEL_DOMAIN_STR_FOR_WEB_URL
+ "|(?:(?:25[0-5]|2[0-4]" // or ip address
+ "[0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9])\\.(?:25[0-5]|2[0-4][0-9]"
+ "|[0-1][0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(?:25[0-5]|2[0-4][0-9]|[0-1]"
+ "[0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(?:25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}" + "|[1-9][0-9]|[0-9])))"
+ "(?:\\:\\d{1,5})?)" // plus option port number
+ "(\\/(?:(?:[" + GOOD_IRI_CHAR + "\\;\\/\\?\\:\\@\\&\\=\\#\\~" // plus option query
// params
+ "\\-\\.\\+\\!\\*\\'\\(\\)\\,\\_])|(?:\\%[a-fA-F0-9]{2}))*)?" + "(?:\\b|$)");

public static final Pattern EMAIL_ADDRESS = Pattern.compile("[a-zA-Z0-9\\+\\.\\_\\%\\-\\+]{1,256}" + "\\@"
+ "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,64}" + "(" + "\\." + "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,25}" + ")+");
public static final Pattern EMAIL_PATTERN = Pattern.compile("[A-Z0-9a-z\\._%+-]+@([A-Za-z0-9-]+\\.)+[A-Za-z]{2,4}");
public static final Pattern WEB_PATTERN =
Pattern
.compile("((http[s]{0,1}|ftp)://[a-zA-Z0-9\\.\\-]+\\.([a-zA-Z]{2,4})(:\\d+)?(/[a-zA-Z0-9\\.\\-~!@#$%^&*+?:_/=<>]*)?)|(www.[a-zA-Z0-9\\.\\-]+\\.([a-zA-Z]{2,4})(:\\d+)?(/[a-zA-Z0-9\\.\\-~!@#$%^&*+?:_/=<>]*)?)");

public static final Pattern PHONE = Pattern.compile( // sdd = space, dot, or dash
"(\\+[0-9]+[\\- \\.]*)?" // +<digits><sdd>*
+ "(\\([0-9]+\\)[\\- \\.]*)?" // (<digits>)<sdd>*
+ "([0-9][0-9\\- \\.][0-9\\- \\.]+[0-9])");


}

上述WEB_URL正则仍不能正常识别,最后采用:

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
// all domain names
private static final String[] ext = {
"top", "com.cn", "com", "net", "org", "edu", "gov", "int", "mil", "cn", "tel", "biz", "cc", "tv", "info",
"name", "hk", "mobi", "asia", "cd", "travel", "pro", "museum", "coop", "aero", "ad", "ae", "af",
"ag", "ai", "al", "am", "an", "ao", "aq", "ar", "as", "at", "au", "aw", "az", "ba", "bb", "bd",
"be", "bf", "bg", "bh", "bi", "bj", "bm", "bn", "bo", "br", "bs", "bt", "bv", "bw", "by", "bz",
"ca", "cc", "cf", "cg", "ch", "ci", "ck", "cl", "cm", "cn", "co", "cq", "cr", "cu", "cv", "cx",
"cy", "cz", "de", "dj", "dk", "dm", "do", "dz", "ec", "ee", "eg", "eh", "es", "et", "ev", "fi",
"fj", "fk", "fm", "fo", "fr", "ga", "gb", "gd", "ge", "gf", "gh", "gi", "gl", "gm", "gn", "gp",
"gr", "gt", "gu", "gw", "gy", "hk", "hm", "hn", "hr", "ht", "hu", "id", "ie", "il", "in", "io",
"iq", "ir", "is", "it", "jm", "jo", "jp", "ke", "kg", "kh", "ki", "km", "kn", "kp", "kr", "kw",
"ky", "kz", "la", "lb", "lc", "li", "lk", "lr", "ls", "lt", "lu", "lv", "ly", "ma", "mc", "md",
"mg", "mh", "ml", "mm", "mn", "mo", "mp", "mq", "mr", "ms", "mt", "mv", "mw", "mx", "my", "mz",
"na", "nc", "ne", "nf", "ng", "ni", "nl", "no", "np", "nr", "nt", "nu", "nz", "om", "qa", "pa",
"pe", "pf", "pg", "ph", "pk", "pl", "pm", "pn", "pr", "pt", "pw", "py", "re", "ro", "ru", "rw",
"sa", "sb", "sc", "sd", "se", "sg", "sh", "si", "sj", "sk", "sl", "sm", "sn", "so", "sr", "st",
"su", "sy", "sz", "tc", "td", "tf", "tg", "th", "tj", "tk", "tm", "tn", "to", "tp", "tr", "tt",
"tv", "tw", "tz", "ua", "ug", "uk", "us", "uy", "va", "vc", "ve", "vg", "vn", "vu", "wf", "ws",
"ye", "yu", "za", "zm", "zr", "zw"
};

static {
StringBuilder sb = new StringBuilder();
sb.append("(");
for (int i = 0; i < ext.length; i++) {
sb.append(ext[i]);
sb.append("|");
}
sb.deleteCharAt(sb.length() - 1);
sb.append(")");
// final pattern str
String pattern = "((https?|s?ftp|irc[6s]?|git|afp|telnet|smb)://)?((\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})|((www\\.|[a-zA-Z\\.\\-]+\\.)?[a-zA-Z0-9\\-]+\\." + sb.toString() + "(:[0-9]{1,5})?))((/[a-zA-Z0-9\\./,;\\?'\\+&%\\$#=~_\\-]*)|([^\\u4e00-\\u9fa5\\s0-9a-zA-Z\\./,;\\?'\\+&%\\$#=~_\\-]*))";
// Log.v(TAG, "pattern = " + pattern);
WEB_URL = Pattern.compile(pattern);
}

设置了Linkify.addLinks后导致ClickableSpan的点击无法拦截

设置了Linkify.addLinks后导致ClickableSpan的点击无法拦截,会调用隐式意图打开配置了filter的Activity 使用下面步骤:

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
public static void interceptHyperLink(TextView tv, ChatContext chatContext, int msg_type,
long msg_id, String send_ucid) {
tv.setMovementMethod(LinkMovementMethod.getInstance());
CharSequence text = tv.getText();
if (text instanceof Spannable) {
SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(text.toString());
Linkify.addLinks(spannableStringBuilder, CommonPatterns.CHINESE_PHONE_NUMBER, PHONE_SCHEME);
Linkify.addLinks(spannableStringBuilder, CommonPatterns.WEB_URL, HTTP_SCHEME);
Linkify.addLinks(spannableStringBuilder, CommonPatterns.AUTOLINK_WEB_URL, HTTP_SCHEME);

URLSpan[] urlSpans = spannableStringBuilder.getSpans(0, spannableStringBuilder.length(), URLSpan.class);
if (urlSpans.length == 0) {
return;
}
for (URLSpan uri : urlSpans) {
String url = uri.getURL();
CustomURLSpan custom = new CustomURLSpan(url, chatContext, msg_type, msg_id, send_ucid);
int spanStart = spannableStringBuilder.getSpanStart(uri);
int spanEnd = spannableStringBuilder.getSpanEnd(uri);
spannableStringBuilder.removeSpan(uri);
spannableStringBuilder.setSpan(custom, spanStart,
spanEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
tv.setText(spannableStringBuilder);
}
}

当需要使自定义模式和内置模式web,phone等一起被识别时,一定要先声明内置模式,然后再声明自定义模式,而且不能在xml中通过autoLink属性声明,否则自定义模式不起作用。因为在设置内置模式时,会先删除已有模式。

使用该方式拦截点击事件的话,Linkify.addLinks(spannableStringBuilder, CommonPatterns.WEB_URL, HTTP_SCHEME); http和https需要分开,如果不分开,https的链接也会被加上http变成http://http://xxx,同时HTTP_SCHEME不能设置为空,如果设置为空的话,不再判断系统的scheme头,如baidu.com不会自动增加http变成https://baidu.com.

坚持原创技术分享,您的支持将鼓励我继续创作!