Android造*---联系人快速索引
联系人快速索引,可以根据右边索引导航来定位具体拼音首字母的数据,下面是效果图:
实现主要使用三个View:
1、ListView:负责展示联系人数据
2、右边的索引IndexView(自定义View):负责处理用户点击或滑动事件,根据事件坐标值定位相应的字母索引,并通知索引事件监听者
3、正中间的当前索引拼音首字母提示View
IndexView实现原理:
1、根据控件宽度和高度计算出每个字母所占的区域大小
2、通过Paint.getTextBounds()计算出每个字母本身的宽高
3、根据计算出的控件宽高,计算出每个字母需要绘制的左下角坐标(用于文字绘制)
4、计算出每个字母有效的点击/触摸区域坐标(用Rect保存,后面可以通过Rect.contains()方法来判断某个坐标是否在该区域内)
5、被点击/触目到的字母区域用另外一个Paint来绘制通过不同画笔颜色来区分
6、通过回调接口通知事件监听者索引的变化
【注意】要注意获取控件宽高的时机,这里是在onMeasure()方法被调用时获取。
LisetView的定位原理:
1、数据需根据拼音进行排序
2、当IndexView索引发生变化,需要在回调函数里将所有数据的首字母拼音与回调的拼音进行比较,从而获取到具体的数据索引
3、通过ListView.setSelection(int index)方法来定位到具体数据索引的位置
索引拼音首字母提示View:
1、当IndexView索引发生变化,在回调函数里设置当前字母数据并控制View的显示和隐藏
下面开始贴代码,首先是布局文件:
contacts.xml
<?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">
<ListView
android:id="@+id/lv_contacts"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<TextView
android:id="@+id/tv_abc"
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_centerInParent="true"
android:textSize="18sp"
android:gravity="center"
android:textColor="#FFFFFF"
android:background="#88333333"
android:visibility="gone"/>
<com.log.anotherapp.customView.IndexView
android:id="@+id/index_words"
android:layout_width="50dp"
android:layout_height="match_parent"
android:background="#88ff0000"
android:layout_alignParentRight="true"/>
</RelativeLayout>
contacts_item.xml
<?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_title"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="#C3BCBB"
android:gravity="center_vertical"
android:paddingLeft="20dp"
android:textStyle="bold"
android:textSize="18sp" />
<TextView
android:id="@+id/tv_content"
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center_vertical"
android:paddingLeft="20dp"
android:textSize="16sp" />
</LinearLayout>
IndexView.java
package com.log.anotherapp.customView;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.util.AttributeSet;
import android.util.Pair;
import android.view.MotionEvent;
import android.view.View;
import androidx.annotation.Nullable;
import com.log.anotherapp.util.DensityUtil;
public class IndexView extends View {
private String[] words = {"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 Pair<Integer, Integer>[] wordSizes = new Pair[words.length];
// 每个文字的坐下角坐标
private Pair<Float, Float>[] wordCoordinates = new Pair[words.length];
// 每个文字的有效区域(用于判断是否点击或滑动该文字区域,做高亮显示)
private Rect[] wordRects = new Rect[words.length];
// 常态文字的paint
private Paint paint;
// 高亮文字的paint
private Paint selectedPaint;
// 每一个字母的宽度和高度
private float itemWidth;
private float itemHeight;
// 当前字母索引
private int currentIndex = -1;
// 绘制的文本字体大小
private int textSize;
private OnIndexListener listener;
// 是否已经计算过字体大小和坐标,防止重复计算
private boolean isNotComputed = true;
public IndexView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 系统调用这个方法后,本控件才测量出宽和高
itemWidth = getMeasuredWidth();
itemHeight = getMeasuredHeight() / words.length;
}
/**
* 设置字体大小
*
* @param textSize
*/
public void setTextSize(int textSize) {
this.textSize = textSize;
}
private void init() {
// 默认字体大小
if (textSize <= 0) {
textSize = DensityUtil.sp2px(getContext(), 18);
}
paint = new Paint();
paint.setAntiAlias(true);
paint.setTextSize(textSize);
paint.setTypeface(Typeface.DEFAULT_BOLD); // 粗体字
paint.setColor(Color.WHITE);
selectedPaint = new Paint();
selectedPaint.setAntiAlias(true);
selectedPaint.setTextSize(textSize);
selectedPaint.setTypeface(Typeface.DEFAULT_BOLD); // 粗体字
selectedPaint.setColor(Color.GRAY);
}
private void compute() {
Rect rect = new Rect();
for (int i = 0; i < words.length; i++) {
// 计算文字宽高
paint.getTextBounds(words[i], 0, 1, rect);
Pair pair = new Pair(rect.width(), rect.height());
wordSizes[i] = pair;
// 计算每个字的左下角x和y坐标
float x = (itemWidth - wordSizes[i].first) / 2.f;
float y = (itemHeight + wordSizes[i].second) / 2.f + itemHeight * i;
wordCoordinates[i] = new Pair<>(x, y);
// 计算文字点击/触摸有效区域
wordRects[i] = new Rect(0, (int) (i * itemHeight), (int) itemWidth, (int) ((i + 1) * itemHeight));
}
isNotComputed = false;
}
@Override
protected void onDraw(Canvas canvas) {
if (isNotComputed) {
compute();
}
// 高亮文字的画笔
Paint currentPaint;
for (int i = 0; i < words.length; i++) {
if (i == currentIndex) {
currentPaint = selectedPaint;
} else {
currentPaint = paint;
}
canvas.drawText(words[i], wordCoordinates[i].first, wordCoordinates[i].second, currentPaint);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
for (int i = 0; i < wordRects.length; i++) {
// 判断点击坐标落在哪个字母上
if (wordRects[i].contains((int) x, (int) y)) {
// 当前选中索引
currentIndex = i;
invalidate();
if (listener != null) {
listener.onIndexChange(words[i]);
}
break;
}
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
currentIndex = -1;
invalidate();
if (listener != null) {
listener.onIndexRelease();
}
break;
}
return true;
}
public void setOnIndexListener(OnIndexListener listener) {
this.listener = listener;
}
public interface OnIndexListener {
void onIndexChange(String text);
void onIndexRelease();
}
}
PinYinUtil.java
package com.log.anotherapp.util;
import net.sourceforge.pinyin4j.PinyinHelper;
import net.sourceforge.pinyin4j.format.HanyuPinyinCaseType;
import net.sourceforge.pinyin4j.format.HanyuPinyinOutputFormat;
import net.sourceforge.pinyin4j.format.HanyuPinyinToneType;
import net.sourceforge.pinyin4j.format.exception.BadHanyuPinyinOutputFormatCombination;
public class PinYinUtil {
public static String getPinYin(String chineseWord) {
String pinyin = "";
HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat();//控制转换是否大小写,是否带音标
format.setCaseType(HanyuPinyinCaseType.UPPERCASE);//大写
format.setToneType(HanyuPinyinToneType.WITHOUT_TONE);
//由于不能直接对多个汉字转换,只能对单个汉字转换
char[] arr = chineseWord.toCharArray();
for (int i = 0; i < arr.length; i++) {
if (Character.isWhitespace(arr[i])) continue;//如果是空格,则不处理,进行下次遍历
//汉字是2个字节存储,肯定大于127,所以大于127就可以当为汉字转换
if (arr[i] > 127) {
try {
//由于多音字的存在,单 dan shan
String[] pinyinArr = PinyinHelper.toHanyuPinyinStringArray(arr[i], format);
if (pinyinArr != null) {
pinyin += pinyinArr[0];
} else {
pinyin += arr[i];
}
} catch (BadHanyuPinyinOutputFormatCombination e) {
e.printStackTrace();
//不是正确的汉字
pinyin += arr[i];
}
} else {
//不是汉字,
pinyin += arr[i];
}
}
return pinyin;
}
}
ContactsActivity.java
package com.log.anotherapp;
import android.os.Bundle;
import android.os.Handler;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ListView;
import android.widget.TextView;
import androidx.annotation.Nullable;
import com.log.anotherapp.customView.IndexView;
import com.log.anotherapp.model.Contacts;
import com.log.anotherapp.util.PinYinUtil;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class ContactsActivity extends BaseActivity implements IndexView.OnIndexListener {
private ListView listView;
private IndexView indexView;
private TextView textView;
private Handler handler;
private List<Contacts> contacts;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.contacts);
initView();
}
private void initView() {
handler = new Handler();
textView = findViewById(R.id.tv_abc);
listView = findViewById(R.id.lv_contacts);
indexView = findViewById(R.id.index_words);
indexView.setOnIndexListener(this);
initData();
ContactsAdapter adapter = new ContactsAdapter();
listView.setAdapter(adapter);
}
private void initData() {
contacts = new ArrayList<>();
contacts.add(new Contacts("阿一", PinYinUtil.getPinYin("阿一")));
contacts.add(new Contacts("奥利奥", PinYinUtil.getPinYin("奥利奥")));
contacts.add(new Contacts("布尔", PinYinUtil.getPinYin("布尔")));
contacts.add((new Contacts("曹操", PinYinUtil.getPinYin("曹操"))));
contacts.add((new Contacts("曹尼玛", PinYinUtil.getPinYin("曹尼玛"))));
contacts.add((new Contacts("董卿", PinYinUtil.getPinYin("董卿"))));
contacts.add((new Contacts("厄尼", PinYinUtil.getPinYin("厄尼"))));
contacts.add((new Contacts("凡妮莎", PinYinUtil.getPinYin("凡妮莎"))));
contacts.add((new Contacts("高手", PinYinUtil.getPinYin("高手"))));
contacts.add((new Contacts("韩红", PinYinUtil.getPinYin("韩红"))));
contacts.add((new Contacts("花果", PinYinUtil.getPinYin("花果"))));
contacts.add((new Contacts("InDon", PinYinUtil.getPinYin("InDon"))));
contacts.add((new Contacts("节操", PinYinUtil.getPinYin("节操"))));
contacts.add((new Contacts("康嘉", PinYinUtil.getPinYin("康嘉"))));
contacts.add((new Contacts("岚岚", PinYinUtil.getPinYin("岚岚"))));
contacts.add((new Contacts("玛尼", PinYinUtil.getPinYin("玛尼"))));
contacts.add((new Contacts("能升", PinYinUtil.getPinYin("能升"))));
contacts.add((new Contacts("欧洋", PinYinUtil.getPinYin("欧洋"))));
contacts.add((new Contacts("盼盼", PinYinUtil.getPinYin("盼盼"))));
contacts.add((new Contacts("钱途", PinYinUtil.getPinYin("钱途"))));
contacts.add((new Contacts("让龙", PinYinUtil.getPinYin("让龙"))));
contacts.add((new Contacts("诗人", PinYinUtil.getPinYin("诗人"))));
contacts.add((new Contacts("唐僧", PinYinUtil.getPinYin("唐僧"))));
contacts.add((new Contacts("ULove", PinYinUtil.getPinYin("ULove"))));
contacts.add((new Contacts("VLog", PinYinUtil.getPinYin("VLog"))));
contacts.add((new Contacts("王帝", PinYinUtil.getPinYin("王帝"))));
contacts.add((new Contacts("现金", PinYinUtil.getPinYin("现金"))));
contacts.add((new Contacts("媛媛", PinYinUtil.getPinYin("媛媛"))));
contacts.add((new Contacts("正宗", PinYinUtil.getPinYin("正宗"))));
// 按照拼音排序
Collections.sort(contacts, new Comparator<Contacts>() {
@Override
public int compare(Contacts o1, Contacts o2) {
return o1.getPinyin().compareTo(o2.getPinyin());
}
});
}
@Override
public void onIndexChange(String text) {
textView.setText(text);
textView.setVisibility(View.VISIBLE);
// 查找指定的拼音首字母在联系人数组的哪个索引
// 由于数组已经排序,只要找到第一个匹配的就可以了
int index = -1;
for (int i = 0; i < contacts.size(); i++) {
if (contacts.get(i).getPinyin().substring(0,1).equalsIgnoreCase(text)) {
index = i;
break;
}
}
// 跳转到ListView指定位置
if (index >= 0) {
listView.setSelection(index);
}
}
@Override
public void onIndexRelease() {
// 延迟一秒再隐藏
handler.postDelayed(new Runnable() {
@Override
public void run() {
textView.setVisibility(View.GONE);
}
}, 1000);
}
class ContactsAdapter extends BaseAdapter {
@Override
public int getCount() {
return contacts == null ? 0 : contacts.size();
}
@Override
public Object getItem(int position) {
return contacts == null ? null : contacts.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
holder = new ViewHolder();
convertView = LayoutInflater.from(getContext()).inflate(R.layout.contacts_item, null);
holder.title = convertView.findViewById(R.id.tv_title);
holder.content = convertView.findViewById(R.id.tv_content);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
Contacts data = contacts.get(position);
if (position == 0) {
holder.title.setText(data.getPinyin().substring(0, 1));
holder.title.setVisibility(View.VISIBLE);
} else {
// 比较上一个是否与当前的拼音首字母是否一样
if (data.getPinyin().charAt(0) == contacts.get(position - 1).getPinyin().charAt(0)) {
holder.title.setVisibility(View.GONE);
} else {
holder.title.setText(data.getPinyin().substring(0, 1));
holder.title.setVisibility(View.VISIBLE);
}
}
holder.content.setText(data.getName());
return convertView;
}
class ViewHolder {
TextView title;
TextView content;
}
}
}
因为用到了Pinyin4j,需要在build.gradle里添加依赖:
// https://mvnrepository.com/artifact/org.clojars.cbilson/pinyin4j
implementation group: 'org.clojars.cbilson', name: 'pinyin4j', version: '2.5.0'
本文地址:https://blog.csdn.net/lognic10/article/details/108979041