思路分析
1、自定义View实现字母导航栏
2、ListView实现联系人列表
3、字母导航栏滑动事件处理
4、字母导航栏与中间字母的联动
5、字母导航栏与ListView的联动
效果展示
实现步骤
1、先看主布局,方便后面代码的说明
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"><EditTextandroid:layout_width="match_parent"android:layout_height="wrap_content"android:background="@drawable/search_border"android:drawableLeft="@android:drawable/ic_menu_search"android:padding="8dp" /><RelativeLayoutandroid:layout_width="match_parent"android:layout_height="match_parent"><ListViewandroid:id="@+id/lv"android:layout_width="match_parent"android:layout_height="match_parent"android:divider="@null" /><TextViewandroid:id="@+id/tv"android:layout_width="50dp"android:layout_height="50dp"android:layout_centerInParent="true"android:background="#888888"android:gravity="center"android:textColor="#000000"android:textSize="18dp"android:visibility="gone" /><com.handsome.tulin.View.NavViewandroid:id="@+id/nv"android:layout_width="20dp"android:layout_height="match_parent"android:layout_alignParentRight="true"android:layout_margin="16dp" /></RelativeLayout></LinearLayout>
2、分析自定义字母导航栏
1.我们在使用的时候把宽设置为20dp,高设置为填充父控件,所以这里获取的宽度为20dp
2.通过循环,画出竖直的字母,每画一次得重新设置一下颜色,因为我们需要一个选中的字母颜色和默认不一样
public class NavView extends View {private Paint textPaint = new Paint();private String[] s = new String[]{"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", "#"};//鼠标点击、滑动时选择的字母private int choose = -1;//中间的文本private TextView tv;public NavView(Context context, AttributeSet attrs) {super(context, attrs);}public NavView(Context context) {super(context);}public NavView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);}private void initPaint() {textPaint.setTextSize(20);textPaint.setAntiAlias(true);textPaint.setColor(Color.BLACK);}@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);//画字母drawText(canvas);}/*** 画字母** @param canvas*/private void drawText(Canvas canvas) {//获取View的宽高int width = getWidth();int height = getHeight();//获取每个字母的高度int singleHeight = height / s.length;//画字母for (int i = 0; i < s.length; i++) {//画笔默认颜色initPaint();//高亮字母颜色if (choose == i) {textPaint.setColor(Color.RED);}//计算每个字母的坐标float x = (width - textPaint.measureText(s[i])) / 2;float y = (i + 1) * singleHeight;canvas.drawText(s[i], x, y, textPaint);//重置颜色textPaint.reset();}}}
3、ListView实现联系人列表
1.在主Activity中,定义一个数据数组,使用工具类获取数组的第一个字母,使用Collections根据第一个字母进行排序,由于工具类有点长,就不贴出来了。
2.创建一个ListView子布局,创建一个Adapter进行填充。
主布局
public class MainActivity extends AppCompatActivity {private TextView tv;private ListView lv;private NavView nv;private List<User> list;private UserAdapter adapter;private String[] name = new String[]{"潘粤明", "戴军", "薛之谦", "蓝雨", "任泉", "张杰", "秦俊杰","陈坤", "田亮", "夏雨", "保剑锋", "陆毅", "乔振宇", "吉杰", "郭敬明", "巫迪文", "欢子", "井柏然","左小祖咒", "段奕宏", "毛宁", "樊凡", "汤潮", "山野", "陈龙", "侯勇", "俞思远", "冯绍峰", "崔健","杜淳", "张翰", "彭坦", "柏栩栩", "蒲巴甲", "凌潇肃", "毛方圆", "武艺", "耿乐", "钱泳辰"};@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);initView();initData();}private void initView() {tv = (TextView) findViewById(R.id.tv);lv = (ListView) findViewById(R.id.lv);nv = (NavView) findViewById(R.id.nv);nv.setTextView(tv);}private void initData() {//初始化数据list = new ArrayList<>();for (int i = 0; i < name.length; i++) {list.add(new User(name[i], CharacterUtils.getFirstSpell(name[i]).toUpperCase()));}//将拼音排序Collections.sort(list, new Comparator<User>() {@Overridepublic int compare(User lhs, User rhs) {return lhs.getFirstCharacter().compareTo(rhs.getFirstCharacter());}});//填充ListViewadapter = new UserAdapter(this, list);lv.setAdapter(adapter);}}
ListView子布局
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"android:background="#ffffff"android:orientation="vertical"><TextViewandroid:id="@+id/tv_firstCharacter"android:layout_width="match_parent"android:layout_height="wrap_content"android:background="#DBDBDA"android:padding="8dp"android:text="A"android:textColor="#000000"android:textSize="14dp" /><TextViewandroid:id="@+id/tv_name"android:layout_width="match_parent"android:layout_height="wrap_content"android:background="#ffffff"android:padding="8dp"android:text="张栋梁"android:textColor="#2196F3"android:textSize="14dp" /></LinearLayout>
Adapter
public class UserAdapter extends BaseAdapter {private List<User> list;private User user;private LayoutInflater mInflater;private Context context;public UserAdapter(Context context, List<User> list) {this.list = list;mInflater = LayoutInflater.from(context);this.context = context;}@Overridepublic int getCount() {return list.size();}@Overridepublic Object getItem(int position) {return list.get(position);}@Overridepublic long getItemId(int position) {return position;}@Overridepublic View getView(int position, View convertView, ViewGroup parent) {if (convertView == null) {convertView = mInflater.inflate(R.layout.adapter_user, null);}ViewHolder holder = getViewHolder(convertView);user = list.get(position);if (position == 0) {//第一个数据要显示字母和姓名holder.tv_firstCharacter.setVisibility(View.VISIBLE);holder.tv_firstCharacter.setText(user.getFirstCharacter());holder.tv_name.setText(user.getUsername());} else {//其他数据判断是否为同个字母,这里使用Ascii码比较大小if (CharacterUtils.getCnAscii(list.get(position - 1).getFirstCharacter().charAt(0)) <CharacterUtils.getCnAscii(user.getFirstCharacter().charAt(0))) {//后面字母的值大于前面字母的值,需要显示字母holder.tv_firstCharacter.setVisibility(View.VISIBLE);holder.tv_firstCharacter.setText(user.getFirstCharacter());holder.tv_name.setText(user.getUsername());} else {//后面字母的值等于前面字母的值,不显示字母holder.tv_firstCharacter.setVisibility(View.GONE);holder.tv_name.setText(user.getUsername());}}return convertView;}/*** 获得控件管理对象** @param view* @return*/private ViewHolder getViewHolder(View view) {ViewHolder holder = (ViewHolder) view.getTag();if (holder == null) {holder = new ViewHolder(view);view.setTag(holder);}return holder;}/*** 控件管理类*/private class ViewHolder {private TextView tv_firstCharacter, tv_name;ViewHolder(View view) {tv_firstCharacter = (TextView) view.findViewById(R.id.tv_firstCharacter);tv_name = (TextView) view.findViewById(R.id.tv_name);}}/*** 通过字符查找位置** @param s* @return*/public int getSelectPosition(String s) {for (int i = 0; i < getCount(); i++) {String firChar = list.get(i).getFirstCharacter();if (firChar.equals(s)) {return i;}}return -1;}}
4、字母导航栏滑动事件处理、字母导航栏与中间字母的联动
1.在自定义View中重写dispatchTouchEvent处理滑动事件,最后返回true。
2.在主Activity传进来一个TextView,在我们滑动的时候设置Text,松开的时候消失Text。设置Text的时候需要计算Text的位置,并且滑过多的话会出现数组越界的问题,所以我们在里面处理数组越界问题。
3.最后,提供一个接口,记录我们滑到的字母,为了后面可以和ListView联动。
@Overridepublic boolean dispatchTouchEvent(MotionEvent event) {//计算选中字母int index = (int) (event.getY() / getHeight() * s.length);//防止脚标越界if (index >= s.length) {index = s.length - 1;} else if (index < 0) {index = 0;}switch (event.getAction()) {case MotionEvent.ACTION_DOWN:case MotionEvent.ACTION_MOVE:setBackgroundColor(Color.GRAY);//选中字母高亮choose = index;//出现中间文字tv.setVisibility(VISIBLE);tv.setText(s[choose]);//调用ListView连动接口if (listener != null) {listener.touchCharacterListener(s[choose]);}//重绘invalidate();break;default:setBackgroundColor(Color.TRANSPARENT);//取消选中字母高亮choose = -1;//隐藏中间文字tv.setVisibility(GONE);//重绘invalidate();break;}return true;}public onTouchCharacterListener listener;public interface onTouchCharacterListener {void touchCharacterListener(String s);}public void setListener(onTouchCharacterListener listener) {this.listener = listener;}/*** 传进来一个TextView** @param tv*/public void setTextView(TextView tv) {this.tv = tv;}
5、字母导航栏和ListView的联动
1.我们已经通过接口传递过去了一个选择的字母,和在adapter写好了根据字母查询position的方法,这个时候只要主Activity对自定义View设置监听,判断即可。
//ListView连动接口nv.setListener(new NavView.onTouchCharacterListener() {@Overridepublic void touchCharacterListener(String s) {int position = adapter.getSelectPosition(s);if (position != -1) {lv.setSelection(position);}}});
源码下载:建议使用Import Module