先看效果图
这里写图片描述
这是比较常见的效果了吧
列表根据首字符的拼音字母来排序,且可以通过侧边栏的字母索引来进行定位
实现这样一个效果并不难,只要自定义一个索引View,然后引入一个可以对汉字进行拼音解析的jar包——pinyin4j-2.5.0即可
首先,先来定义侧边栏控件View,只要直接画出来即可
字母选中项会变为红色,且滑动时背景会变色,此时SideBar并不包含居中的提示文本
public class SideBar extends View {
private Paint paint = new Paint();
private int choose = -1;
private boolean showBackground;
public static String[] letters = {"#", "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 OnChooseLetterChangedListener onChooseLetterChangedListener;
public SideBar(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public SideBar(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SideBar(Context context) {
super(context);
}
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (showBackground) {
canvas.drawColor(Color.parseColor("#D9D9D9"));
}
int height = getHeight();
int width = getWidth();
//平均每个字母占的高度
int singleHeight = height / letters.length;
for (int i = 0; i < letters.length; i++) {
paint.setColor(Color.BLACK);
paint.setAntiAlias(true);
paint.setTextSize(25);
if (i == choose) {
paint.setColor(Color.parseColor("#FF2828"));
paint.setFakeBoldText(true);
}
float x = width / 2 - paint.measureText(letters[i]) / 2;
float y = singleHeight * i + singleHeight;
canvas.drawText(letters[i], x, y, paint);
paint.reset();
}
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
int action = event.getAction();
float y = event.getY();
int oldChoose = choose;
int c = (int) (y / getHeight() * letters.length);
switch (action) {
case MotionEvent.ACTION_DOWN:
showBackground = true;
if (oldChoose != c && onChooseLetterChangedListener != null) {
if (c > -1 && c < letters.length) {
onChooseLetterChangedListener.onChooseLetter(letters[c]);
choose = c;
invalidate();
}
}
break;
case MotionEvent.ACTION_MOVE:
if (oldChoose != c && onChooseLetterChangedListener != null) {
if (c > -1 && c < letters.length) {
onChooseLetterChangedListener.onChooseLetter(letters[c]);
choose = c;
invalidate();
}
}
break;
case MotionEvent.ACTION_UP:
showBackground = false;
choose = -1;
if (onChooseLetterChangedListener != null) {
onChooseLetterChangedListener.onNoChooseLetter();
}
invalidate();
break;
}
return true;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return super.onTouchEvent(event);
}
public void setOnTouchingLetterChangedListener(OnChooseLetterChangedListener onChooseLetterChangedListener) {
this.onChooseLetterChangedListener = onChooseLetterChangedListener;
}
public interface OnChooseLetterChangedListener {
void onChooseLetter(String s);
void onNoChooseLetter();
}
}
SideBar只是画出了侧边栏索引条而已,不包含居中的提示文本,这个在另一个布局添加即可
public class HintSideBar extends RelativeLayout implements SideBar.OnChooseLetterChangedListener {
private TextView tv_hint;
private SideBar.OnChooseLetterChangedListener onChooseLetterChangedListener;
public HintSideBar(Context context, AttributeSet attrs) {
super(context, attrs);
LayoutInflater.from(context).inflate(R.layout.view_hint_side_bar, this);
initView();
}
private void initView() {
SideBar sideBar = (SideBar) findViewById(R.id.sideBar);
tv_hint = (TextView) findViewById(R.id.tv_hint);
sideBar.setOnTouchingLetterChangedListener(this);
}
@Override
public void onChooseLetter(String s) {
tv_hint.setText(s);
tv_hint.setVisibility(VISIBLE);
if (onChooseLetterChangedListener != null) {
onChooseLetterChangedListener.onChooseLetter(s);
}
}
@Override
public void onNoChooseLetter() {
tv_hint.setVisibility(INVISIBLE);
if (onChooseLetterChangedListener != null) {
onChooseLetterChangedListener.onNoChooseLetter();
}
}
public void setOnChooseLetterChangedListener(SideBar.OnChooseLetterChangedListener onChooseLetterChangedListener) {
this.onChooseLetterChangedListener = onChooseLetterChangedListener;
}
}
HintSideBar通过回调接口来更新居中TextView的文本内容和可见性
使用到的布局
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.czy.demo.SideBar
android:id="@+id/sideBar"
android:layout_width="30dp"
android:layout_height="match_parent"
android:layout_alignParentRight="true" />
<TextView
android:id="@+id/tv_hint"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_centerInParent="true"
android:background="#4b0e0e0e"
android:gravity="center"
android:textColor="#ffffff"
android:textSize="30sp"
android:visibility="invisible" />
</RelativeLayout>
此时就完成了索引View的绘制,不过定位功能还需要再通过回调接口来完成
引入jar包后,先来设定一个工具类,包含一个可以解析字符串的方法,返回值为首字符对应的拼音首字母或者为包含一个空格的char类型数据
public class Utils {
/**
* 如果字符串的首字符为汉字,则返回该汉字的拼音大写首字母
* 如果字符串的首字符为字母,也转化为大写字母返回
* 其他情况均返回' '
*
* @param str 字符串
* @return 首字母
*/
public static char getHeadChar(String str) {
if (str != null && str.trim().length() != 0) {
char[] strChar = str.toCharArray();
char headChar = strChar[0];
//如果是大写字母则直接返回
if (Character.isUpperCase(headChar)) {
return headChar;
} else if (Character.isLowerCase(headChar)) {
return Character.toUpperCase(headChar);
}
// 汉语拼音格式输出类
HanyuPinyinOutputFormat hanYuPinOutputFormat = new HanyuPinyinOutputFormat();
hanYuPinOutputFormat.setCaseType(UPPERCASE);
hanYuPinOutputFormat.setToneType(WITHOUT_TONE);
if (String.valueOf(headChar).matches("[\\u4E00-\\u9FA5]+")) {
try {
String[] stringArray = PinyinHelper.toHanyuPinyinStringArray(headChar, hanYuPinOutputFormat);
if (stringArray != null && stringArray[0] != null) {
return stringArray[0].charAt(0);
}
} catch (BadHanyuPinyinOutputFormatCombination e) {
return ' ';
}
}
}
return ' ';
}
}
然后再定义一个实体类,包含用户名,电话,用户名首字符的拼音首字母等三个属性
需要实现Comparable 接口,用于排序
public class User implements Comparable {
private String userName;
private String phone;
private char headLetter;
public User(String userName, String phone) {
this.userName = userName;
this.phone = phone;
headLetter = Utils.getHeadChar(userName);
}
public String getUserName() {
return userName;
}
public String getPhone() {
return phone;
}
public char getHeadLetter() {
return headLetter;
}
@Override
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (object == null || getClass() != object.getClass()) {
return false;
}
User that = (User) object;
return getUserName().equals(that.getUserName()) && getPhone().equals(that.getPhone());
}
@Override
public int compareTo(Object object) {
if (object instanceof User) {
User that = (User) object;
if (getHeadLetter() == ' ') {
if (that.getHeadLetter() == ' ') {
return 0;
}
return -1;
}
if (that.getHeadLetter() == ' ') {
return 1;
} else if (that.getHeadLetter() > getHeadLetter()) {
return -1;
} else if (that.getHeadLetter() == getHeadLetter()) {
return 0;
}
return 1;
} else {
throw new ClassCastException();
}
}
}
主布局文件如下
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/rv_userList"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.czy.demo.HintSideBar
android:id="@+id/hintSideBar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="right" />
</FrameLayout>
联系人列表使用的是RecyclerView,还需要定义一个Adapter
public class UserAdapter extends RecyclerView.Adapter<UserAdapter.UserHolder> {
private List<User> userList;
private LayoutInflater inflater;
public UserAdapter(Context context) {
inflater = LayoutInflater.from(context);
userList = new ArrayList<>();
}
@Override
public UserHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = inflater.inflate(R.layout.item_user, parent, false);
return new UserHolder(view);
}
@Override
public void onBindViewHolder(UserHolder holder, int position) {
holder.tv_userName.setText(userList.get(position).getUserName());
holder.tv_phone.setText(userList.get(position).getPhone());
}
public void setData(List<User> userList) {
this.userList.clear();
this.userList = userList;
}
public int getFirstPositionByChar(char sign) {
if (sign == '#') {
return 0;
}
for (int i = 0; i < userList.size(); i++) {
if (userList.get(i).getHeadLetter() == sign) {
return i;
}
}
return -1;
}
@Override
public int getItemCount() {
return userList.size();
}
class UserHolder extends RecyclerView.ViewHolder {
public TextView tv_userName;
public TextView tv_phone;
public UserHolder(View itemView) {
super(itemView);
tv_userName = (TextView) itemView.findViewById(R.id.tv_userName);
tv_phone = (TextView) itemView.findViewById(R.id.tv_phone);
}
}
}
以下方法用于获取联系人列表中第一个首字符为sign的item的位置
public int getFirstPositionByChar(char sign)
主Activity代码如下
public class MainActivity extends AppCompatActivity implements SideBar.OnChooseLetterChangedListener {
private List<User> userList;
private UserAdapter adapter;
private RecyclerView rv_userList;
private LinearLayoutManager manager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
supportRequestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_main);
HintSideBar hintSideBar = (HintSideBar) findViewById(R.id.hintSideBar);
rv_userList = (RecyclerView) findViewById(R.id.rv_userList);
hintSideBar.setOnChooseLetterChangedListener(this);
manager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
rv_userList.setLayoutManager(manager);
userList = new ArrayList<>();
adapter = new UserAdapter(this);
initData();
adapter.setData(userList);
rv_userList.setAdapter(adapter);
}
@Override
public void onChooseLetter(String s) {
int i = adapter.getFirstPositionByChar(s.charAt(0));
if (i == -1) {
return;
}
manager.scrollToPositionWithOffset(i, 0);
}
@Override
public void onNoChooseLetter() {
}
}
initData()
用于向Adapter填充数据
public void initData() {
User user1 = new User("陈", "12345678");
User user2 = new User("赵", "12345678");
...
userList.add(user1);
userList.add(user2);
...
Collections.sort(userList);
adapter.notifyDataSetChanged();
}
这样,整个效果就都完成了
代码我已上传到GitHub——HintSideBar