簡介
在android開發的過程中,列表是相當常用的組件,所以美觀簡單易用的列表是必須的。尤其在數據比較多的時候,索引和搜索功能就變得極爲重要。例如android自帶的聯繫人頁面和一些App帶的城市選擇頁面。
如下圖爲某團購網的城市選擇頁面。
但是縱觀網上的大部分分組實現,數據都是諸如以下的形式(摘自某篇博客,或者這裏只是說明基本的原理),分組標籤和數據分開,不夠典型。
public void setData(){
list.add("A");
listTag.add("A");
for(int i=0;i<3;i++){
list.add("阿凡達"+i);
}
list.add("B");
listTag.add("B");
for(int i=0;i<3;i++){
list.add("比特風暴"+i);
}
list.add("C");
listTag.add("C");
for(int i=0;i<30;i++){
list.add("查理風雲"+i);
}
}
關於索引的實現大部分都不太完整,或者沒有處理排序,或者假定是排序好的,對於一般的情況不太實用。本文將提供一種較爲全面的實現。
功能分析
在開始實現之前,首先需要明確要實現的功能,才能進而一步步的實現。主要的功能有以下兩個:
1. 所有的數據按照首字母排序並按首字母分組;
2. 右邊的字母索引(以後也稱爲”尺子”)可以直接索引到相應的分組位置。
我們所要實現的也就是上面的兩個功能。第一個功能需要把分組標籤頁插入到數據中,並設置分組標記,以便在生成列表的時候特殊處理;第二個功能則需要記錄標籤在數據中位置。可以細分爲如下:
1. 漢字轉拼音;
2. 按照首字母排序;
3. 所有的首字母集合;
4. 索引添加到數據集中,並記錄索引在數據集合中的位置。
以上的功能分析中,分組標籤也會添加到數據中,這個添加是動態完成的,不像簡介中硬性添加。
實現思路
1. android提供的實現,HanziToPinyin.Java;
2. Collections.Sort()方法;
3. 遍歷排序好的數據集,找出所有的首字母集合。
4. 可以和3同步進行。
實現
先加一張實現結果截圖:
接下來一步步的實現。
1. 數據BO
首先是數據,爲了簡單,我們假定數據只有一個字段,那就是待排序和分組的字段。其他的均爲輔助字段。
public class TestBo {
/** * 主要字段 */ private StringboStr =null;
/** * bo拼音緩存 */ private StringboPinYin =null; /** * Bo標籤標記 */ private StringboTagFlag =null;
public TestBo() { super(); }
public TestBo(String str) { super(); this.boStr = str; } public String getSortStrPinyin() { returnboPinYin; }
public void setSortStrPinYin(String pinyin) { this.boPinYin = pinyin; }
public void setTag(String tag) { this.boTagFlag = tag; } public String getTag() { returnboTagFlag; } } |
2. 佈局和activity
<?xmlversion="1.0"encoding="utf-8"?> <FrameLayoutxmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" > <!-- 主列表 --> <ListView android:id="@+id/g_base_list" android:layout_width="match_parent" android:layout_height="match_parent" android:visibility="gone"/> <!-- 無數據提示 --> <TextView android:id="@+id/g_base_list_nodata" android:layout_width="fill_parent" android:layout_height="fill_parent" android:visibility="gone" android:gravity="center" android:text="@string/g_nodata" android:textSize="16sp"/> <!-- 加載進度框 --> <com.wbx.testpub.wigdit.ProgressBarWithText android:id="@+id/g_base_progressbar_withtext" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_gravity="center" android:gravity="center"/>
<FrameLayout android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginTop="30dp" android:layout_marginBottom="30dp" android:orientation="vertical" >
<!-- 索引被點擊到的時候放大顯示 --> <TextView android:id="@+id/g_ruler_tag" android:layout_width="100dp" android:layout_height="100dp" android:layout_gravity="center" android:background="@drawable/g_item_bk_normal" android:gravity="center" android:text="A" android:textColor="#FF58BD21" android:textSize="70sp" android:textStyle="bold" /> <!-- 字母索引導航--> <LinearLayout android:id="@+id/g_ruler" android:layout_width="30dp" android:layout_height="match_parent" android:layout_gravity="right" android:paddingLeft="5dp" android:paddingRight="5dp" android:orientation="vertical"/>
</FrameLayout> </FrameLayout> |
在onCreate 方法中初始化,主要做以下工作
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); initView();//初始化view,設置各個View的可見性 initData(); }
private void initView() { noDataView = (TextView) findViewById(R.id.g_base_list_nodata); RulerTag = (TextView) findViewById(R.id.g_ruler_tag); progress = (ProgressBarWithText) findViewById(R.id.g_base_progressbar_withtext); listView = (ListView) findViewById(R.id.g_base_list); ruler = (RulerWidget) findViewById(R.id.g_ruler); progress.setVisibility(View.VISIBLE); RulerTag.setVisibility(View.GONE); noDataView.setVisibility(View.GONE); listView.setVisibility(View.GONE); ruler.setVisibility(View.GONE); }
/** *加載數據,處理數據,並在處理成功後 在界面上進行展示 */ private void initData() { originalData = getBoList();//這裏可以從任何地方獲取數據,然後回調處理結果; doWithData();//處理後數據放在resData; handleRes();//處理後續結果,初始化字母索引導航 } public List<TestBo> getBoList() { List<TestBo> res = new ArrayList<TestBo>(); TestBo a = new TestBo("aa"); TestBo a1 = new TestBo("阿姨"); TestBo b = new TestBo("bb"); TestBo b1 = new TestBo("爸爸"); res.add(b1); res.add(a); res.add(a1); res.add(b); return res; } |
3. 數據處理
數據處理爲原始的數據添加字母分組標籤,並記錄每個標籤在列表中的位置。當然得需要先排序。
生成的標籤也是 TestBo 類型的,且tag字段不爲null,正常的數據 tag字段爲null。
/** * 處理數據,排序,添加標籤,並記錄標籤的位置。 */ private void doWithData() {
resData.addAll(originalData); //首先排序 Collections.sort(resData,new Comparator<TestBo>() { @Override public int compare(TestBo lhs, TestBo rhs) { char firChar = checkAndGetPinyin(lhs); char secChar = checkAndGetPinyin(rhs); if (firChar < secChar) { return -1; } else if (firChar > secChar) { return 1; } else return 0; } });
int size =resData.size(); int i = 0; char nowTag ='\0';
for (i = 0; i < size; i++) { TestBo temp = resData.get(i); char tempTag = checkAndGetPinyin(temp); if(Arrays.binarySearch(letters, tempTag) < 0){ tempTag = '#'; } //生成新的TestBo,作爲標籤,並設置標籤標記,以便數據展示的時候區別 if (nowTag != tempTag) { TestBo tagBO = new TestBo(); tagBO.setTag(tempTag+""); resData.add(i, tagBO); tagLoc.put(tempTag +"", i); i++; size++; nowTag = tempTag; } } tagLoc.put("#", 0);
}
/** * 獲取首字母,並設置拼音緩存 */ private char checkAndGetPinyin(TestBo bo){ String pinyinStr = bo.getSortStrPinyin(); if (pinyinStr==null) { bo.setSortStrPinYin(HanziToPinyin.getPinYin(bo.getBoStr()).toUpperCase()); pinyinStr = bo.getSortStrPinyin(); } if(pinyinStr!=null&&pinyinStr.length()>=1){ return pinyinStr.charAt(0); } return'\0'; } |
4. Adptor
在實現字母索引導航之前先實現ListView的adaptor,主要是其getView方法。其實就是對於處理後的數據resData中的每一個元素,判斷其類型是否爲分組標籤,如果爲標籤的話,則採用標籤佈局,否則採用正常佈局。
代碼如下:
首先是列表項佈局:
<?xmlversion="1.0"encoding="utf-8"?> <LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android" xmlns:imagecontrol="http://schemas.android.com/apk/res-auto" android:layout_width="fill_parent" android:layout_height="wrap_content" android:background="@drawable/g_ruler_list_item_bg" android:gravity="center_vertical" android:orientation="vertical" android:paddingBottom="4dp" android:paddingLeft="10dp" android:paddingRight="20dp" android:paddingTop="4dp">
<TextView android:id="@+id/g_ruler_list_item_tag" android:layout_width="wrap_content" android:layout_height="40dp" android:gravity="center_vertical" android:text="A" android:textColor="#FF58BD21" android:textSize="20sp" /> <!—空的佈局,用來添加非標籤佈局 --!> <LinearLayout android:id="@+id/g_ruler_list_item" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="40dp" android:gravity="center_vertical" >
</LinearLayout>
</LinearLayout> |
然後是adaptor的getView方法,此adaptor基礎BaseAdaptor
@Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder;
if (convertView ==null) { convertView = LayoutInflater.from(mContext).inflate(R.layout.g_ruler_list_item,null); holder = new ViewHolder(); holder.tag = (TextView) convertView.findViewById(R.id.g_ruler_list_item_tag); holder.content = (LinearLayout) convertView.findViewById(R.id.g_ruler_list_item); convertView.setTag(holder); } else { holder = (ViewHolder) convertView.getTag(); } final TestBo bo = getItem(position);
String tag = bo.getTag(); if(tag!=null){//是標籤 holder.tag.setVisibility(View.VISIBLE); holder.content.setVisibility(View.GONE);
holder.tag.setText(tag); }else{//是內容 //首先取自定義視圖,如果沒的話就自定義一個TextView add到content中 holder.content.removeAllViews(); holder.tag.setVisibility(View.GONE); holder.content.setVisibility(View.VISIBLE); View contentView = getContentView(position, bo); if(contentView==null){ TextView textV = new TextView(mContext); textV.setText(bo.getBoStr()); textV.setTextColor(Color.BLACK); textV.setGravity(Gravity.CENTER_VERTICAL); textV.setTextSize(16); contentView = textV; } holder.content.addView(contentView, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); }
return convertView; }
/** * @date 2014-9-3 * @Description:需要的時候覆蓋此方法,提供自己的自定義非標籤視圖 * @param * @return View */ public View getContentView(int position,TestBo bo){ return null; }
static class ViewHolder{ TextView tag; LinearLayout content; }
|
5. 字母索引導航
基本思路就是在導航條中添加所有的字母標籤,然後監聽其onTouch事件,根據觸摸的不同的位置,計算所觸摸的字母,根據tagLoc讓列表滾動到相應的位置代碼如下:
/** *處理結束後的數據 */ private void handleRes() { initRuler();//初始化字母索引 showView();//展示 }
/** * 初始化字母索引 */ private void initRuler() { int color = getResources().getColor(R.color.g_ruler_letter_color);//綠色 LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); params.gravity = Gravity.CENTER_HORIZONTAL; ruler.bringToFront();
params.weight = 1; for (int i = 0; i <indexStr.length; i++) { final TextView tv =new TextView(this); tv.setLayoutParams(params); tv.setTextColor(color); tv.setGravity(Gravity.CENTER); tv.setText(RulerUtil.indexStr[i]); ruler.addView(tv); } ruler.setOnTouchListener(new OnTouchListener() {
@Override public boolean onTouch(View v, MotionEvent event) { int height = v.getHeight(); float pos = event.getY(); int sectionPosition = (int) ((pos / height) / (1f /indexStr.length)); if (sectionPosition < 0) { sectionPosition = 0; } else if (sectionPosition > indexStr.length-1) { sectionPosition = indexStr.length-1; }
switch (event.getAction()) { case MotionEvent.ACTION_DOWN: RulerTag.setVisibility(View.VISIBLE); RulerTag.setText(RulerUtil.indexStr[sectionPosition]); listView.setSelection(getPosition(sectionPosition)); ruler.setBackgroundResource(R.color.g_ruler_selected);//灰色 break; case MotionEvent.ACTION_MOVE: RulerTag.setText(RulerUtil.indexStr[sectionPosition]); listView.setSelection(getPosition(sectionPosition)); break; default:
RulerTag.setVisibility(View.GONE);
ruler.setBackgroundResource(R.color.g_blank); } return true; } }); }
private void showView() { progress.setVisibility(View.GONE); listView.setVisibility(View.VISIBLE); ruler.setVisibility(View.VISIBLE); }
|
完了麼
寫到這裏,功能已經全部實現。貌似很完美,但是完了麼?假如要在不同的地方使用類似的列表。上面的代碼通用性很低。不得不復制和修改,並且上述代碼具有代碼侵入,想實現分鐘列表其對象TestBo 有很多無用的字段,如
private StringboPinYin =null;
/**
* Bo標籤標記
*/
private StringboTagFlag =null;
這些字段是爲了提高排序效率和區分分組標籤而加進去的,正常的數據不應該有這些字段。有辦法去掉麼。
還有優化的空間,把其寫成一個通用的控件。
先到這裏了。