智能拨号匹配算法
本文出自 “NashLegend” 博客,请务必保留此出处http://nashlegend.blog.51cto.com/5635342/1566108
完整源码在我的github上 https://github.com/NashLegend/QuicKid
智能拨号是指,呃不用解释了,国内拨号软件都带的大家都知道,就是输入姓名拼音的一部分就可快速搜索出联系人的拨号方式。如下图

智能匹配,很容易想到的就是先把九宫格输入键盘上输入的数字转换成可能的拼音组合,然后再用这些可能的拼音与联系人列表中的姓名拼音一一匹配,取匹配度最高的排到最前,但是这有一个问题就是数组对应的可能的拼音组合实在是点儿多,跑一下下面的代码就知道了。如果想智能一些的话还要先剔除一些不可能的拼音组合实在有点麻烦。
| 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 | public static HashMap public static void main(String[] args) { keyMaps = new HashMap keyMaps.put( '0' , new String[ 0 ]); keyMaps.put( '1' , new String[ 0 ]); keyMaps.put( '2' , new String[] { "a" , "b" , "c" }); keyMaps.put( '3' , new String[] { "d" , "e" , "f" }); keyMaps.put( '4' , new String[] { "g" , "h" , "i" }); keyMaps.put( '5' , new String[] { "j" , "k" , "l" }); keyMaps.put( '6' , new String[] { "m" , "n" , "o" }); keyMaps.put( '7' , new String[] { "p" , "q" , "r" , "s" }); keyMaps.put( '8' , new String[] { "t" , "u" , "v" }); keyMaps.put( '9' , new String[] { "w" , "x" , "y" , "z" }); List "726" ); System.out.println(lss.size()); } public static ArrayList ArrayList new ArrayList if (key.length() > 0 ) { if (key.contains( "1" ) || key.contains( "0" )) { list.add(key); } else { int keyLen = key.length(); String[] words; if (keyLen == 1 ) { words = keyMaps.get(key.charAt( 0 )); for ( int i = 0 ; i < words.length; i++) { list.add(words[i]); } } else { ArrayList 0 , key.length() - 1 )); words = keyMaps.get(key.charAt(key.length() - 1 )); for ( int i = 0 ; i < words.length; i++) { for (Iterator .hasNext();) { String sonStr = iterator.next(); list.add(sonStr + words[i]); } } } } } return list; } |
所以可以反过来想,为什么一定要匹配拼音呢。其实我们可以匹配数字,将姓名的拼音转化成九宫格上的数字,比如张三就是94264,726。用输入的数字来匹配这些数字,匹配次数将大大减少。匹配出的数值越高,匹配度越强。
下面先定义一下几个匹配规则
-
完全匹配。用来匹配姓名和电话号码。指输入字符串与联系人内某一匹配项完全匹配。无加减分项。
PanZhiHui-->PanZhiHui -
前置首字母完全匹配。用来匹配姓名。指输入字符串与联系人前几个首字母完全匹配。用来匹配姓名。是前置首字母溢出匹配的特殊形式。 无加分项,减分项为不匹配的首字母个数。
PZH-->PanZhiHui。+2-0
PZ-->PanZhiHui。+2-1 -
前置首字母溢出匹配。用来匹配姓名。指在匹配首字母的情况下,还匹配了某一个或者几个首字母后一段连贯的字符串。加分项为匹配到的首字母个数,减分项为不匹配的首字母个数。
PanZH-->PanZhiHui。+1-0
PZhiHui-->PanZhiHui。+1-0
PZHui-->PanZhiHui。+1-0
PZHu-->PanZhiHui。+1-0
PZhi-->PanZhiHui。+1-1 -
前置段匹配。用来匹配姓名。指一个长度为N的连贯字符与联系人内某一匹配项的前N个字符完全匹配。是前置首字母溢出匹配的特殊形式。
panzh-->PanZhiHui -
后置首字母完全匹配。用来匹配姓名。指输入字符串匹配除第一个首字母以外的其他几个连续首字母。 无加分项,减分项为不匹配的首字母个数。
ZH-->PanZhiHui -
后置首字母溢出匹配。用来匹配姓名。后置首字母完全匹配的情况下,还匹配了某一个或者几个首字母后一段连贯的字符串。加分项为匹配的首字母的数量,减分项为不匹配的首字母个数。
ZHu-->PanZhiHui。+1-0
Zh-->PanZhiRui。+1-1 -
后置段匹配。用来匹配姓名。指有一串长度为N的连贯字符与与联系人内某一匹配项的后半部的一段N个字符串匹配,且此连贯字符的开头位置必须是某一首字母位置。是后置首字母溢出匹配的特殊形式,同时意味着后置首字母溢出匹配事实上不需要加分项,只要保证后置首字母完全匹配的加分项比它大就足够了。
ZhiHui/Zhi/Hui-->PanZhiHui -
后置无头匹配。用来匹配姓名和电话号码。指一串连贯字符在前7种全部未匹配成功的情况下,却被包含在字符串里。加分项为-index,减分项为长度差
hiHui-->PanZhiHui
每个规则都有一个基础数值,以及加分减分项,基本数值不同。取减分项为0.001,加分项为1。至于为什么,在下一段。
查询时匹配以上8种,其他情况不匹配。
匹配的原则是匹配尽可能多的单词。
上面这些名字完全是临时胡编乱造的好么 0.0
排列规则
-
查询出的列表将按匹配度排序,匹配度是一个float(当然double也一样),优先级别从高到低如下(减分项足够小以至于高优先级的匹配度无论如何减分都仍然会高于下面的优先级,因此减分项事实上只用来区别同一优先级中不同联系人匹配程度的高低)。
-
完全匹配,对应的基础数值为4000。
-
前置首字母完全匹配、前置首字母溢出匹配、前置段匹配,这三个其实都可以视作前置首字母溢出匹配,对应的基础数值为3000。(当只有一个字母时,按规则#1算)
-
后置首字母完全匹配、后置首字母溢出匹配、后置段匹配,这三个其实都可以视作后置首字母溢出匹配。对应的基础数值为2000。(当只有一个字母时,按规则#5算)
-
后置无头匹配。对应的基础数值为1000。(可以考虑摒弃此匹配,没有人会这么按,而按键出错的可能性导致无头匹配的可能性又极小,往往不是想要的结果)
输入的一列查询字符串将同时与联系人的名字和电话匹配。对于一个联系人,他的名字可能有多种发音,这时候要取匹配度最高的。对于一个联系人,他可能有两个甚至更多的电话号码,匹配的时候要分别匹配,而不是单独取匹配度最高的。
好了,先写一个类Contact。
添加几个常量,看字面意思应该看得懂。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | static final int Match_Type_Name = 1 ; static final int Match_Type_Phone = 2 ; static final int Level_Complete = 4 ; static final int Level_Fore_Acronym_Overflow = 3 ; static final int Level_Back_Acronym_Overflow = 2 ; static final int Level_Headless = 1 ; static final int Level_None = 0 ; static final float Match_Level_None = 0 ; static final float Match_Level_Headless = 1000 ; static final float Match_Level_Back_Acronym_Overflow = 2000 ; static final float Match_Level_Fore_Acronym_Overflow = 3000 ; static final float Match_Level_Complete = 4000 ; static final float Match_Score_Reward = 1 ; static final float Match_Miss_Punish = 0 .001f; static final int Max_Reward_Times = 999 ; static final int Max_Punish_Times = 999 ; |
再添加下面几条字段
| 1 2 3 | List new ArrayList List new ArrayList List new ArrayList |
fullNameNumber是一个二维的ArrayList,它存放的是将一个联系人打散后数字后的List。比如张三的fullNameString就是{{94264,726}},之所以是二维的,原因是有可能姓名是含有多音字……
fullNameNumberWithoutSpace是联系人姓名的全拼对应的数字,比如张三就是{94264726},之所以是二维的,原因是有可能姓名是含有多音字……
abbreviationNumber是联系人姓名首字母对应的数字,比如张三对应的就是{97},之所以是二维的,原因是有可能姓名是含有多音字……
在设置了Contact的名字后上面三个字段将同时生成数据。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | synchronized public void initPinyin() { String trimmed = name.replaceAll( " " , "" ); //将姓名转化为拼音 String fullNamesString = HanyuPinyinHelper.hanyuPinYinConvert(trimmed, false ); for (Iterator .hasNext();) { String str = iterator.next(); ArrayList new ArrayList String[] pinyins = TextUtil.splitIgnoringEmpty(str, " " ); String abbra = "" ; String fullNameNumberWithoutSpaceString = "" ; for ( int i = 0 ; i < pinyins.length; i++) { String string = pinyins[i]; String res = convertString2Number(string); abbra += res.charAt( 0 ); fullNameNumberWithoutSpaceString += res; lss.add(res); } abbreviationNumber.add(abbra); fullNameNumberWithoutSpace .add(fullNameNumberWithoutSpaceString); fullNameNumber.add(lss); } } |
给它一个match方法。下面调用的xxxMatch()方法都是针对四种不同种类的匹配的对应方法。
| 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 float match(String reg) { // 无法通过第一个字母来判断是不是后置匹配 // 但是可以通过第一个字母判断是不是前置匹配 // match的原则是匹配尽可能多的字符 // 事实上前五种匹配方式都可以使用crossMatch来实现 ScoreAndHits scoreAndHits = new ScoreAndHits(- 1 , 0f, new ArrayList if (!TextUtils.isEmpty(reg)) { boolean checkBack = !canPrematch(reg); if (!checkBack) { if ((scoreAndHits = completeMatch(reg)).score == 0f) { if ((scoreAndHits = foreAcronymOverFlowMatch(reg)).score == 0f) { checkBack = true ; } } } if (checkBack) { if ((scoreAndHits = backAcronymOverFlowMatch(reg)).score == 0f) { scoreAndHits = backHeadlessParagraphMatch(reg); } } } scoreAndHits.reg = reg; matchValue = scoreAndHits; return scoreAndHits.score; } |
所有的xxxMatch返回的结果是一个自定义类ScoreAndHits。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public static class ScoreAndHits { public float score = 0f; public int nameIndex; public ArrayList new ArrayList public int matchType = Match_Type_Name; public int matchLevel = Level_None; public String reg = "" ; public ScoreAndHits( int nameIndex, float score, ArrayList this .nameIndex = nameIndex; this .score = score; this .pairs = pairs; } } |
nameIndex是匹配到了第几个拼音。score是匹配度。pairs是指匹配到的数字在对应的二维list中的位置,用来将来高亮显示匹配的字符用的。如果完全匹配的话,就用不到pairs了。
几个匹配方法的具体内容看下一篇,超过字数限制,写不开了
1.完全匹配
完全匹配很简单了,只要判断string是否相等就行了。这里要判断所有拼音和所有号码。如果拼音已经符合,就不再判断号码。反正是一个人……
| 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 | private ScoreAndHits completeMatch(String reg) { ScoreAndHits scoreAndHits = new ScoreAndHits(- 1 , 0f, new ArrayList for ( int i = 0 ; i < fullNameNumberWithoutSpace.size(); i++) { String str = fullNameNumberWithoutSpace.get(i); if (reg.equals(str)) { scoreAndHits.nameIndex = i; scoreAndHits.score = Match_Level_Complete; scoreAndHits.pairs.add( new PointPair(i, - 1 )); scoreAndHits.matchLevel = Level_Complete; return scoreAndHits; } } for ( int i = 0 ; i < phones.size(); i++) { PhoneStruct phone = phones.get(i); if (reg.equals(phone.phoneNumber)) { scoreAndHits.nameIndex = i; scoreAndHits.score = Match_Level_Complete; scoreAndHits.pairs.add( new PointPair(i, - 1 )); scoreAndHits.matchType = Match_Type_Phone; scoreAndHits.matchLevel = Level_Complete; return scoreAndHits; } } // 走到这里说明没有匹配 return new ScoreAndHits(- 1 , 0f, new ArrayList } |
2.前置首字母溢出匹配。(能不能想个好听的名字
)
| 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 | private ScoreAndHits foreAcronymOverFlowMatch(String reg) { // 因为有可能是多音字,所以这个方法用来对比不同拼音的匹配度,并取最大的那个 ScoreAndHits scoreAndHits = new ScoreAndHits(- 1 , 0f, new ArrayList for ( int i = 0 ; i < fullNameNumber.size(); i++) { ArrayList ScoreAndHits tmpscore = foreAcronymOverFlowMatch(names, reg); if (tmpscore.score > scoreAndHits.score) { scoreAndHits = tmpscore; scoreAndHits.nameIndex = i; } } scoreAndHits.matchLevel = Level_Fore_Acronym_Overflow; return scoreAndHits; } // 在第一个字母确定的情况下,第二个字母有可能有三种情况 // 一、在第一个字母所在单词的邻居位置charAt(x+1); // 二、在第二个单词的首字母处 // 三、以上两种情况皆不符合,不匹配,出局 private ScoreAndHits foreAcronymOverFlowMatch(ArrayList String reg) { // 用来得出某一个拼音的匹配值。 ScoreAndHits scoreAndHits = new ScoreAndHits(- 1 , 0f, new ArrayList if (names.get( 0 ).charAt( 0 ) == reg.charAt( 0 )) { //其实crossWords()方法才是求匹配值的方法,lol OverflowMatchValue value = crossWords(names, reg, 0 , 0 , 0 ); int cross = crossWords(names, reg, 0 , 0 , 0 ).crossed; if (cross > 0 ) { scoreAndHits.score = Match_Level_Fore_Acronym_Overflow + cross * Match_Score_Reward - (names.size() - cross) * Match_Miss_Punish; scoreAndHits.pairs = value.pairs; } } return scoreAndHits; } /** * 返回一串字符能跨越另一串字符的长度,根据上面的匹配规则,要尽可能的多匹配单词。若要保证 * 能匹配最长的长度,只要保证下一个字符开始的一段字符能匹配最长的长度即可,换名话说, * 如果想要让96758匹配最长的字符串,那么只要保证6758能匹配最长的字符串即可, * 然后758,再然后58……。例如,名字叫PanAnNing,输入pan,那么应该匹配三个首字母, * PAN,而不是第一姓的拼音Pan.这是一个递归。 * * * @param names * @param regString * 匹配字符串 * @param listIndex * 匹配到的list的第listIndex个单词 * @param strIndex * 匹配到第listIndex个单词中的第strIndex个字母 * @param regIndex * regchar的匹配位置,比如匹配到了96758的7上,也就是regIndex==2. * @return */ private OverflowMatchValue crossWords(ArrayList String regString, int listIndex, int strIndex, int regIndex) { //在进入此方法时,第listIndex个单词的第strIndex的字母肯定是 // 与regString的第regIndex个字母相等的 OverflowMatchValue result = new OverflowMatchValue( 0 , false ); OverflowMatchValue reser = new OverflowMatchValue( 0 , false ); //返回如果匹配到本单词的下一个字母能得到的匹配值 OverflowMatchValue impul = new OverflowMatchValue( 0 , false ); //返回如果匹配到下一个单词的第一个字母的匹配值 // 仍然以【名字叫PanAnNing,输入pan(其实对比的是数字,这里转化成字母为了方便)】举例 // 假设这时listIndex,strIndex,regIndex都是0,所以现在匹配的是p字母,它毫无疑问对应姓名的第一个P, // 那么下一步应该怎么做呢,由上面所说【保证下一个字符开始的一段字符能匹配最长的长度即可】 // 也就是说,我们输入的pan中的第二个字母a匹配哪个位置将得到最优结果。这个盒子中显然有两种情况。 // 一是匹配姓氏Pan中的a,另一个是匹配名字AnNing中的A。 // reser就表示如果a匹配到Pan中的a最终的匹配值。 // impul就表示如果a匹配到AnNing中的A得到的最终的匹配值。 if (regIndex < regString.length() - 1 ) { //如果还没匹配到最后一个字母,也就是regString还没匹配到最后一个,那么将检测如 //果将regString的下一个字母放到哪里将得到最优结果 char nextChar = regString.charAt(regIndex + 1 ); if (listIndex < names.size() - 1 && nextChar == names.get(listIndex + 1 ).charAt( 0 )) { impul = crossWords(names, regString, listIndex + 1 , 0 , regIndex + 1 ); } if (strIndex < names.get(listIndex).length() - 1 && nextChar == names.get(listIndex).charAt(strIndex + 1 )) { reser = crossWords(names, regString, listIndex, strIndex + 1 , regIndex + 1 ); } //如果上面两个条件都不成立,那么就表示本次匹配失败 } else { result = new OverflowMatchValue((strIndex == 0 ) ? 1 : 0 , true ); result.pairs.add( 0 , new PointPair(listIndex, strIndex)); } if (reser.matched || impul.matched) { //如果其中任意一个方式可以匹配,那么结果最大的那个就是最优结果 if (impul.crossed > reser.crossed) { result = impul; } else { result = reser; } result.matched = true ; result.crossed = ((strIndex == 0 ) ? 1 : 0 ) + Math.max(result.crossed, result.crossed); result.pairs.add( 0 , new PointPair(listIndex, strIndex)); } return result; } static class OverflowMatchValue { public int crossed = 0 ; public boolean matched = false ; public ArrayList new ArrayList public OverflowMatchValue( int c, boolean m) { this .crossed = c; this .matched = m; } } |
3.后置首字母溢出匹配。(能不能想个好听的名字
)
跟前置首字母溢出匹配基本一样,只不过匹配的第一个字母不再是姓的首字母。
| 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 | private ScoreAndHits backAcronymOverFlowMatch(String reg) { //跟上面差不多 ScoreAndHits scoreAndHits = new ScoreAndHits(- 1 , 0f, new ArrayList for ( int i = 0 ; i < fullNameNumber.size(); i++) { ArrayList ScoreAndHits tmp = backAcronymOverFlowMatch(names, reg); if (tmp.score > scoreAndHits.score) { scoreAndHits = tmp; scoreAndHits.nameIndex = i; } } scoreAndHits.matchLevel = Level_Back_Acronym_Overflow; return scoreAndHits; } private ScoreAndHits backAcronymOverFlowMatch(ArrayList String reg) { int score = 0 ; int punish = 0 ; ScoreAndHits scoreAndHits = new ScoreAndHits(- 1 , 0f, new ArrayList // 有可能会调用多次crossWords,取决于名字的长度。这是跟前面的不同 for ( int i = 0 ; i < names.size(); i++) { String string = (String) names.get(i); if (string.charAt( 0 ) == reg.charAt( 0 )) { OverflowMatchValue value = crossWords(names, reg, i, 0 , 0 ); int cross = value.crossed; int lost = names.size() - cross; if (cross > score || cross == score && punish > lost) { scoreAndHits.pairs = value.pairs; score = cross; punish = lost; } } } if (score > 0 ) { scoreAndHits.score = Match_Level_Back_Acronym_Overflow + score * Match_Score_Reward - punish * Match_Miss_Punish; return scoreAndHits; } else { return new ScoreAndHits(- 1 , 0f, new ArrayList } } |
4.后置无头匹配。(难听就难听了,反正就那个意思)
| 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 | private ScoreAndHits backHeadlessParagraphMatch(String reg) { // TODO,如果此人有两个相似的号码,那么就只能匹配出一个来了,这是很显然不对的 int punish = 0 ; ScoreAndHits scoreAndHits = new ScoreAndHits(- 1 , -1f, new ArrayList scoreAndHits.matchLevel = Level_Headless; scoreAndHits.matchType = Match_Type_Phone; // 不匹配姓名 for ( int i = 0 ; i < phones.size(); i++) { PhoneStruct phone = phones.get(i); int sco = phone.phoneNumber.indexOf(reg); if (sco >= 0 ) { int lost = phone.phoneNumber.length() - reg.length(); if (scoreAndHits.score < sco || sco == scoreAndHits.score && punish > lost) { scoreAndHits.score = sco; scoreAndHits.nameIndex = i; punish = lost; } //pairs.add放到判断外面是因为有可能匹配到同一个人的多个手机号码。 scoreAndHits.pairs.add( new PointPair(i, sco)); } } if (scoreAndHits.score >= 0 ) { scoreAndHits.score = Match_Level_Headless - scoreAndHits.score * Match_Score_Reward - punish * Match_Miss_Punish; } return scoreAndHits; } //表示电话号码的一个静态类,将过滤掉开头的+86以及系统可能自动生成的“-”以及其他非数字的字符以便于搜索 public static class PhoneStruct { public String phoneNumber; public int phoneType; public String displayType; public PhoneStruct(String number, int type) { phoneNumber = number.replaceAll( "^\\+86" , "" ).replaceAll( "[\\]+" , "" ); phoneType = type; } } |
到这里已经可以得出输入字符串与联系人的匹配度了,剩下的事情就是调用和显示了,但是这不是本文的重点
有了匹配算法,下面是如何将搜索联系人并显示出来以及高亮显示匹配到的字符串。
1.搜索并显示联系人
显示列表当然是使用ListView,使用自定义的ContactAdapter,ContactAdapter继承BaseAdapter,并实现了Filterable接口,此接口中有一个getFilter方法,返回一个过滤用的类Filter,Filter需要自己实现,我们就是通过这个Filter实现搜索的。
Filter类有两个方法,publishResults和performFiltering方法,其中publishResults运行在UI线程,而performFiltering运行在其他线程,搜索的过程就在performFiltering中执行。
下面是自己实现的Filter类
| 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 | @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 new ArrayList ArrayList new ArrayList if (preLength > 0 && (preLength == queryLength - 1 ) && queryString.startsWith(preQueryString)) { //如果本次搜索的字符串是上次搜索的字符串开头,那么将只在contacts里面搜索(contacts是当前列表的数据集合) baseList = contacts; } else { //过滤所有联系人 baseList = AllContacts; } for (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。
高亮方法如下
| 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 | 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 contact.fullNameNumber .get(contact.matchValue.nameIndex), contact.matchValue.pairs, "#FF0000" ); SpannableStringBuilder builder = new SpannableStringBuilder(str); for (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 ArrayList int k = 0 ; int idx = - 1 ; int crtHead = - 1 ; int crtTail = - 1 ; ArrayList new ArrayList 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; } |
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
