首页 > 代码库 > 智能拨号匹配算法(三)

智能拨号匹配算法(三)

    完整源码在我的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.高亮显示匹配的字符串


    高亮显示匹配的字符串使用户知道是如何匹配的。比如输入pan得出结果PanAnNing的时候,高亮的是三个首字母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;
}


    

本文出自 “NashLegend” 博客,请务必保留此出处http://nashlegend.blog.51cto.com/5635342/1566479

智能拨号匹配算法(三)