最近项目需要仿照手机通讯录效果做一个城市筛选,虽然 以前做过,用SortListView,具体用法参照老夏的博客,这次在github上发现ContactListView,实现同样的效果,地址:https://github.com/kk-java/ChineseCityList,该项目demo简单运行后发现了个小bug,英文字母筛选和侧边栏筛选无效,侧栏字母间距混乱,具体原因稍后细说,这里先上效果图:
该开源项目主要涉及中英文转换检索、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索引选中项重绘会有个距离问题,不能精确计算位置