1,右边索引导航我自定义一个View:WordsNavigator.java
package com.txhl.testapp.cus;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import com.txhl.testapp.R;
import com.txhl.testapp.act.MailListActivity;
import com.txhl.testapp.listener.OnWordsChangeListener;
import com.txhl.testapp.utils.ScreenUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Created by chen.yingjie on 2019/4/29
* description
*/
public class WordsNavigator extends View {
private int mRealWidth;
private int mRealHeight;
private int mWidth;// 画布宽
private int mHeight;// 画布高
private int mEachHeight;// 每个字母平均分配到的占位高,宽和画布一样。
private int mTouchIndex = 0;
private Paint wordsPaint;// 字母画笔
private Paint selectedPaint;// 背景圈画笔
private Rect mRect;
private OnWordsChangeListener onShowLetterListener;
private int colorTrans;
private int colorNormal;
private int colorChecked;
private int colorCheckedBg;
public void setOnShowLetterListener(OnWordsChangeListener onShowLetterListener) {
this.onShowLetterListener = onShowLetterListener;
}
private List<String> letterLists;
public WordsNavigator(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
letterLists = new ArrayList<>();
if (getContext() instanceof MailListActivity) {
MailListActivity activity = (MailListActivity) getContext();
activity.setOnRecyclerViewScrollListener(new MailListActivity.OnRecyclerViewScrollListener() {
@Override
public void onScroll(String firstWords) {
int wordsIndex = letterLists.indexOf(firstWords);
mTouchIndex = wordsIndex;
invalidate();
}
});
}
colorTrans = getContext().getResources().getColor(R.color.transparent);
colorNormal = getContext().getResources().getColor(R.color.black);
colorChecked = getContext().getResources().getColor(R.color.white);
colorCheckedBg = getContext().getResources().getColor(R.color.red);
mRect = new Rect();
wordsPaint = new Paint();
wordsPaint.setStrokeWidth(0);
wordsPaint.setAntiAlias(true);
wordsPaint.setTextSize(ScreenUtils.dip2px(getContext(), 10));
selectedPaint = new Paint();
selectedPaint.setStrokeWidth(0);
wordsPaint.setAntiAlias(true);
selectedPaint.setStyle(Paint.Style.FILL);
}
public void setLetters(List<String> letterLists) {
this.letterLists = letterLists;
}
@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(colorTrans);
mWidth = canvas.getWidth();
mHeight = canvas.getHeight();
mEachHeight = mHeight / letterLists.size();
for (int i = 0; i < letterLists.size(); i++) {
final String _latter = letterLists.get(i);
wordsPaint.getTextBounds(_latter, 0, 1, mRect);
final int letterWidth = mRect.width();
final int letterHeight = mRect.height();
if (mTouchIndex == i) {
wordsPaint.setColor(colorChecked);
selectedPaint.setColor(colorCheckedBg);
} else {
wordsPaint.setColor(colorNormal);
selectedPaint.setColor(colorTrans);
}
// 画选中背景圈,x:画笔宽的一半,即画布水平终点;y:每个字母高乘个数减去字母高的一半;radius:半径为画布宽的三分之一。
canvas.drawCircle(mWidth / 2, mEachHeight * (i + 1) - letterHeight / 2, mWidth / 3, selectedPaint);
// 画字母,x,y分别为字母的左上角起点,背景圈的圆心取决于字母的起点。
canvas.drawText(_latter, mWidth / 2 - letterWidth / 2, (i + 1) * mEachHeight, wordsPaint);
}
}
@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;
}
private void refreshLetterIndex(int y) {
//y坐标 / 每个字母高度 = 当前字母下标
int index = y / mEachHeight;
if (index > letterLists.size() || index < 0) return;
if (index != mTouchIndex) {
mTouchIndex = index;
//回调选中的字母
if (onShowLetterListener != null) {
onShowLetterListener.wordsChange(letterLists.get(mTouchIndex));
}
invalidate();
}
}
}
这里定义了一个接口为了在手指滑动索引的时候通知activity滚动item。
public interface OnWordsChangeListener {
void wordsChange(String words);
}
2,用RecyclerView来显示列表数据,给它设置滚动监听,当滚动列表的时候要让索引跟着重绘,这里通过LinearLayoutManager找到第一个可见item的position,查出对于的首字母,通过OnRecyclerViewScrollListener接口给到WordsNavigator,让其重绘。
3,让RecyclerView滚动到指定item,用的是它的LayoutManager的startSmoothScroll方法,需要一个Scroller:
public class TopSmoothScroller extends LinearSmoothScroller {
public TopSmoothScroller(Context context) {
super(context);
}
@Override
protected int getHorizontalSnapPreference() {
return SNAP_TO_START;//具体见源码注释
}
@Override
protected int getVerticalSnapPreference() {
return SNAP_TO_START;//具体见源码注释
}
}
4,activity
package com.txhl.testapp.act;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.support.annotation.NonNull;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.txhl.testapp.R;
import com.txhl.testapp.cus.TopSmoothScroller;
import com.txhl.testapp.cus.WordsNavigator;
import com.txhl.testapp.cus.widget.SectionListView;
import com.txhl.testapp.listener.OnWordsChangeListener;
import com.txhl.testapp.utils.PinYinUtils;
import com.txhl.testapp.utils.SortUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import butterknife.BindView;
import butterknife.ButterKnife;
public class MailListActivity extends AppCompatActivity {
private final int CHANGE_WORDS = 1;
@BindView(R.id.recyclerView)
RecyclerView recyclerView;
@BindView(R.id.tvHintWord)
TextView tvHintWord;
@BindView(R.id.wordsNavigator)
WordsNavigator wordsNavigator;
private String[] names = new String[]{
"露娜", "李白", "韩信", "太乙真人", "李元芳", "阿珂", "夏侯惇", "关羽", "张飞", "刘备", "貂蝉", "吕布", "王昭君", "武则天",
"百里守约", "百里玄策", "司马懿", "孙策", "干将莫邪", "裴擒虎", "张良", "诸葛亮", "达摩", "蒙奇", "曹操", "钟馗", "钟无艳",
"程咬金", "米莱狄", "狄仁杰", "后羿", "大乔", "小乔", "刘邦", "杨玉环", "马可波罗", "狂铁", "苏烈", "赵云", "公孙离", "鬼谷子",
"成吉思汗", "哪吒", "杨戬", "嬴政", "女娲", "周瑜", "弈星", "扁鹊", "甄姬墨子", "高渐离", "亚瑟", "姜子牙", "宫本武藏",
"牛魔", "庄周", "蔡文姬", "黄忠", "鲁班七号", "铠", "妲己", "白起", "安其拉", "不知火舞", "芈月", "项羽", "刘禅", "橘右京",
"兰陵王", "典韦", "元歌", "明世隐", "雅典娜", "娜可露露", "东皇太一", "花木兰", "孙尚香", "孙膑", "虞姬", "孙悟空", "老夫子"
};
private List<String> letterLists = new ArrayList<>(Arrays.asList("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 String[] newNames;
private LinearLayoutManager linearLayoutManager;
private TopSmoothScroller smoothScroller;
private MailAdapter mailAdapter;
private OnRecyclerViewScrollListener onRecyclerViewScrollListener;
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case CHANGE_WORDS:
tvHintWord.setVisibility(View.GONE);
break;
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_mail_list);
ButterKnife.bind(this);
wordsNavigator.setLetters(letterLists);
wordsNavigator.setOnShowLetterListener(new OnWordsChangeListener() {
@Override
public void wordsChange(String words) {
mHandler.removeMessages(CHANGE_WORDS);
tvHintWord.setVisibility(View.VISIBLE);
tvHintWord.setText(words);
mHandler.sendEmptyMessageDelayed(CHANGE_WORDS, 500);
scrollToWords(words);
}
});
newNames = SortUtils.sortChar(names);
linearLayoutManager = new LinearLayoutManager(this);
smoothScroller = new TopSmoothScroller(this);
mailAdapter = new MailAdapter();
recyclerView.setAdapter(mailAdapter);
recyclerView.setLayoutManager(linearLayoutManager);
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
if (layoutManager instanceof LinearLayoutManager) {
LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager;
int firstVisibleItemPosition = linearLayoutManager.findFirstVisibleItemPosition();
String firstWords = PinYinUtils.getFirstWords(names[firstVisibleItemPosition]);
onRecyclerViewScrollListener.onScroll(firstWords);
}
}
});
}
private void scrollToWords(String words) {
for (int i = 0; i < newNames.length; i++) {
String firstWrods = PinYinUtils.getFirstWords(newNames[i]);
if (words.equals(firstWrods)) {
// 找到列表中汉字首字母和按下的字母相同的汉字
smoothScroller.setTargetPosition(i);
linearLayoutManager.startSmoothScroll(smoothScroller);
return;
}
}
}
class MailAdapter extends RecyclerView.Adapter<MailAdapter.MailHolder> {
@NonNull
@Override
public MailHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
View view = View.inflate(MailListActivity.this, R.layout.item_mail_list, null);
return new MailHolder(view);
}
@Override
public void onBindViewHolder(MailHolder viewHolder, int position) {
String lastFirstChar = "";// 上一个首汉字
String lastFirstWords = "";// 上一个首汉字的首字母
String firstChar = "";// 当前首汉字
String firstWords = "";// 当前首汉字的首字母
firstChar = newNames[position].substring(0, 1);
firstWords = PinYinUtils.getFirstWords(firstChar);// 多音字,只取第一个字母。
if (position > 0) {
lastFirstChar = newNames[position - 1].substring(0, 1);
lastFirstWords = PinYinUtils.getFirstWords(lastFirstChar);
}
if (firstWords.equals(lastFirstWords)) {
viewHolder.tvWords.setVisibility(View.GONE);
} else {
viewHolder.tvWords.setVisibility(View.VISIBLE);
viewHolder.tvWords.setText(firstWords);
}
viewHolder.tvName.setText(newNames[position]);
}
@Override
public int getItemCount() {
return newNames == null ? 0 : newNames.length;
}
class MailHolder extends RecyclerView.ViewHolder {
@BindView(R.id.tvWords)
TextView tvWords;
@BindView(R.id.tvName)
TextView tvName;
@BindView(R.id.tvMsg)
TextView tvMsg;
@BindView(R.id.ivPhoto)
ImageView ivPhoto;
public MailHolder(@NonNull View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
}
}
}
public interface OnRecyclerViewScrollListener {
void onScroll(String firstWords);
}
public void setOnRecyclerViewScrollListener(OnRecyclerViewScrollListener onRecyclerViewScrollListener) {
this.onRecyclerViewScrollListener = onRecyclerViewScrollListener;
}
}
用pinyin4j.jar来取首字母,可能有多音字,pinyinName是一个类似(比如重 pinyinName=c,z),这里只取第一个:
public static String getFirstWords(String chines) {
StringBuffer pinyinName = new StringBuffer();
char[] nameChar = chines.toCharArray();
HanyuPinyinOutputFormat defaultFormat = new HanyuPinyinOutputFormat();
defaultFormat.setCaseType(HanyuPinyinCaseType.UPPERCASE);
defaultFormat.setToneType(HanyuPinyinToneType.WITHOUT_TONE);
for (int i = 0; i < nameChar.length; i++) {
if (nameChar[i] > 128) {
try {
String[] strs = PinyinHelper.toHanyuPinyinStringArray(nameChar[i], defaultFormat);
if (strs != null) {
for (int j=0; j<strs.length; j++) {
pinyinName.append(strs[j].substring(0,1));
if (j != strs.length - 1) {
pinyinName.append(",");
}
}
}
} catch (BadHanyuPinyinOutputFormatCombination e) {
e.printStackTrace();
}
} else {
pinyinName.append(nameChar[i]);
}
}
return pinyinName.toString().substring(0, 1);
}
排序方式有很多:
* 按首字母排序
* @param strs
* @return
*/
public static String[] sortChar(String[] strs) {
Comparator<Object> com = Collator.getInstance(Locale.CHINA);
List<String> list = Arrays.asList(strs);
Collections.sort(list, com);
return list.toArray(new String[0]);
}
5,布局
activity_mail_list
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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=".act.MailListActivity">
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<com.txhl.testapp.cus.WordsNavigator
android:id="@+id/wordsNavigator"
android:layout_marginRight="12dp"
android:layout_marginTop="100dp"
android:layout_marginBottom="40dp"
android:background="@color/transparent"
android:layout_gravity="right"
android:layout_width="30dp"
android:layout_height="match_parent" />
<TextView
android:id="@+id/tvHintWord"
android:text="a"
android:background="@color/gray"
android:visibility="gone"
android:layout_centerInParent="true"
android:gravity="center"
android:layout_gravity="center"
android:textSize="30dp"
android:textColor="@color/red"
android:layout_width="60dp"
android:layout_height="80dp" />
</FrameLayout>
item_mail_list
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/tvWords"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="5dp"
android:textSize="16sp"
android:background="@color/bg_focus"
android:text="A"
android:textColor="@color/blue" />
<LinearLayout
android:padding="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:id="@+id/ivPhoto"
android:layout_gravity="center_vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/ic_launcher_round" />
<LinearLayout
android:layout_gravity="center_vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/tvName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="5dp"
android:text=""
android:textColor="@color/black"
android:textSize="18sp"/>
<TextView
android:id="@+id/tvMsg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="5dp"
android:maxLines="2"
android:text="我是一个小学生,你不要欺负我,否则我会哭的!"
android:textColor="@color/gray"
android:textSize="13sp" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
补充:测试发现字母导航器在滑动的时候不跟手,没有那种如丝般顺滑的手感,经过分析原因是,在手指滑动导航器的时候,会触发recyclerView的onScroll监听,这个监听里又不断的告诉导航器你该重绘了,但是如果我们滑动导航器,这时候是不应该再让RecyclerView告诉导航器重绘的,所以导致导航器不断的重绘导致滑动像大鼻涕一样粘稠。故我们在activity里定义一个boolean值来控制是否需要重绘导航器,而且这个值默认为true,否则第一次进页面不会绘制导航器,导致其看不见。
MailListActivity.java
private boolean needInvalidate = true;// 是否需要重绘字母导航
是手指滑动recyclerView的时候需要重绘导航器
recyclerView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
needInvalidate = true;
return false;
}
});
在导航器滑动导致recyclerView滑动的时候不需要重绘导航器
wordsNavigator.setOnShowLetterListener(new OnWordsChangeListener() {
@Override
public void wordsChange(String words) {
needInvalidate = false;
mHandler.removeMessages(CHANGE_WORDS);
tvHintWord.setVisibility(View.VISIBLE);
tvHintWord.setText(words);
mHandler.sendEmptyMessageDelayed(CHANGE_WORDS, 500);
scrollToWords(words);
}
});
控制导航器是否重绘
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (!needInvalidate) return;
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
if (layoutManager instanceof LinearLayoutManager) {
LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager;
int firstVisibleItemPosition = linearLayoutManager.findFirstVisibleItemPosition();
String firstWords = PinYinUtils.getFirstWords(names[firstVisibleItemPosition]);
onRecyclerViewScrollListener.onScroll(firstWords);
}
}
});