智能撥號匹配算法(三)

    完整源碼在我的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;
}


    

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章