最近项目需要仿照手机通讯录效果做一个城市筛选,虽然 以前做过,用SortListView,具体用法参照老夏的博客,这次在github上发现ContactListView,实现同样的效果,地址:https://github.com/kk-java/ChineseCityList,该项目demo简单运行后发现了个小bug,英文字母筛选和侧边栏筛选无效,侧栏字母间距混乱,具体原因稍后细说,这里先上效果图:

android 筛选控件 安卓手机筛选_ci


该开源项目主要涉及中英文转换检索、scroller onTouch ondraw绘制 事件分发拦截、排序这几块的知识点,主要类:ContactListView、 HanziToPinyin3、 ContactListAdapter、IndexScroller。先看IndexScroller类:


public IndexScroller(Context context, ListView lv)
	{
		mDensity = context.getResources().getDisplayMetrics().density;
		mScaledDensity = context.getResources().getDisplayMetrics().scaledDensity;
		mListView = lv;


		setAdapter(mListView.getAdapter());


		mIndexbarWidth = 30 * mDensity;
		mIndexbarMargin = 0 * mDensity;
		mPreviewPadding = 5 * mDensity;


		// customization of paint colors
		// outer container
		indexbarContainerPaint.setAntiAlias(true);


		// letter in section
		indexPaint.setAntiAlias(true);
		indexPaint.setTextSize(12 * mScaledDensity);
	}





这里mIndexbarWidth mIndexbarMargin mPreviewPadding分别是设置右侧字母索引控件的pading magin和宽度值


public void drawIndexBarContainer(Canvas canvas)
	{
		indexbarContainerPaint.setColor(indexbarContainerBgColor);
		indexbarContainerPaint.setAlpha((int) (64 * mAlphaRate)); // opacity
		canvas.drawRoundRect(mIndexbarRect, 5 * mDensity, 5 * mDensity,
				indexbarContainerPaint);
	}




该方法是用于绘制圆角容器,存放字母A-Z,绘制的背景颜色值:indexbarContainerBgColor 容器背景色




public void drawCurrentSection(Canvas canvas)
	{
		if (mCurrentSection >= 0)
		{


			// Log.i("IndexScroller", "current section: " + mCurrentSection);


			// Preview is shown when mCurrentSection is set
			// mCurrentSection is the letter that is being pressed
			// this will draw the big preview text on top of the listview
			Paint previewPaint = new Paint();
			previewPaint.setColor(Color.parseColor("#ff0000"));
			previewPaint.setAlpha(96);
			previewPaint.setAntiAlias(true);
			previewPaint.setShadowLayer(3, 0, 0, Color.argb(64, 0, 0, 0));


			Paint previewTextPaint = new Paint();
			previewTextPaint.setColor(Color.WHITE);
			previewTextPaint.setAntiAlias(true);
			previewTextPaint.setTextSize(50 * mScaledDensity);


			float previewTextWidth = previewTextPaint
					.measureText(mSections[mCurrentSection]);
			float previewSize = 2 * mPreviewPadding
					+ previewTextPaint.descent() - previewTextPaint.ascent();
			RectF previewRect = new RectF((mListViewWidth - previewSize) / 2,
					(mListViewHeight - previewSize) / 2,
					(mListViewWidth - previewSize) / 2 + previewSize,
					(mListViewHeight - previewSize) / 2 + previewSize);


			canvas.drawRoundRect(previewRect, 5 * mDensity, 5 * mDensity,
					previewPaint);
			canvas.drawText(mSections[mCurrentSection], previewRect.left
					+ (previewSize - previewTextWidth) / 2 - 1, previewRect.top
					+ mPreviewPadding - previewTextPaint.ascent() + 1,
					previewTextPaint);
		}
	}



这里是绘制弹出字母的视图,修改弹出字母UI>> 弹出字母的圆角背景色:previewPaint.setColor(Color.parseColor("#ff0000"));


弹出文字颜色和大小:previewTextPaint.setColor(Color.parseColor("#d3d4d7"));previewTextPaint.setTextSize(50 * mScaledDensity);indexPaint.setColor(indexPaintColor);



public ContactListView(Context context, AttributeSet attrs, int defStyle)
	{
		super(context, attrs, defStyle);
	}


	public IndexScroller getScroller()
	{
		return mScroller;
	}
       .......

       // override this if necessary for custom scroller
	public void createScroller()
	{
		mScroller = new IndexScroller(getContext(), this);
		mScroller.setAutoHide(autoHide);
		mScroller.setShowIndexContainer(true);


		if (autoHide)
			mScroller.hide();
		else
			mScroller.show();
	}




      得到自定义的IndexScroller,




      

@Override
	public void draw(Canvas canvas)
	{
		super.draw(canvas);


		// Overlay index bar
		if (!inSearchMode) // dun draw the scroller if not in search mode
		{
			if (mScroller != null)
				mScroller.draw(canvas);
		}


	}


	@Override
	public boolean onTouchEvent(MotionEvent ev)
	{
		// Intercept ListView's touch event
		if (mScroller != null && mScroller.onTouchEvent(ev))
			return true;


		if (mGestureDetector == null)
		{
			mGestureDetector = new GestureDetector(getContext(),
					new GestureDetector.SimpleOnGestureListener()
					{


						@Override
						public boolean onFling(MotionEvent e1, MotionEvent e2,
								float velocityX, float velocityY)
						{
							// If fling happens, index bar shows
							mScroller.show();
							return super.onFling(e1, e2, velocityX, velocityY);
						}


					});
		}
		mGestureDetector.onTouchEvent(ev);


		return super.onTouchEvent(ev);
	}


	@Override
	public boolean onInterceptTouchEvent(MotionEvent ev)
	{
		return true;
	}



事件拦截派发交给scroller 处理



@Override
	public void setFastScrollEnabled(boolean enabled)
	{
		mIsFastScrollEnabled = enabled;
		if (mIsFastScrollEnabled)
		{
			if (mScroller == null)
			{
				createScroller();
			}
		} else
		{
			if (mScroller != null)
			{
				mScroller.hide();
				mScroller = null;
			}
		}
	}



设置是否允许快速滚动定位


@Override
	protected void onSizeChanged(int w, int h, int oldw, int oldh)
	{
		super.onSizeChanged(w, h, oldw, oldh);
		if (mScroller != null)
			mScroller.onSizeChanged(w, h, oldw, oldh);
	}



大小改变的时候,事件派发给IndexScroller 的onSizeChange重新测量size


   ContactItemComparator.java

@Override
	public int compare(ContactItemInterface lhs, ContactItemInterface rhs)
	{
		if (lhs.getItemForIndex() == null || rhs.getItemForIndex() == null)
			return -1;


		return (lhs.getItemForIndex().compareTo(rhs.getItemForIndex()));


	}


比较排序,这里ContactItemInterface 定义空方法,实体类实现该接口,通过定义的接口相应函数就可以直接获取相应的值,实体类和接口如下实例:


import com.liucanwen.citylist.widget.ContactItemInterface;


public class CityContact implements ContactItemInterface
{
	private String fullName;
	private String cityId;
	private String cityName;
	
	public CityContact(String cityId,
			String cityName,String fullName) {
		super();
		this.fullName = fullName;
		this.cityId = cityId;
		this.cityName = cityName;
	}
	@Override
	public String getItemForIndex()
	{
		return fullName;
	}


	@Override
	public String getDisplayInfo()
	{
		return cityName;
	}
	
	@Override
	public String getItemCityName() {
		// TODO Auto-generated method stub
		return cityName;
	}




	@Override
	public String getItemCityId() {
		// TODO Auto-generated method stub
		return cityId;
	}
	
	@Override
	public void setItemCityId(String cityId) {
		// TODO Auto-generated method stub
		setCityId(cityId);
	}


	public String getFullName()
	{
		return fullName;
	}

      ......
	
}


public interface ContactItemInterface
{
	// 根据该字段来排序
	public String getItemForIndex();


	// 该字段用来显示出来
	public String getDisplayInfo();
	
	public String getItemCityName();
	
	public String getItemCityId();
	
	public void setItemCityId(String cityId);
}<strong style="font-size: 14.4444446563721px; font-family: 'Comic Sans MS'; background-color: rgb(255, 255, 255);"><em> </em></strong>

根据不同的SearchMode选择不同的适配数据源,从而准确判断当前Item选中项

listview.setOnItemClickListener(new AdapterView.OnItemClickListener()
{
@SuppressWarnings("rawtypes")
@Override
public void onItemClick(AdapterView parent, View v, int position,
long id)
{
List<ContactItemInterface> searchList = inSearchMode ? filterList
: contactList;


Toast.makeText(context_,
searchList.get(position).getDisplayInfo(),
Toast.LENGTH_SHORT).show();
}
});



EditText绑定监听事件:addTextChangedListener(TextWatcher..);监听EditText值得变化进行过滤筛选数据源



@Override
public void afterTextChanged(Editable s)
{
searchString = searchBox.getText().toString().trim().toUpperCase();


if (curSearchTask != null
&& curSearchTask.getStatus() != AsyncTask.Status.FINISHED)
{
try
{
curSearchTask.cancel(true);
} catch (Exception e)
{
Log.i(TAG, "Fail to cancel running search task");
}


}
curSearchTask = new SearchListTask();
curSearchTask.execute(searchString); 
}

private class SearchListTask extends AsyncTask<String, Void, String>
{


@Override
protected String doInBackground(String... params)
{
filterList.clear();


String keyword = params[0];


inSearchMode = (keyword.length() > 0);


if (inSearchMode)
{
// get all the items matching this
for (ContactItemInterface item : contactList)
{
CityItem contact = (CityItem) item;


boolean isPinyin = contact.getFullName().toUpperCase()
.indexOf(keyword) > -1;
boolean isChinese = contact.getNickName().indexOf(keyword) > -1;


if (isPinyin || isChinese)
{
filterList.add(item);
}


}


}
return null;
}


protected void onPostExecute(String result)
{


synchronized (searchLock)
{


if (inSearchMode)
{


CityAdapter adapter = new CityAdapter(context_,
R.layout.city_item, filterList);
adapter.setInSearchMode(true);
listview.setInSearchMode(true);
listview.setAdapter(adapter);
} else
{
CityAdapter adapter = new CityAdapter(context_,
R.layout.city_item, contactList);
adapter.setInSearchMode(false);
listview.setInSearchMode(false);
listview.setAdapter(adapter);
}
}


}
}



项目的界面风格配置参照colors.xml



<?xml version="1.0" encoding="utf-8"?>
<resources>


    <color name="background">#FF000000</color>
    <color name="black">#FF000000</color>
    <color name="white">#FFFFFFFF</color>
    <color name="yellow">#FFFFE400</color>
    <color name="pink_red">#F63E2A</color>
    <color name="red">#FF0000</color>
    <color name="bulegray">#f0f0f0</color>
    <color name="gray">#828282</color>
    <color name="graylight">#cccccc</color>
    <color name="orange">#FF6600</color>
    <color name="orange_light">#fdaa0b</color>
    <color name="gold">#ffd700</color>
    <color name="transparent">#00000000</color>
    <color name="semitransparent">#90000000</color>
    <color name="nearly_hard">#97000000</color>
    <color name="font_focus">#D7F679</color>
    <color name="gold_player">#F7F466</color>
    <color name="whitetran">#87FFFFFF</color>
    <color name="divider_line">#FF575757</color>
    
    <color name="listview_item_normal">#eef0ed</color>
    <color name="listview_item_pressed">#d4d4d4</color>
    
    <color name="message_checked">#90918f</color>
    <color name="message_no_check">#323232</color>
    
    <color name="stick_color_bg">#e2e2e2</color>


</resources>

Entity对象数据模型改成自己需要的城市数据对象需要修改CityItem 、ContactItemInterface,添加字段同时interface 提供一个函数供获取id值,这里就不上代码了,下面来说说之前提到的bug问题,github上demo无法筛选原因如下:


public static List<ContactItemInterface> getSampleContactList()
{
List<ContactItemInterface> list = new ArrayList<ContactItemInterface>();

try
{
JSONObject jo1 = new JSONObject(cityJson);

JSONArray ja1 = jo1.getJSONArray("cities");

for(int i = 0; i < ja1.length(); i++)
{
String cityName = ja1.getString(i);

list.add(new CityItem(cityName, PinYin.getPinYin(cityName)));
}
} 
catch (JSONException e)
{
e.printStackTrace();
}


return list;
}

这里跟踪发现返回的PinYin.getPinYin(cityName)返回是空的,继续跟进..


// 汉字返回拼音,字母原样返回,都转换为小写
public static String getPinYin(String input)
{
ArrayList<Token> tokens = HanziToPinyin3.getInstance().get(input);
StringBuilder sb = new StringBuilder();
if (tokens != null && tokens.size() > 0)
{
for (Token token : tokens)
{
if (Token.PINYIN == token.type)
{
sb.append(token.target);
} else
{
sb.append(token.source);
}
}
}
return sb.toString().toLowerCase();
}

这里tokens返回也是空的,继续跟进..


public static HanziToPinyin3 getInstance() {
        synchronized (HanziToPinyin3.class) {
            if (sInstance != null) {
                return sInstance;
            }
            // Check if zh_CN collation data is available
            final Locale locale[] = Collator.getAvailableLocales();
            for (int i = 0; i < locale.length; i++) {
                if (locale[i].equals(Locale.CHINA)) {
                    // Do self validation just once.
                    if (DEBUG) {
                        Log.d(TAG, "Self validation. Result: " + doSelfValidation());
                    }
                    sInstance = new HanziToPinyin3(true);
                    return sInstance;
                }
            }
            Log.w(TAG, "There is no Chinese collator, HanziToPinyin is disabled");
            sInstance = new HanziToPinyin3(false);
            return sInstance;
        }
    }

  public ArrayList<Token> get(final String input) {
        ArrayList<Token> tokens = new ArrayList<Token>();
        if (mHasChinaCollator || TextUtils.isEmpty(input)) {
            // return empty tokens.
            return tokens;
        }
        final int inputLength = input.length();
        final StringBuilder sb = new StringBuilder();
        int tokenType = Token.LATIN;
        // Go through the input, create a new token when
        // a. Token type changed
        // b. Get the Pinyin of current charater.
        // c. current character is space.
        for (int i = 0; i < inputLength; i++) {
            final char character = input.charAt(i);
            if (character == ' ') {
                if (sb.length() > 0) {
                    addToken(sb, tokens, tokenType);
                }
            } else if (character < 256) {
                if (tokenType != Token.LATIN && sb.length() > 0) {
                    addToken(sb, tokens, tokenType);
                }
                tokenType = Token.LATIN;
                sb.append(character);
            } else {
                Token t = getToken(character);
                if (t.type == Token.PINYIN) {
                    if (sb.length() > 0) {
                        addToken(sb, tokens, tokenType);
                    }
                    tokens.add(t);
                    tokenType = Token.PINYIN;
                } else {
                    if (tokenType != t.type && sb.length() > 0) {
                        addToken(sb, tokens, tokenType);
                    }
                    tokenType = t.type;
                    sb.append(character);
                }
            }
        }
        if (sb.length() > 0) {
            addToken(sb, tokens, tokenType);
        }
        return tokens;
    }



具体原因找到了: sInstance = new HanziToPinyin3(false); mHasChinaCollator 赋值为false,在get方法时,

if (!mHasChinaCollator || TextUtils.isEmpty(input)) {
            // return empty tokens.
            return tokens;
        }‘



改后:

if (mHasChinaCollator || TextUtils.isEmpty(input)) {
            // return empty tokens.
            return tokens;
        }

修改:再次发现Bug,个人粗心只在某款5.1的手机系统上运行测试了,其实源代码这里是没问题的,但是个别手机的兼容性问题,Demo里面未修改,参照如下:这里的修改如下:

if (!mHasChinaCollator || TextUtils.isEmpty(input)) {            // return empty tokens.
            return tokens;
        }‘
public static HanziToPinyin3 getInstance() {
        synchronized (HanziToPinyin3.class) {
            if (sInstance != null) {
                return sInstance;
            }
            // Check if zh_CN collation data is available
            final Locale locale[] = Collator.getAvailableLocales();
            for (int i = 0; i < locale.length; i++) {
                if (locale[i].equals(Locale.CHINA)) {
                    // Do self validation just once.
                    if (DEBUG) {
                        Log.d(TAG, "Self validation. Result: " + doSelfValidation());
                    }
                    sInstance = new HanziToPinyin3(true);
                    return sInstance;
                }
            }
            Log.w(TAG, "There is no Chinese collator, HanziToPinyin is disabled");
            sInstance = new HanziToPinyin3(<span style="color:#cc0000;">true</span>);
            return sInstance;
        }
    }

下面是右侧字母混乱问题:引起原因是系统键盘弹出,自定义控件的size改变,从而重新计算高度,最简单的做法修改系统输入法的弹出方式:


<activity
            android:name="com.liucanwen.citylist.MainActivity"
            android:label="@string/app_name"
            android:windowSoftInputMode="adjustPan">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />


                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>




代码修改方式如下:IndexScroller


public void onSizeChanged(int w, int h, int oldw, int oldh)
	{
		if(isFirst){
			mListViewWidth = w;
			mListViewHeight = h;
			isFirst=false;
			mIndexbarRect = new RectF(w - mIndexbarMargin - mIndexbarWidth,
					mIndexbarMargin, w - mIndexbarMargin, h - mIndexbarMargin);
		}
	}




之测量一次,如果应用遇到强制不可以转屏可以这样做,不过个人不建议这样做,(可能)重计算字母A-Z索引选中项重绘会有个距离问题,不能精确计算位置