Android通用索引栏实现
Posted 冷不冷
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android通用索引栏实现相关的知识,希望对你有一定的参考价值。
偶尔看到之前写过的代码,感觉好多东西几乎在很多项目中都要用到,虽然每个项目的需求和设计都不同,不过实现的效果都是一样的,可能只是数据格式和一些颜色等的细微差距.但是有的时候因为一个小改变,就要去重复的修改代码,麻烦不说,也容易导致新的问题和BUG.
就拿忽然想到的索引栏来说,几乎写过的项目中都用到了,比如城市选择、联系人等等.这些地方全都需要用到索引栏,但是用法都是一样的.翻看了几处之前写过的代码,发现每次用到索引栏,都要重新去写方法来处理数据或者对数据的索引进行提取这些,做法也都大同小异.于是乎,尝试着重构一下这部分,也方便之后的使用.
先看一下效果图:
实现
索引栏的实现,网上有很多例子,也比较简单,就不做过多解释.因为在不同项目中可能涉及到索引栏字体颜色、大小不同等问题,所以把之前用到的代码做一下修改,提取出一些自定义属性,方便修改,就不必每次都去代码中修改,也避免影响到其他人的使用.直接看一下代码,在attr中定义一些自定义属性,如下:
attr:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--SideBar相关-->
<!--普通时的颜色-->
<attr name="normalColor" format="color"/>
<!--选中时的颜色-->
<attr name="chooseColor" format="color"/>
<!--普通时的背景图-->
<attr name="normalBackground" format="reference"/>
<!--选中时的背景图-->
<attr name="chooseBackground" format="reference"/>
<!--索引栏文字大小-->
<attr name="sideTextSize" format="dimension"/>
<declare-styleable name="SideBar">
<attr name="normalColor"/>
<attr name="chooseColor"/>
<attr name="normalBackground"/>
<attr name="chooseBackground"/>
<attr name="sideTextSize"/>
</declare-styleable>
</resources>
把颜色文字大小等属性提取出来方便修改.然后看一下SideBar
SideBar:
package com.example.junweiliu.commindexdemo.widget;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.View;
import android.widget.TextView;
import com.example.junweiliu.commindexdemo.R;
/**
* Created by junweiliu on 16/11/24.
*/
public class SideBar extends View
/**
* 点击回调
*/
private OnTouchingLetterChangedListener onTouchingLetterChangedListener;
/**
* 26字母
*/
public static String[] letterStrs = "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 Paint paint = new Paint();
/**
* 显示的TextView
*/
private TextView mTextDialog;
/**
* 普通时的颜色
*/
private int normalColor;
/**
* 选中的颜色
*/
private int chooseColor;
/**
* 普通时的背景
*/
private Drawable normalBackground;
/**
* 选中时的背景
*/
private Drawable chooseBackground;
/**
* 文字大小
*/
private float textSize;
/**
* 边框
*/
private Rect mRect;
public SideBar(Context context, AttributeSet attrs)
this(context, attrs, 0);
public SideBar(Context context, AttributeSet attrs, int defStyle)
super(context, attrs, defStyle);
// 获取自定义属性
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.SideBar);
normalColor = ta.getColor(R.styleable.SideBar_normalColor, Color.GRAY);
chooseColor = ta.getColor(R.styleable.SideBar_chooseColor, Color.RED);
normalBackground = ta.getDrawable(R.styleable.SideBar_normalBackground);
chooseBackground = ta.getDrawable(R.styleable.SideBar_chooseBackground);
textSize = ta.getDimension(R.styleable.SideBar_sideTextSize, TypedValue
.applyDimension(TypedValue.COMPLEX_UNIT_SP, 13,
getResources().getDisplayMetrics()));
ta.recycle();
init();
/**
* 为SideBar设置显示字母的TextView
*
* @param mTextDialog
*/
public void setTextView(TextView mTextDialog)
this.mTextDialog = mTextDialog;
/**
* 设置
*
* @param letter
*/
public void setLetter(String[] letter)
this.letterStrs = letter;
invalidate();
requestLayout();
/**
* 初始化参数
*/
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
private void init()
paint.setColor(normalColor);
paint.setTypeface(Typeface.DEFAULT_BOLD);
paint.setAntiAlias(true);
paint.setTextSize(textSize);
// 获取单个绘制的rect,用于获取单个绘制项的高度
mRect = new Rect();
paint.getTextBounds("A", 0, "A".length(), mRect);
/**
* 绘制
*
* @param canvas
*/
protected void onDraw(Canvas canvas)
super.onDraw(canvas);
// 获取焦点改变背景颜色.
int height = getHeight() - getPaddingTop() - getPaddingBottom();// 获取对应高度
int width = getWidth(); // 获取对应宽度
int singleHeight = height / letterStrs.length;// 获取每一个字母的高度
for (int i = 0; i < letterStrs.length; i++)
// 选中的状态
if (i == choose)
paint.setColor(chooseColor);
paint.setFakeBoldText(true);
// x坐标等于中间-字符串宽度的一半.
float xPos = width / 2 - paint.measureText(letterStrs[i]) / 2;
float yPos = singleHeight * i + singleHeight;
canvas.drawText(letterStrs[i], xPos, yPos, paint);
paint.reset();// 重置画笔
init();
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
@Override
public boolean dispatchTouchEvent(MotionEvent event)
final int action = event.getAction();
// 点击的y坐标
final float y = event.getY();
final int oldChoose = choose;
final OnTouchingLetterChangedListener listener = onTouchingLetterChangedListener;
// 获取当前点击的字母位置,点击位置的y坐标比上总的高度相当于点击的位置比上全部位置(c / b.length = y / getHeight())
final int currChoose = (int) (y / getHeight() * letterStrs.length);
switch (action)
case MotionEvent.ACTION_UP:
// 重置背景色
if (null != normalBackground)
setBackground(normalBackground);
else
setBackgroundColor(Color.argb(0, 0, 0, 0));
// 抬起时置为-1
choose = -1;
invalidate();
if (mTextDialog != null)
mTextDialog.setVisibility(View.INVISIBLE);
break;
default:
// 设置背景色
if (null != chooseBackground)
setBackground(chooseBackground);
if (oldChoose != currChoose)
if (currChoose >= 0 && currChoose < letterStrs.length)
if (null != listener)
listener.onTouchingLetterChanged(letterStrs[currChoose]);
if (null != mTextDialog)
mTextDialog.setText(letterStrs[currChoose]);
mTextDialog.setVisibility(View.VISIBLE);
// 设置选中的位置为当前位置
choose = currChoose;
invalidate();
break;
return true;
/**
* 向外公开的方法
*
* @param onTouchingLetterChangedListener
*/
public void setOnTouchingLetterChangedListener(
OnTouchingLetterChangedListener onTouchingLetterChangedListener)
this.onTouchingLetterChangedListener = onTouchingLetterChangedListener;
/**
* 回调接口
*
* @author coder
*/
public interface OnTouchingLetterChangedListener
void onTouchingLetterChanged(String s);
/**
* 测量
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
// 当高度为自适应时,高度为字母高度*字母数量*2 即间隔为单位高度
int wrapHeight = letterStrs.length * (mRect.height() * 2);
// 当宽度为自适应使,宽度为字母宽度*2
int warpWidth = mRect.width() * 2;
setMeasuredDimension((widthMode == MeasureSpec.EXACTLY) ? sizeWidth : warpWidth
, (heightMode == MeasureSpec.EXACTLY) ? sizeHeight
//wrap_content时的高度
: wrapHeight);
很简单,只是提取出来了一些自定义属性,没什么可说的,接下来分析一下如何让索引栏变得通用,先来想一下索引栏一般的写法.首先拿到一个数据源,然后对这个数据源进行处理,从数据源中提取出首字母当做索引(当然数据源中可能已经含有首字母或者索引等字段),有了索引之后,再在适配器中进行判断来控制是否显示索引标题(我的做法是判断第一次出现当前索引的数据源位置和当前位置是否相同,如果相同则显示索引标题),处理完索引标题的显示和隐藏,最后就是跟索引栏进行绑定(实现索引栏的回调方法并做相关处理).大体步骤就是这样,接下来就是找一下处理不同的地方,比对了一下,发现问题基本都是出现在数据格式不同上,有的数据的索引字段可能叫Letter,有的可能叫LetterName,这就导致了每次对这些数据进行处理时,都要重新写方法或者修改方法,使得这些方法不共用.那怎么解决一下这个问题呢,最开始想到的是写一个抽象类,然后用一个抽象方法getLetterName()来约束索引.每个需要用到索引栏的Bean都去继承这个抽象类,重写这个抽象方法,从而达到统一约束索引值的效果,也就解决了索引值字段不同的问题,这样就可以用一个公共的方法来处理不同的数据源.后来又考虑了一下,这个地方其实用接口会更加合适一点,接口灵活性更大,而且也是面向接口编程的一种体现.分析了这么多,来具体代码实现一下,提出一个接口,之后所有需要用到索引的数据Bean去实现这个接口中的getLetterName()方法并且重写这个方法来返回索引值即可.
接口SideBase:
package com.example.junweiliu.commindexdemo.bean;
/**
* Created by junweiliu on 16/11/21.
*/
public interface SideBase
String getLetterName();
然后数据Bean去实现这个接口,例如比较常见的CityBean:
package com.example.junweiliu.commindexdemo.bean;
public class CityBean implements SideBase
/**
* 城市名
*/
private String cityName;
/**
* 首字母
*/
private String cityHeader;
/**
* 城市信息
*/
private String cityMes;
public CityBean(String cityName, String cityHeader, String cityMes)
this.cityName = cityName;
this.cityHeader = cityHeader;
this.cityMes = cityMes;
public String getCityName()
return cityName;
public void setCityName(String cityName)
this.cityName = cityName;
public String getCityHeader()
return cityHeader;
public void setCityHeader(String cityHeader)
this.cityHeader = cityHeader;
public String getCityMes()
return cityMes;
public void setCityMes(String cityMes)
this.cityMes = cityMes;
/**
* 获取索引
*
* @return
*/
@Override
public String getLetterName()
return cityHeader;
在getLetterName()方法中去返回索引值.
接下来就可以去写一些公共的处理方法.比如
/**
* 根据当前选中的项获取其第一次出现该项首字母的位置
*
* @param position 当前选中的位置
* @param datas 数据源
* @return
*/
public static int getPositionForSection(int position, List<? extends SideBase> datas)
// 当前选中的项
SideBase sideBase = datas.get(position);
for (int i = 0; i < datas.size(); i++)
String firstStr = datas.get(i).getLetterName().toUpperCase();
// 返回第一次出现该项首字母的位置
if (firstStr.equals(sideBase.getLetterName()))
return i;
return -1;
因为使用的接口,这里就可以用通配符?的方式来对数据进行处理,只关心和处理getLetterName()方法即可.还可以做其他处理:
/**
* 获取所选中的索引在列表中的位置
*
* @param list
* @param letter
* @return
*/
public static int getLetterPosition(List<? extends SideBase> list, String letter)
int position = -1;
if (list != null && !list.isEmpty() && !"".equals(letter))
for (int i = 0; i < list.size(); i++)
SideBase bean = list.get(i);
if (bean.getLetterName().equals(letter))
position = i;
break;
return position;
/**
* 筛选出数据源中所包含的全部索引值
*
* @param list
* @return
*/
public static String[] getLetters(List<? extends SideBase> list)
List<String> letters = new ArrayList<>();
if (list != null && !list.isEmpty())
for (int i = 0; i < list.size(); i++)
if (!letters.contains(list.get(i).getLetterName()))
letters.add(list.get(i).getLetterName());
return (String[]) letters.toArray(new String[letters.size()]);
通过分析和重构之后,这些之前不能公用的方法就变得通用起来,很方便,之后用起来也会特别简单、舒心.
完整代码
公共处理类
CommUtil:
package com.example.junweiliu.commindexdemo.util;
import com.example.junweiliu.commindexdemo.bean.SideBase;
import java.util.ArrayList;
import java.util.List;
/**
* Created by junweiliu on 16/11/24.
*/
public class CommUtil
/**
* 根据当前选中的项获取其第一次出现该项首字母的位置
*
* @param position 当前选中的位置
* @param datas 数据源
* @return
*/
public static int getPositionForSection(int position, List<? extends SideBase> datas)
// 当前选中的项
SideBase sideBase = datas.get(position);
for (int i = 0; i < datas.size(); i++)
String firstStr = datas.get(i).getLetterName().toUpperCase();
// 返回第一次出现该项首字母的位置
if (firstStr.equals(sideBase.getLetterName()))
return i;
return -1;
/**
* 获取所选中的索引在列表中的位置
*
* @param list
* @param letter
* @return
*/
public static int getLetterPosition(List<? extends SideBase> list, String letter)
int position = -1;
if (list != null && !list.isEmpty() && !"".equals(letter))
for (int i = 0; i < list.size(); i++)
SideBase bean = list.get(i);
if (bean.getLetterName().equals(letter))
position = i;
break;
return position;
/**
* 筛选出数据源中所包含的全部索引值
*
* @param list
* @return
*/
public static String[] getLetters(List<? extends SideBase> list)
List<String> letters = new ArrayList<>();
if (list != null && !list.isEmpty())
for (int i = 0; i < list.size(); i++)
if (!letters.contains(list.get(i).getLetterName()))
letters.add(list.get(i).getLetterName());
return (String[]) letters.toArray(new String[letters.size()]);
适配器CityAdapter:
package com.example.junweiliu.commindexdemo.adapter;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;
import com.example.junweiliu.commindexdemo.R;
import com.example.junweiliu.commindexdemo.bean.CityBean;
import com.example.junweiliu.commindexdemo.util.CommUtil;
import java.util.List;
/**
* Created by junweiliu on 16/11/24.
*/
public class CityAdapter extends BaseAdapter
/**
* 上下文
*/
private Context context;
/**
* 布局加载器
*/
private LayoutInflater mInflater;
/**
* 数据源
*/
private List<CityBean> cityBeanList;
/**
* 构造方法
*
* @param context
* @param cityBeanList
*/
public CityAdapter(Context context, List<CityBean> cityBeanList)
this.context = context;
this.cityBeanList = cityBeanList;
@Override
public int getCount()
return cityBeanList.size();
@Override
public Object getItem(int i)
return cityBeanList.get(i);
@Override
public long getItemId(int i)
return i;
@Override
public View getView(int position, View convertView, ViewGroup viewGroup)
ViewHolder viewHolder = null;
CityBean bean = cityBeanList.get(position);
if (convertView == null)
convertView = mInflater.from(context).inflate(R.layout.item_city, null);
viewHolder = new ViewHolder();
viewHolder.headerTv = (TextView) convertView.findViewById(R.id.tv_item_citys_header);
viewHolder.contentTv = (TextView) convertView.findViewById(R.id.tv_item_citys_context);
convertView.setTag(viewHolder);
else
viewHolder = (ViewHolder) convertView.getTag();
// 如果当前位置为第一次出现该类首字母的位置,则显示headerTv
if (position == CommUtil.getPositionForSection(position, cityBeanList))
viewHolder.contentTv.setVisibility(View.VISIBLE);
viewHolder.headerTv.setVisibility(View.VISIBLE);
viewHolder.headerTv.setText(bean.getLetterName());
else
viewHolder.headerTv.setVisibility(View.GONE);
viewHolder.contentTv.setVisibility(View.VISIBLE);
viewHolder.contentTv.setText(bean.getCityName());
return convertView;
/**
* vh
*/
class ViewHolder
TextView headerTv;
TextView contentTv;
MainActivity:
package com.example.junweiliu.commindexdemo;
import android.app.Activity;
import android.os.Bundle;
import android.widget.ListView;
import android.widget.TextView;
import com.example.junweiliu.commindexdemo.adapter.CityAdapter;
import com.example.junweiliu.commindexdemo.bean.CityBean;
import com.example.junweiliu.commindexdemo.util.CommUtil;
import com.example.junweiliu.commindexdemo.widget.SideBar;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends Activity
/**
* 城市列表数据
*/
private List<CityBean> cityBeanList = new ArrayList<>();
/**
* 城市lv
*/
private ListView cityList;
/**
* 索引栏
*/
private SideBar mSideBar;
/**
* 显示的tv
*/
private TextView mShowTv;
/**
* 适配器
*/
private CityAdapter mCityAdapter;
@Override
protected void onCreate(Bundle savedInstanceState)
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initData();
initView();
/**
* 初始化数据
*/
private void initData()
CityBean bean1 = new CityBean("安徽", "A", "安徽故事");
CityBean bean2 = new CityBean("安徽1", "A", "安徽1故事");
CityBean bean3 = new CityBean("安徽2", "A", "安徽2故事");
CityBean bean4 = new CityBean("北京", "B", "北京故事");
CityBean bean5 = new CityBean("北京1", "B", "北京1故事");
CityBean bean6 = new CityBean("北京2", "B", "北京2故事");
CityBean bean7 = new CityBean("重庆", "C", "重庆故事");
CityBean bean8 = new CityBean("重庆1", "C", "重庆1故事");
CityBean bean9 = new CityBean("重庆2", "C", "重庆2故事");
CityBean bean10 = new CityBean("贵州", "G", "贵州故事");
CityBean bean11 = new CityBean("贵州1", "G", "贵州2故事");
CityBean bean12 = new CityBean("贵州2", "G", "贵州3故事");
CityBean bean13 = new CityBean("天津", "T", "天津故事");
CityBean bean14 = new CityBean("天津1", "T", "天津1故事");
CityBean bean15 = new CityBean("天津2", "T", "天津2故事");
cityBeanList.add(bean1);
cityBeanList.add(bean2);
cityBeanList.add(bean3);
cityBeanList.add(bean1);
cityBeanList.add(bean2);
cityBeanList.add(bean3);
cityBeanList.add(bean4);
cityBeanList.add(bean5);
cityBeanList.add(bean6);
cityBeanList.add(bean4);
cityBeanList.add(bean5);
cityBeanList.add(bean6);
cityBeanList.add(bean7);
cityBeanList.add(bean8);
cityBeanList.add(bean9);
cityBeanList.add(bean7);
cityBeanList.add(bean8);
cityBeanList.add(bean9);
cityBeanList.add(bean10);
cityBeanList.add(bean11);
cityBeanList.add(bean12);
cityBeanList.add(bean10);
cityBeanList.add(bean11);
cityBeanList.add(bean12);
cityBeanList.add(bean13);
cityBeanList.add(bean14);
cityBeanList.add(bean15);
cityBeanList.add(bean13);
cityBeanList.add(bean14);
cityBeanList.add(bean15);
/**
* 初始化控件
*/
private void initView()
cityList = (ListView) findViewById(R.id.lv_city);
mSideBar = (SideBar) findViewById(R.id.sb_city);
mShowTv = (TextView) findViewById(R.id.tv_city_show);
mCityAdapter = new CityAdapter(MainActivity.this, cityBeanList);
cityList.setAdapter(mCityAdapter);
// 设置需要显示的索引栏内容
mSideBar.setLetter(CommUtil.getLetters(cityBeanList));
// 设置需要显示的提示框
mSideBar.setTextView(mShowTv);
mSideBar.setOnTouchingLetterChangedListener(new SideBar.OnTouchingLetterChangedListener()
@Override
public void onTouchingLetterChanged(String s)
int position = CommUtil.getLetterPosition(cityBeanList, s);
if (position != -1)
cityList.setSelection(position);
);
布局文件activity_main:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.junweiliu.commindexdemo.MainActivity">
<!--城市列表-->
<ListView
android:id="@+id/lv_city"
android:layout_width="match_parent"
android:layout_height="match_parent">
</ListView>
<!--索引栏-->
<com.example.junweiliu.commindexdemo.widget.SideBar
android:id="@+id/sb_city"
android:layout_width="wrap_content"
android:layout_height="200dp"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
app:sideTextSize="13sp"/>
<!--显示的字母-->
<TextView
android:id="@+id/tv_city_show"
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_centerInParent="true"
android:background="#3F51B5"
android:gravity="center"
android:textColor="#ffffff"
android:textSize="25sp"
android:visibility="gone"
/>
</RelativeLayout>
适配器布局item_city:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<!-- 索引头 -->
<TextView
android:id="@+id/tv_item_citys_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#E3E3E3"
android:padding="15dp"
android:text="A"
android:textColor="#666666"
android:textSize="14sp"
android:visibility="gone"/>
<!-- 内容 -->
<TextView
android:id="@+id/tv_item_citys_context"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#ffffff"
android:padding="15dp"
android:textColor="#000000"
android:textSize="14sp"
android:visibility="gone"/>
</LinearLayout>
源码下载
以上是关于Android通用索引栏实现的主要内容,如果未能解决你的问题,请参考以下文章