Android之列表索引
其实这个功能是仿苹果的,但是现在大多数Android设备都已经有了这个功能,尤其是在通讯录中最为常见。先来看看今天这个DEMO的效果图(如下图):从图中我们可以看到,屏幕中的主体是一个ListView,右边有一个导航栏,里面放着字母/数字的索引(如图1),用手指点击/移动手指可以改变选中的索引,同时ListView将滚动到选中的索引对应的第一条数据(如图2);如果ListView的数据中没有选中的索引对应的数据,则将ListView滚动到选中索引上面离选中索引最近的有数据的索引对应的第一条数据(如图3)。
通过这个DEMO,我想写两件事:第一,这种样式的ListView怎么做;第二,索引工具怎样实现。下面会一一记录。
一、DEMO中ListView的实现
这种内容复杂、各Item样式不一样的ListView,就是通过在Adapter上做手脚来实现的,这个DEMO也不例外。在这个DEMO中我用的是BaseAdapter,通过ContentView结合ViewHolder实现的。当然,我们还需要自定义一个Item的布局文件,ListView中的每个Item其实都是一个这个布局中的Item。下面上Item的效果图(帖子最后会上代码哒~):
那么问题来了,运行效果中ListView中的Item不是每个都带有上面那一条灰色的啊,这是怎么做到的呢?其实,这是我在Adapter中做了手脚,在Adapter的getView()中判断这一条数据是不是这一索引对应的一系列数据中的第一条,如果不是,则隐藏掉上面的TextView。就是这么简单~
二、索引工具的实现
前面说过,这是一个自定义控件。开始我认为需要为索引栏中的每个索引都建立一个TextView,后来发现原来不需要建立那么多TextView,我们只需要通过自定义控件的onDraw()方法“画”出所有的索引字符。
然后就是和手指的交互了,我们通过重写View的onTouchEvent()方法来获取手指当前的动作,并利用接口回调机制,对手指的动作进行监听。这里也体现出了上一段的一个优点:如果我们用一堆TextView来表示索引字符,那么我们就需要自定义一个ViewGroup,但是这样一来我们就不能——至少很难——达到像现在一样的效果,因为我们肯定要为每个TextView都设置一个onTouchEvent()事件,但多个TextView之间无法达到无缝的衔接,即只有按下手指或抬起手指时可能会有交互,而滑动期间无法达到交互,因为那是两个TextView!因此,我们使用一个单一的View,通过判断当前手指所在的高度来判断当前用户选中的是那个索引,从而就可以写回调事件了。
在这里还有一个问题,我们在最开始介绍到,“如果ListView的数据中没有选中的索引对应的数据,则将ListView滚动到选中索引上面离选中索引最近的有数据的索引对应的第一条数据”。这句话听上去挺复杂,但是我们只需要把上面的效果图图2、图3进行一下比较就显而易见了:比如说,在ListView的数据源中没有以E开头的数据,那么当我们选择字母E为索引是,程序会自动跳到离E最近的、有数据的一个索引——D。当然,如果D中也没有数据,那么可能会跳到C、B、A或者#。看上去很复杂的功能,其实只是一个遍历的问题而已。
三、代码实现
国际惯例,先上程序结构图:
主界面布局文件activity_main.xml中的代码:
1 <?xml version="1.0" encoding="utf-8"?> 2 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 android:layout_width="match_parent" 4 android:layout_height="match_parent" 5 android:background="@android:color/white"> 6 7 <ListView 8 android:id="@+id/find_main_lv_names" 9 android:layout_width="match_parent" 10 android:layout_height="match_parent" 11 android:cacheColorHint="@android:color/transparent" 12 android:divider="@null" /> 13 14 <com.view.SideBar 15 android:id="@+id/find_main_ly_indexes" 16 android:layout_width="25.0dip" 17 android:layout_height="match_parent" 18 android:layout_alignParentRight="true" 19 android:orientation="vertical" /> 20 21 <TextView 22 android:id="@+id/find_main_tv_indexviewer" 23 android:layout_width="wrap_content" 24 android:layout_height="wrap_content" 25 android:layout_centerInParent="true" 26 android:textSize="100.0sp" 27 android:textStyle="bold" /> 28 </RelativeLayout>
主界面MainActivity.java中的代码:
1 package com.activity; 2 3 import android.app.Activity; 4 import android.os.Bundle; 5 import android.widget.ListView; 6 import android.widget.TextView; 7 8 import com.entity.DataModel; 9 import com.tools.MyAdapter; 10 import com.view.SideBar; 11 12 import net.sourceforge.pinyin4j.PinyinHelper; 13 14 import java.util.ArrayList; 15 import java.util.Collections; 16 import java.util.Comparator; 17 import java.util.List; 18 19 public class MainActivity extends Activity { 20 private ListView nameList; // 显示所有数据的ListView 21 private SideBar sideBar; // 右边的索引条 22 private TextView indexViewer; // 位于屏幕中间的当前选中的索引的放大 23 24 private List<DataModel> dataList; // 用来存放所有数据的列表 25 26 @Override 27 protected void onCreate(Bundle savedInstanceState) { 28 super.onCreate(savedInstanceState); 29 setContentView(R.layout.activity_main); 30 initView(); 31 initData(); 32 initEvent(); 33 } 34 35 /** 36 * 初始化控件 37 */ 38 private void initView() { 39 nameList = (ListView) findViewById(R.id.find_main_lv_names); 40 sideBar = (SideBar) findViewById(R.id.find_main_ly_indexes); 41 indexViewer = (TextView) findViewById(R.id.find_main_tv_indexviewer); 42 sideBar.setIndexViewer(indexViewer); 43 } 44 45 /** 46 * 初始化数据 47 */ 48 private void initData() { 49 // 初始化存放所有信息的List并添加所有数据 50 dataList = new ArrayList<DataModel>(); 51 String[] names = getResources().getStringArray(R.array.names); 52 for (String name : names) { 53 dataList.add(new DataModel(name, getPinYinHeadChar(name))); 54 } 55 // 对dataList进行排序:根据每条数据的indexName(字符串)属性进行排序 56 Collections.sort(dataList, new Comparator<DataModel>() { 57 @Override 58 public int compare(DataModel model1, DataModel model2) { 59 return String.valueOf(model1.getIndexName()).compareTo(String.valueOf(model2.getIndexName())); 60 } 61 }); 62 MyAdapter adapter = new MyAdapter(MainActivity.this, dataList); 63 nameList.setAdapter(adapter); 64 } 65 66 /** 67 * 初始化事件 68 */ 69 private void initEvent() { 70 // 回调接口将ListView滑动到选中的索引指向的第一条数据 71 // 如果选中的索引没有对应任何数据,则指向这个索引上面的最近的有数据的索引 72 sideBar.setOnIndexChoiceChangedListener(new SideBar.OnIndexChoiceChangedListener() { 73 @Override 74 public void onIndexChoiceChanged(String s) { 75 char indexName = s.charAt(0); 76 int lastIndex = 0; 77 for (int i = 1; i < dataList.size(); i++) { 78 char currentIndexName = dataList.get(i).getIndexName(); 79 if (currentIndexName >= indexName) { 80 if (currentIndexName == indexName) { 81 nameList.setSelection(i); 82 if (currentIndexName == '#') { 83 nameList.setSelection(0); 84 } 85 } else { 86 nameList.setSelection(lastIndex); 87 } 88 break; 89 } 90 if (dataList.get(i - 1).getIndexName() != currentIndexName) { 91 lastIndex = i; 92 } 93 } 94 } 95 }); 96 } 97 98 /** 99 * 返回中文的首字母(大写字母),如果str不是汉字,则返回 “#” 100 */ 101 public static char getPinYinHeadChar(String str) { 102 char result = str.charAt(0); 103 String[] pinyinArray = PinyinHelper.toHanyuPinyinStringArray(result); 104 if (pinyinArray != null) { 105 result = pinyinArray[0].charAt(0); 106 } 107 if (!(result >= 'A' && result <= 'Z' || result >= 'a' && result <= 'z')) { 108 result = '#'; 109 } 110 return String.valueOf(result).toUpperCase().charAt(0); 111 } 112 }
主界面的ListView中Item的布局文件sideworks_main_namelist_item.xml中的代码:
1 <?xml version="1.0" encoding="utf-8"?> 2 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 android:layout_width="match_parent" 4 android:layout_height="wrap_content" 5 android:orientation="vertical"> 6 7 <TextView 8 android:id="@+id/find_nameitem_index" 9 android:layout_width="match_parent" 10 android:layout_height="wrap_content" 11 android:background="@android:color/darker_gray" 12 android:paddingBottom="5.0dip" 13 android:paddingLeft="10.0dip" 14 android:paddingTop="5.0dip" 15 android:text="@string/app_name" 16 android:textColor="@android:color/black" 17 android:textSize="18.0sp" 18 android:textStyle="bold" /> 19 20 <TextView 21 android:id="@+id/find_nameitem_name" 22 android:layout_width="match_parent" 23 android:layout_height="wrap_content" 24 android:background="@android:color/white" 25 android:paddingBottom="15.0dip" 26 android:paddingLeft="10.0dip" 27 android:paddingTop="15.0dip" 28 android:text="@string/app_name" 29 android:textColor="@color/colorPrimaryDark" 30 android:textSize="20.0sp" /> 31 32 </LinearLayout>
主界面ListView中Item映射到的实体类DataModel.java中的代码:
1 package com.entity; 2 3 public class DataModel { 4 private String name; // 在ListView中显示的数据 5 private char indexName; // 拼音首字母 6 7 public DataModel() { 8 9 } 10 11 public DataModel(String name, char indexName) { 12 this.name = name; 13 this.indexName = indexName; 14 } 15 16 public String getName() { 17 return name; 18 } 19 20 public void setName(String name) { 21 this.name = name; 22 } 23 24 public char getIndexName() { 25 return indexName; 26 } 27 28 public void setIndexName(char indexName) { 29 this.indexName = indexName; 30 } 31 }
主界面中ListView的适配器类MyAdapter.java中的代码:
1 package com.tools; 2 3 import android.content.Context; 4 import android.view.LayoutInflater; 5 import android.view.View; 6 import android.view.ViewGroup; 7 import android.widget.BaseAdapter; 8 import android.widget.TextView; 9 10 import com.activity.R; 11 import com.entity.DataModel; 12 13 import java.util.List; 14 15 public class MyAdapter extends BaseAdapter { 16 private List<DataModel> list; 17 private LayoutInflater inflater; 18 19 public MyAdapter(Context context, List<DataModel> list) { 20 this.list = list; 21 inflater = LayoutInflater.from(context); 22 } 23 24 @Override 25 public int getCount() { 26 return list.size(); 27 } 28 29 @Override 30 public Object getItem(int position) { 31 return list.get(position); 32 } 33 34 @Override 35 public long getItemId(int position) { 36 return position; 37 } 38 39 @Override 40 public View getView(int position, View convertView, ViewGroup parent) { 41 convertView = inflater.inflate(R.layout.sideworks_main_namelist_item, parent, false); 42 TextView indexView = (TextView) convertView.findViewById(R.id.find_nameitem_index); 43 TextView nameView = (TextView) convertView.findViewById(R.id.find_nameitem_name); 44 indexView.setText(String.valueOf(list.get(position).getIndexName())); 45 nameView.setText(list.get(position).getName()); 46 if (position != 0 && list.get(position - 1).getIndexName() == (list.get(position).getIndexName())) { 47 indexView.setVisibility(View.GONE); 48 } 49 return convertView; 50 } 51 }
主界面中的自定义索引栏控件类SideBar.java中的代码:
1 package com.view; 2 3 import android.content.Context; 4 import android.graphics.Canvas; 5 import android.graphics.Color; 6 import android.graphics.Paint; 7 import android.graphics.Typeface; 8 import android.graphics.drawable.ColorDrawable; 9 import android.util.AttributeSet; 10 import android.view.MotionEvent; 11 import android.view.View; 12 import android.widget.TextView; 13 14 import com.activity.R; 15 16 public class SideBar extends View { 17 private String[] indexNames = {"#", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"}; 18 private Paint paint = new Paint(); // onDraw()中用到的绘制工具类对象 19 private TextView indexViewer; // 显示当前选中的索引的放大版 20 private int currentChoice = -1; // 当前选择的字母 21 private OnIndexChoiceChangedListener listener; // 回调接口,用来监听选中索引该表时出发的事件 22 23 public SideBar(Context context) { 24 this(context, null); 25 } 26 27 public SideBar(Context context, AttributeSet attrs) { 28 this(context, attrs, 0); 29 } 30 31 public SideBar(Context context, AttributeSet attrs, int defStyleAttr) { 32 super(context, attrs, defStyleAttr); 33 } 34 35 @Override 36 protected void onDraw(Canvas canvas) { 37 super.onDraw(canvas); 38 // 获取焦点改变背景颜色 39 int totalWidth = getWidth(); // SideLayout控件的高度 40 int totalHeight = getHeight(); // SideLayout控件的宽度 41 int singleHeight = (totalHeight - 5) / indexNames.length; // 每一个字母的高度 42 for (int i = 0; i < indexNames.length; i++) { 43 paint.setColor(Color.rgb(33, 65, 98)); 44 paint.setTypeface(Typeface.DEFAULT_BOLD); 45 paint.setAntiAlias(true); 46 paint.setTextSize(30); 47 // 选中的状态 48 if (i == currentChoice) { 49 paint.setColor(Color.parseColor("#3399ff")); 50 paint.setFakeBoldText(true); 51 } 52 float xPos = totalWidth / 2 - paint.measureText(indexNames[i]) / 2; // x坐标等于中间-字符串宽度的一半 53 float yPos = singleHeight * i + singleHeight; 54 canvas.drawText(indexNames[i], xPos, yPos, paint); 55 paint.reset(); // 重置画笔 56 invalidate(); 57 } 58 } 59 60 @Override // 为自定义控件设置触摸事件 61 public boolean onTouchEvent(MotionEvent event) { 62 final int action = event.getAction(); 63 final float y = event.getY(); // 点钱手指所在的Y坐标 64 final int lastChoice = currentChoice; 65 final OnIndexChoiceChangedListener listener = this.listener; 66 final int c = (int) (y / getHeight() * indexNames.length); // 点击y坐标所占总高度的比例*b数组的长度就等于点击b中的个数 67 switch (action) { 68 case MotionEvent.ACTION_UP: // 抬起手指 69 setBackgroundColor(Color.parseColor("#00000000")); // 背景设置为透明 70 currentChoice = -1; 71 invalidate(); // 实时更新视图绘制 72 if (indexViewer != null) { 73 indexViewer.setVisibility(View.INVISIBLE); // 隐藏放大的索引 74 } 75 break; 76 default: // 按下手指或手指滑动 77 setBackgroundResource(R.drawable.sidebar_background); 78 if (lastChoice != c) { // 选中的索引改变时 79 if (c >= 0 && c < indexNames.length) { 80 if (listener != null) { 81 listener.onIndexChoiceChanged(indexNames[c]); 82 } 83 if (indexViewer != null) { 84 indexViewer.setText(indexNames[c]); 85 indexViewer.setVisibility(View.VISIBLE); 86 } 87 currentChoice = c; 88 invalidate(); 89 } 90 } 91 break; 92 } 93 return true; 94 } 95 96 public void setIndexViewer(TextView indexViewer) { 97 this.indexViewer = indexViewer; 98 } 99 100 public void setOnIndexChoiceChangedListener(OnIndexChoiceChangedListener listener) { 101 this.listener = listener; 102 } 103 104 /** 105 * 回调接口,用来监听选中索引该表时出发的事件 106 */ 107 public interface OnIndexChoiceChangedListener { 108 void onIndexChoiceChanged(String s); 109 } 110 }
点击索引栏时索引栏变化的背景文件sidebar_background.xml的代码:
1 <?xml version="1.0" encoding="utf-8"?> 2 <shape xmlns:android="http://schemas.android.com/apk/res/android" 3 android:shape="rectangle"> 4 <gradient 5 android:angle="90.0" 6 android:endColor="#99C60000" 7 android:startColor="#99C60000" /> 8 9 <corners 10 android:bottomLeftRadius="8dip" 11 android:topLeftRadius="8dip" /> 12 13 </shape>
最后是数据(以string-array的形式写在strings.xml文件中):
1 <resources> 2 <string name="app_name">索引列表</string> 3 4 <string-array name="names"> 5 <item>阿妹</item> 6 <item>阿郎</item> 7 <item>陈奕迅</item> 8 <item>周杰伦</item> 9 <item>曾一鸣</item> 10 <item>成龙</item> 11 <item>王力宏</item> 12 <item>汪峰</item> 13 <item>王菲</item> 14 <item>那英</item> 15 <item>张伟</item> 16 <item>张学友</item> 17 <item>李德华</item> 18 <item>郑源</item> 19 <item>白水水</item> 20 <item>白天不亮</item> 21 <item>陈龙</item> 22 <item>陈丽丽</item> 23 <item>哈林</item> 24 <item>高进</item> 25 <item>高雷</item> 26 <item>阮今天</item> 27 <item>龚琳娜</item> 28 <item>苏醒</item> 29 <item>苏永康</item> 30 <item>陶喆</item> 31 <item>沙宝亮</item> 32 <item>宋冬野</item> 33 <item>宋伟</item> 34 <item>袁成杰</item> 35 <item>戚薇</item> 36 <item>齐大友</item> 37 <item>齐天大圣</item> 38 <item>品冠</item> 39 <item>吴克群</item> 40 <item>BOBO</item> 41 <item>Jobs</item> 42 <item>动力火车</item> 43 <item>伍佰</item> 44 <item>#蔡依林</item> 45 <item>$797835344$</item> 46 <item>Jack</item> 47 <item>~夏先生</item> 48 </string-array> 49 50 </resources>