博主聲明:
轉(zhuǎn)載請?jiān)陂_頭附加本文鏈接及作者信息屋摔,并標(biāo)記為轉(zhuǎn)載芹彬。本文由博主 威威喵 原創(chuàng),請多支持與指教贡未。
本文首發(fā)于此 博主:威威喵 | 博客主頁:https://blog.csdn.net/smile_running
我們來實(shí)現(xiàn)這樣的一個小Demo熊榛,聯(lián)系人的快速檢索功能锚国,左側(cè)帶有字母快速定位,滑動列表時(shí)字母相繼切換的效果玄坦。那么我們來看看實(shí)現(xiàn)的Demo血筑,顯而易見,這是使用列表控件煎楣,而我使用的是ListView控件豺总。至于ListView的一些用法,您可以看這博主的這篇文章ListView使用技巧择懂、優(yōu)化和用法拓展喻喳,掌握ListView,這篇文章我們就不說明它的使用姿勢了:
- 源碼下載:
- CSDN資源鏈接:聯(lián)系人列表(右側(cè)帶首字母檢索休蟹、分類和快速定位)
- GitHub下載地址
效果圖
想來想去沸枯,我就搞了個王者榮耀的英雄名稱來作為我們的聯(lián)系人。因?yàn)槟M器上也沒有聯(lián)系人赂弓,也懶得搞绑榴,手機(jī)的話這個真實(shí)的聯(lián)系人也就不放出來,這也是個人隱私嘛盈魁。搞這么多英雄的名字還挺累的翔怎,大家將就著看,其實(shí)原理都一樣杨耙。所以赤套,這不是我們的關(guān)鍵,重要的是如何實(shí)現(xiàn)這種效果呢珊膜?
-
思路及實(shí)現(xiàn)
一容握、左側(cè)字母檢索器
左邊是一個自定義View,通過獲得屏幕的高度车柠,將26個字母逐一從上往下繪制剔氏。通過點(diǎn)擊事件繪制不同的顏色,再通過監(jiān)聽右邊列表的滑動檢索字母選擇器中的相對應(yīng)字母下標(biāo)竹祷,然后繪制改變字母的顏色谈跛。
二、列表顯示字母分類
列表添加數(shù)據(jù)塑陵,主要說一下頭部的字母分類實(shí)現(xiàn)感憾。通過獲取漢字的原拼音,再檢索出首字母令花。在適配器中判斷當(dāng)前字母是否與前一個字母相同阻桅。相同則歸類,不相同則分類兼都。
三鳍刷、中間的字母提示
這個應(yīng)該是最簡單的了,在我們的自定義View里通過接口回調(diào)獲取點(diǎn)擊或滑動切換時(shí)所顯示的字母俯抖,然后在設(shè)置到一個TextView上输瓜,通過定時(shí)器將它延遲隱藏。
大致思路就是這樣了芬萍,其實(shí)自己使勁的想一想尤揣,還是可以實(shí)現(xiàn)出這樣的效果的,也不是特別難柬祠。思路講完了北戏,那么我們就上代碼吧,由于代碼比較多漫蛔,所以這里只貼比較核心的地方來看看嗜愈,若有需要請到我的Github上查看完整的項(xiàng)目旧蛾。
一、左側(cè)字母檢索器
/**
* @Created by xww.
* @Creation time 2018/8/18.
*/
public class ContactIndexView extends View {
private int mRealWidth;
private int mRealHeight;
private int mWidth;
private int mHeight;
private int mEachHeight;
private int mTouchIndex = 0;
private Paint mPaint;
private Rect mRect;
private onShowLetterListener onShowLetterListener = null;
private int colorBackground;
private int colorNormal;
private int colorChecked;
public interface onShowLetterListener {
void showLatter(String letter);
}
public void setOnShowLetter(onShowLetterListener showLetterListener) {
this.onShowLetterListener = showLetterListener;
}
private String[] letters = 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"};
@TargetApi(Build.VERSION_CODES.M)
private void init() {
EventBus.getDefault().register(this);
colorBackground = getContext().getResources().getColor(R.color.colorDivider, null);
colorNormal = getContext().getResources().getColor(R.color.colorWhite, null);
colorChecked = getContext().getResources().getColor(R.color.colorPrimaryDark, null);
mRect = new Rect();
mPaint = new Paint();
mPaint.setStrokeWidth(3f);
mPaint.setAntiAlias(true);
mPaint.setTextSize(38f);
}
public ContactIndexView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
switch (widthMode) {
case MeasureSpec.UNSPECIFIED:
case MeasureSpec.AT_MOST:
break;
case MeasureSpec.EXACTLY:
mRealWidth = widthSize;
break;
}
switch (heightMode) {
case MeasureSpec.UNSPECIFIED:
case MeasureSpec.AT_MOST:
break;
case MeasureSpec.EXACTLY:
mRealHeight = heightSize;
break;
}
setMeasuredDimension(mRealWidth, mRealHeight);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(colorBackground);
mWidth = canvas.getWidth();
mHeight = canvas.getHeight() - getStatusbarHeight();
mEachHeight = mHeight / letters.length;
for (int i = 0; i < letters.length; i++) {
final String _latter = letters[i];
mPaint.getTextBounds(_latter, 0, 1, mRect);
final int letterWidth = mRect.width();
final int letterHeight = mRect.height();
if (mTouchIndex == i) {
mPaint.setColor(colorChecked);
} else {
mPaint.setColor(colorNormal);
}
canvas.drawText(_latter, mWidth / 2 - letterWidth / 2, (i + 1) * mEachHeight - letterHeight / 2, mPaint);
}
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
final int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
refreshLetterIndex(y);
break;
case MotionEvent.ACTION_MOVE:
refreshLetterIndex(y);
break;
}
return true;
}
@Subscribe
public void onListScrollEvent(ScrollEvent event) {
if (event.isLast()) {
return;
}
for (int i = 0; i < letters.length; i++) {
if (event.getLetter().equals(letters[i])) {
mTouchIndex = i;
invalidate();
return;
}
}
}
private void refreshLetterIndex(int y) {
//y坐標(biāo) / 每個字母高度 = 當(dāng)前字母下標(biāo)
int index = y / mEachHeight;
if (index != mTouchIndex) {
mTouchIndex = index;
//回調(diào)選中的字母
if (onShowLetterListener != null) {
onShowLetterListener.showLatter(letters[mTouchIndex]);
}
invalidate();
}
}
private int getStatusbarHeight() {
int resId = getContext().getResources().getIdentifier("status_bar_height", "dimen", "android");
return resId > 0 ? getContext().getResources().getDimensionPixelSize(resId) : 0;
}
}
代碼挺長的蠕嫁,最重要的是計(jì)算字母所在的x锨天,y坐標(biāo),坐標(biāo)計(jì)算準(zhǔn)了剃毒,才可以繪制到控件的中心去病袄,否則你會發(fā)現(xiàn)字母將會偏右或偏左等,顯示效果比較難看赘阀。
final String _latter = letters[i];
mPaint.getTextBounds(_latter, 0, 1, mRect);
final int letterWidth = mRect.width();
final int letterHeight = mRect.height();
這段代碼片段益缠,它主要是給文本繪制了一個邊框,然后我們從邊框的寬基公、高間接得到文本的寬和高幅慌。
二、列表顯示字母分類
/**
* @Created by xww.
* @Creation time 2018/8/18.
*/
public class MainActivity extends AppCompatActivity {
private ListView mListView;
private ContactIndexView mIndexView;
private TextView mShowTextView;
private CountDownTimer mCountDownTimer;
private ContactAdapter mAdapter;
private ArrayList<Contact> mContacts;
private String mLetter;
private static final String[] NAME = new String[]{
"露娜", "李白", "韓信", "太乙真人", "李元芳", "阿珂", "夏侯惇", "關(guān)羽", "張飛", "劉備", "貂蟬", "呂布", "王昭君", "武則天",
"百里守約", "百里玄策", "司馬懿", "孫策", "干將莫邪", "裴擒虎", "張良", "諸葛亮", "達(dá)摩", "蒙奇", "曹操", "鐘馗", "鐘無艷",
"程咬金", "米萊狄", "狄仁杰", "后羿", "大喬", "小喬", "劉邦", "楊玉環(huán)", "馬可波羅", "狂鐵", "蘇烈", "趙云", "公孫離", "鬼谷子",
"成吉思汗", "哪吒", "楊戩", "嬴政", "女媧", "周瑜", "弈星", "扁鵲", "甄姬墨子", "高漸離", "亞瑟", "姜子牙", "宮本武藏",
"牛魔", "莊周", "蔡文姬", "黃忠", "魯班七號", "鎧", "妲己", "白起", "安其拉", "不知火舞", "羋月", "項(xiàng)羽", "劉禪", "橘右京",
"蘭陵王", "典韋", "元歌", "明世隱", "雅典娜", "娜可露露", "東皇太一", "花木蘭", "孫尚香", "孫臏", "虞姬", "孫悟空", "老夫子"
};
@SuppressWarnings("unchecked")
private <T extends View> T $(int id) {
View view = findViewById(id);
return (T) view;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
}
private void initView() {
mListView = $(R.id.list_contact);
mIndexView = $(R.id.view_contact);
mShowTextView = $(R.id.tv_show_letter);
initData();
initListener();
}
private void initData() {
mContacts = new ArrayList<>();
for (String name : NAME) {
mContacts.add(new Contact(name));
}
Collections.sort(mContacts, (o1, o2) -> o1.getPinyin().compareTo(o2.getPinyin()));
mAdapter = new ContactAdapter(mContacts);
mListView.setAdapter(mAdapter);
}
@SuppressWarnings("UnusedAssignment")
private void initListener() {
mIndexView.setOnShowLetter(letter -> {
mShowTextView.setText(letter);
mLetter = letter;
if (mCountDownTimer == null) {
mCountDownTimer = new CountDownTimer(1500, 500) {
@Override
public void onTick(long millisUntilFinished) {
mShowTextView.setVisibility(View.VISIBLE);
}
@Override
public void onFinish() {
mShowTextView.setVisibility(View.INVISIBLE);
if (mCountDownTimer != null) {
mCountDownTimer.cancel();
mCountDownTimer = null;
}
}
}.start();
} else {
mCountDownTimer.start();
}
for (int i = 0; i < mContacts.size(); i++) {
final String letterName = mContacts.get(i).getPinyin().substring(0, 1);
if (letterName.equals(mLetter)) {
mListView.setSelection(i);
return;
}
}
});
mListView.setOnScrollListener(new AbsListView.OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
String letter = mContacts.get(firstVisibleItem).getPinyin().substring(0, 1);
boolean isLast = false;
if (firstVisibleItem + visibleItemCount == totalItemCount) {
isLast = true;
}
EventBus.getDefault().post(new ScrollEvent(letter, isLast));
}
});
}
}
我們的Activity沒什么好說的轰豆,就是添加數(shù)據(jù)和一些監(jiān)聽事件欠痴。主要注意列表的滑動監(jiān)聽,我們要判斷是否已經(jīng)滑動到了最底部秒咨,如果在列表最底部喇辽,我們不再處理滑動監(jiān)聽事件。其實(shí)雨席,我之前用的是自己隨機(jī)組的一些名字菩咨,發(fā)現(xiàn)有點(diǎn)影響美觀,所以就換了陡厘。
三抽米、列表適配器的邏輯代碼
/**
* @Created by xww.
* @Creation time 2018/8/18.
*/
public class ContactAdapter extends BaseAdapter {
private ArrayList<Contact> mContacts;
ContactAdapter(ArrayList<Contact> contacts) {
this.mContacts = contacts;
}
@Override
public int getCount() {
return mContacts == null ? 0 : mContacts.size();
}
@Override
public Object getItem(int position) {
return mContacts.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) {
convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.adapter_contact, parent, false);
holder = new ViewHolder(convertView);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
final String name = mContacts.get(position).getName();
final String pinyin = mContacts.get(position).getPinyin().substring(0, 1);
if (position == 0) {
holder.tvPinYin.setVisibility(View.VISIBLE);
} else {
final String prePinyin = mContacts.get(position - 1).getPinyin().substring(0, 1);
if (pinyin.equals(prePinyin)) {
holder.tvPinYin.setVisibility(View.GONE);
} else {
holder.tvPinYin.setVisibility(View.VISIBLE);
}
}
holder.tvName.setText(name);
holder.tvPinYin.setText(pinyin);
return convertView;
}
private static class ViewHolder {
private TextView tvName;
private TextView tvPinYin;
ViewHolder(View itemView) {
tvName = itemView.findViewById(R.id.tv_name);
tvPinYin = itemView.findViewById(R.id.tv_pinyin);
}
}
}
我們看關(guān)鍵代碼:
if (position == 0) {
holder.tvPinYin.setVisibility(View.VISIBLE);
} else {
final String prePinyin = mContacts.get(position - 1).getPinyin().substring(0, 1);
if (pinyin.equals(prePinyin)) {
holder.tvPinYin.setVisibility(View.GONE);
} else {
holder.tvPinYin.setVisibility(View.VISIBLE);
}
}
這段是實(shí)現(xiàn)列表頭部的字母分類的,我們知道第一個字母永遠(yuǎn)不會重復(fù)糙置,而從第二個字母開始云茸,我們判斷是否與前一個字母一樣來決定字母的顯示與否。
四谤饭、漢字轉(zhuǎn)拼音的一個工具類
/**
* @Created by xww.
* @Creation time 2018/8/18.
*/
final class PinYinUtils {
public static String getPinYin(String hanzi) {
StringBuilder pinyin = new StringBuilder();
HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat();
format.setCaseType(HanyuPinyinCaseType.UPPERCASE);
format.setToneType(HanyuPinyinToneType.WITHOUT_TONE);
char[] arr = hanzi.toCharArray();
for (char anArr : arr) {
if (Character.isWhitespace(anArr)) continue;
if (anArr > 127) {
try {
String[] pinyinArr = PinyinHelper.toHanyuPinyinStringArray(anArr, format);
if (pinyinArr != null) {
pinyin.append(pinyinArr[0]);
} else {
pinyin.append(anArr);
}
} catch (BadHanyuPinyinOutputFormatCombination e) {
e.printStackTrace();
pinyin.append(anArr);
}
} else {
pinyin.append(anArr);
}
}
return pinyin.toString();
}
}
這個工具欄比較固定 标捺,那么所有的關(guān)鍵是的代碼基本就這些了。這是我個人寫的一個聯(lián)系人的Demo揉抵,個人認(rèn)為界面還不錯亡容,哈哈。