完整源码在我的github上 https://github.com/NashLegend/QuicKid
有了匹配算法,下面是如何将搜索联系人并显示出来以及高亮显示匹配到的字符串。
1.搜索并显示联系人
显示列表当然是使用ListView,使用自定义的ContactAdapter,ContactAdapter继承BaseAdapter,并实现了Filterable接口,此接口中有一个getFilter方法,返回一个过滤用的类Filter,Filter需要自己实现,我们就是通过这个Filter实现搜索的。
Filter类有两个方法,publishResults和performFiltering方法,其中publishResults运行在UI线程,而performFiltering运行在其他线程,搜索的过程就在performFiltering中执行。
下面是自己实现的Filter类
@Override
public Filter getFilter() {
return filter;
}
// 上一次搜索的字符串
private String preQueryString = "";
private Filter filter = new Filter() {
@Override
protected void publishResults(CharSequence constraint,
FilterResults results) {
if (results != null) {
if (results.count > 0) {
notifyDataSetChanged();
} else {
notifyDataSetInvalidated();
}
}
}
@Override
synchronized protected FilterResults performFiltering(CharSequence constraint) {
if (TextUtils.isEmpty(constraint)
|| preQueryString.equals(constraint)) {
return null;
}
String queryString = constraint.toString();
FilterResults results = new FilterResults();
int preLength = preQueryString.length();
int queryLength = queryString.length();
ArrayList<Contact> baseList = new ArrayList<Contact>();
ArrayList<Contact> resultList = new ArrayList<Contact>();
if (preLength > 0 && (preLength == queryLength - 1)
&& queryString.startsWith(preQueryString)) {
//如果本次搜索的字符串是上次搜索的字符串开头,那么将只在contacts里面搜索(contacts是当前列表的数据集合)
baseList = contacts;
} else {
//过滤所有联系人
baseList = AllContacts;
}
for (Iterator<Contact> iterator = baseList.iterator(); iterator
.hasNext();) {
Contact contact = (Contact) iterator.next();
if (contact.match(queryString) > 0) {
resultList.add(contact);
}
}
sortContact(resultList);// 这是ContactAdapter中的方法,将ContactAdapter的数据换成resultList。
preQueryString = queryString;
results.values = resultList;
results.count = resultList.size();
setContacts(resultList);
return results;
}
};
如果用户搜索的手速十分快的话将会带来线程同步的问题。在执行performFiltering的时候有可能正在执行ContactAdapter的getView方法,而match()方法是有可能改变Contact的数据的,这将导致显示出错。比如未匹配到结果的话,Contact的匹配结果的nameIndex会是-1,如果在上次搜索中某用户成功匹配,nameIndex=0,就意味着将取用户的第一种拼音组合做为匹配结果,但是如果手速过快,在执行getView之前就进行了下一次搜索,那么有可能这个联系人不再匹配,这里的nameIndex将会是-1,取第-1个拼音的时候就会报错。这里的解决方法很简单,并没有做过多的保证同步的工作(让getView,publishResults和performFiltering不互相打断貌似是很困难的),所以如果发现nameIndex不对,就直接不显示这个拼音,因为用户操作非常之快,他是无法发现也没必要关心这几十毫秒的显示不正常的。
还有一个线程同步的问题,在notifyDataSetChanged之后,adapter会顺序执行getView,但是在getView的时候,setContacts可能又会执行,从而改变了contacts的长度,contacts.get(position)可能会发生越界的问题,因此这时候getView要捕获这个错误,返回一个空view,跟上次一样,空view存在时间很短,不会有人注意的……
搜索某个单词的时候,使用getFilter.filter(queryString)即可实现搜索。剩下的不用多说,都是普通的adapter和listview的问题。
2.高亮显示匹配的字符串
PanAnNing.高亮这里用的是SpannableStringBuilder。
高亮方法如下
if (contact.matchValue.matchLevel == Contact.Level_Complete) {
//如果是完全匹配,那么只要全部高亮对应的姓名拼音或者电话号码就OK了
if (contact.matchValue.matchType == Contact.Match_Type_Name) {
String str = contact.fullNamesString.get(
contact.matchValue.nameIndex).replaceAll(" ", "");
SpannableStringBuilder builder = new SpannableStringBuilder(
str);
ForegroundColorSpan redSpan = new ForegroundColorSpan(
Color.RED);
builder.setSpan(redSpan, 0, str.length(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
pinyinTextView.setText(builder);
} else {
shouldDisplayMorePhones = false;
String str = contact.getPhones().get(
contact.matchValue.nameIndex).phoneNumber;
SpannableStringBuilder builder = new SpannableStringBuilder(
str);
ForegroundColorSpan redSpan = new ForegroundColorSpan(
Color.RED);
builder.setSpan(redSpan, 0, str.length(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
phoneTextView.setText(builder);
}
} else if (contact.matchValue.matchLevel == Contact.Level_Headless) {
//如果是后置无头匹配,那么高亮从strIndex开始的regString长度的一串就行了
shouldDisplayMorePhones = false;
String str = contact.getPhones().get(
contact.matchValue.nameIndex).phoneNumber;
SpannableStringBuilder builder = new SpannableStringBuilder(str);
ForegroundColorSpan redSpan = new ForegroundColorSpan(Color.RED);
builder.setSpan(redSpan,
contact.matchValue.pairs.get(0).strIndex,
contact.matchValue.pairs.get(0).strIndex
+ contact.matchValue.reg.length(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
phoneTextView.setText(builder);
for (int i = 1; i < contact.matchValue.pairs.size(); i++) {
int idx = contact.matchValue.pairs.get(i).listIndex;
PhoneStruct phoneStruct = contact.getPhones().get(idx);
PhoneView phoneView = new PhoneView(getContext());
phoneView.setPhone(phoneStruct, contact.matchValue.reg);
phoneViews.addView(phoneView);
}
} else {
// 剩下的情况就是两个首字母匹配了。首字母匹配到的字符串位置不是连续的
// 匹配到的字母一个一个记录在contact.matchValue.pairs里面
// 所以要先将contact.matchValue.pairs里的一个个不连续的字母连接成几个字符串
String str = contact.fullNamesString.get(
contact.matchValue.nameIndex).replaceAll(" ", "");
ArrayList<PointPair> pa = getColoredString(
contact.fullNameNumber
.get(contact.matchValue.nameIndex),
contact.matchValue.pairs, "#FF0000");
SpannableStringBuilder builder = new SpannableStringBuilder(str);
for (Iterator<PointPair> iterator = pa.iterator(); iterator
.hasNext();) {
PointPair pointPair = iterator.next();
builder.setSpan(new ForegroundColorSpan(Color.RED),
pointPair.listIndex, pointPair.strIndex,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
pinyinTextView.setText(builder);
}
// getColoredString是将PointPairs列表单个的字符转化成几个字符串范围。这时候返回的PointPair的listIndex
// 变成了字符串开关的位置,strIndex变成了长度。builder.setSpan将使这几段范围内的字符高亮
private ArrayList<PointPair> getColoredString(ArrayList<String> strings,
ArrayList<PointPair> pairs, String color) {
int k = 0;
int idx = -1;
int crtHead = -1;
int crtTail = -1;
ArrayList<PointPair> ps = new ArrayList<PointPair>();
for (int i = 0; i < strings.size(); i++) {
String str = strings.get(i);
for (int j = 0; j < str.length() && k < pairs.size(); j++) {
idx++;
if (pairs.get(k).listIndex == i && pairs.get(k).strIndex == j) {
if (crtHead == -1) {
crtHead = idx;
crtTail = idx + 1;
} else {
if (crtTail == idx) {
crtTail = idx + 1;
}
}
k++;
} else {
if (crtHead != -1) {
ps.add(new PointPair(crtHead, crtTail));
crtHead = -1;
crtTail = -1;
}
}
}
}
if (crtHead != -1) {
ps.add(new PointPair(crtHead, crtTail));
crtHead = -1;
crtTail = -1;
}
return ps;
}
转载于:https://blog.51cto.com/nashlegend/1566479