完整源碼在個人github上 https://github.com/NashLegend/QuicKid java
有了匹配算法,下面是如何將搜索聯繫人並顯示出來以及高亮顯示匹配到的字符串。git
1.搜索並顯示聯繫人github
顯示列表固然是使用ListView,使用自定義的ContactAdapter,ContactAdapter繼承BaseAdapter,並實現了Filterable接口,此接口中有一個getFilter方法,返回一個過濾用的類Filter,Filter須要本身實現,咱們就是經過這個Filter實現搜索的。算法
Filter類有兩個方法,publishResults和performFiltering方法,其中publishResults運行在UI線程,而performFiltering運行在其餘線程,搜索的過程就在performFiltering中執行。less
下面是本身實現的Filter類
ide
@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不對,就直接不顯示這個拼音,由於用戶操做很是之快,他是沒法發現也不必關心這幾十毫秒的顯示不正常的。ui
還有一個線程同步的問題,在notifyDataSetChanged以後,adapter會順序執行getView,可是在getView的時候,setContacts可能又會執行,從而改變了contacts的長度,contacts.get(position)可能會發生越界的問題,所以這時候getView要捕獲這個錯誤,返回一個空view,跟上次同樣,空view存在時間很短,不會有人注意的……spa
搜索某個單詞的時候,使用getFilter.filter(queryString)便可實現搜索。剩下的不用多說,都是普通的adapter和listview的問題。線程
2.高亮顯示匹配的字符串orm
高亮顯示匹配的字符串使用戶知道是如何匹配的。好比輸入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; }