高仿微信通讯录
通讯录是很多社交 App 都有的一个功能,实现这一功能其实并不难,但是还是有一些问题会困扰一点时间,在这里我就记录一下我根据自己思路仿照微信通讯录的过程,其中会有记录我想过的方式和最终实现的方法,先看看我实现后的样子吧。 GIF.gif
一.首先我们先准备好我们的布局文件
(一)主界面的 activity_main.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"
android:orientation="vertical">
<android.support.v7.widget.RecyclerView
android:id="@+id/rv_contracts"
android:layout_width="match_parent"
android:layout_height="match_parent">
</android.support.v7.widget.RecyclerView>
<com.example.asus.demo.widget.IndexBar
android:id="@+id/v_index"
android:background="@drawable/drawableIndexView"
android:layout_alignRight="@id/rv_contracts"
android:layout_width="22dp"
android:layout_height="match_parent"
/>
<TextView
android:visibility="invisible"
android:id="@+id/tv_letter"
android:layout_centerInParent="true"
android:background="@drawable/letterview_background_shape"
android:layout_width="70dp"
android:layout_height="70dp"
android:textSize="45sp"
android:gravity="center"
android:textColor="@color/colorTextWhite"
/>
</RelativeLayout>
(二)通讯录的布局item_contract.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="52dp"
android:clickable="true"
android:background="@drawable/item_background_selector"
android:orientation="vertical">
<RelativeLayout
android:id="@+id/rl"
android:layout_width="match_parent"
android:layout_height="51dp">
<ImageView
android:id="@+id/iv_profile_picture"
android:layout_width="32dp"
android:layout_height="33dp"
android:layout_centerVertical="true"
android:layout_marginLeft="10dp"
android:background="@drawable/ic_launcher_background" />
<TextView
android:id="@+id/tv_name"
android:layout_width="match_parent"
android:layout_height="30dp"
android:layout_centerVertical="true"
android:layout_marginLeft="10dp"
android:layout_toRightOf="@+id/iv_profile_picture"
android:text="名字"
android:textColor="@color/colorTextBlack"
android:gravity="center_vertical"
android:textSize="17.5sp" />
</RelativeLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginLeft="10dp"
android:background="@color/colorViewDivider" />
</LinearLayout>
(三)分隔线的布局
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="20dp">
<TextView
tools:text="A"
android:textSize="15dp"
android:paddingLeft="10dp"
android:layout_gravity="center_horizontal"
android:id="@+id/tv_letter"
android:background="@color/colorDivider"
android:layout_width="match_parent"
android:layout_height="20dp" />
</LinearLayout>
(四)底部显示联系人数据的 TextView 的布局 item_footer.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="50dp"
android:background="@color/colorItemFooter"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical">
<TextView
android:id="@+id/tv_footer"
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center"
tools:text="187位联系人"
android:textSize="17sp"
/>
</LinearLayout>
(五)最后一个就是我们点击弹出“设置备注及标签”的窗口布局 menu_window.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:clickable="true"
android:background="@drawable/item_background_selector"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:text="设置备注及标签"
android:layout_width="150dp"
android:gravity="center"
android:textSize="17dp"
android:layout_height="50dp" />
</LinearLayout>
二.准备好布局文件后我们先定义我们联系人的实体类和一些常量类
联系人的实体类
public class ContractorEntity {
private int ContractorId;
private int mProfilePicture;//联系人的头像
private String mName;//联系人名字
private int mType;//类型由于区分联系人和字母分隔
public int getContractorId() {
return ContractorId;
}
public void setContractorId(int contractorId) {
ContractorId = contractorId;
}
public int getProfilePicture() {
return mProfilePicture;
}
public void setProfilePicture(int profilePicture) {
mProfilePicture = profilePicture;
}
public String getName() {
return mName == null ? "" : mName;
}
public void setName(String name) {
mName = name;
}
public int getType() {
return mType;
}
public void setType(int type) {
mType = type;
}
}
三.一些简单的准备工作准备好后,我们就开始一点一点的实现吧,实现这一功能需要解决的问题有如下的几个:
1.通讯录的排序。
2.添加分隔线和底部的显示总数的布局
3.每个 item 的点击事件的处理,这里分为两种:
(1)单纯的短按点击:只有 item 背景颜色的改变
(2)长按点击:根据点击的位置弹出 window ,并背景颜色改变,点击外部 window 收回,背景颜色的恢复。
4.添加索引条,并定位到相应的 字母分隔线。
四.通讯录的排序
private List<ContractorEntity> mContractorEntities;
private List<ContractorEntity> mEntityList;
public void initData() {
mEntityList = new ArrayList<>();
mContractorEntities = new ArrayList<>();
for (int i = 0; i < sNAME.length; i++) {
ContractorEntity entity = new ContractorEntity();
entity.setContractorId(i);
entity.setType(Type.CONTRACT);
entity.setName(sNAME[i]);
entity.setProfilePicture(R.drawable.category);
mEntityList.add(entity);
}
SortListUtil.addDividerLetter(mContractorEntities, mEntityList);
mContractorEntities.addAll(mEntityList);
SortListUtil.sortList(mContractorEntities);
ContractorEntity entity = new ContractorEntity();
entity.setType(Type.FOOTER);
entity.setName(mContractorEntities.size() + "");
mContractorEntities.add(entity);
}
这里我用了两个 List 集合,mContractorEntities 是用于 Adapter 数据源的集合,即所有的数据(联系人,字母分割)会放在这个集合里面。mEntityList 是我用来辅助添加的集合,为什么要这么做呢?等我把 SortListUtil.addDividerLetter 和 SortListUtil.sortList 贴出来的就知道了。
public static List<ContractorEntity> addDividerLetter(List<ContractorEntity>
contractorEntities,List<ContractorEntity> entityList){
boolean isAddChar = false;
for (int i=1;i< LETTERCHAR.length;i++){
for (int j=0;j<entityList.size();j++){
char name = Pinyin.toPinyin(entityList.get(j).getName(),"").charAt(0);
if (name==LETTERCHAR[i]){
ContractorEntity entity = new ContractorEntity();
entity.setName(Letter.LETTER[i]);
entity.setType(Type.LETTER);
contractorEntities.add(entity);
break;
}else if (name>'Z'||name<'A'&&!isAddChar){
ContractorEntity entity =new ContractorEntity();
entity.setName("#");
entity.setType(Type.LETTER);
contractorEntities.add(entity);
isAddChar =true;
}
}
}
return contractorEntities;
}
public static List<ContractorEntity> sortList(List<ContractorEntity> list){
List<ContractorEntity> entityList = new ArrayList<>();
entityList.addAll(list);
Comparator<ContractorEntity> characterComparator = new Comparator<ContractorEntity>() {
@Override
public int compare(ContractorEntity contractorEntity, ContractorEntity t1) {
return Comparate(contractorEntity,t1);
}
};
// list.sort(characterComparator);
Collections.sort(list,characterComparator);
return entityList;
}
可以看到我的思路就是先把联系人放入辅助集合 mEntityList 中,然后就是添加分隔的字母,从第一个字母开始,每次都去遍历集合中的姓名的拼音的首个字母,如果匹配到,就添加到 contractorEntities 来,并设置 类型为 Type.LETTER ,而对于非字母的,为了避免重复的添加,所以设置了一个 boolean isAddChar = false; 一旦添加了就设为 true. 接着就是把 联系人姓名添加到 mContractorEntities ,注意对于mContractorEntities ,首先添加的是联系人中含有的字母,然后才是通讯人,这样做其实就保证了在排序后字母分隔线总是在每个分类的最前面。具体的规则就是 Comparate 这个方法。
public static int Comparate(ContractorEntity entity,ContractorEntity nextEntity){
int result = 0;
String name = Pinyin.toPinyin(entity.getName(),"");
String nextName = Pinyin.toPinyin(nextEntity.getName(),"");
int nameIndex=0;
int nextNameIndex=0;
int nameLength = name.length();
int nextNameLength = nextName.length();
char nameChar = name.charAt(0);
char nextNameChar = nextName.charAt(0);
//首先比较首字母,把是字母和不是字母的分开,属于字母在前(-1),不属于字母的在后(1),
if ((nameChar<='Z'&&nameChar>='A')&&!(nextNameChar<='Z'&&nextNameChar>='A')){
return -1;
}else if (!(nameChar<='Z'&&nameChar>='A')&&(nextNameChar<='Z'&&nextNameChar>='A')){
return 1;
}
//如果都是字母,就对每个字符进行遍历比较
while ((nameIndex<nameLength)&&(nextNameIndex<nextNameLength)){
//name 的首字母 比 nextName 的首字母小,排在前面(-1)
if (name.charAt(nameIndex)<nextName.charAt(nextNameIndex)){
result = -1;
break;
//name 的首字母 比 nextName 的首字母大,排在后面(1)
}else if (name.charAt(nameIndex)>nextName.charAt(nextNameIndex)){
result = 1;
break;
//name 的首字母 和 nextName 的首字母 一样大,向下一个字符遍历
}else {
result = 0;
nameIndex++;
nextNameIndex++;
}
//如果前部分都一样,比较长度
if (nextNameIndex==nextNameLength){
result = 1;
}else if (nameIndex==nameLength){
result = -1;
}else {
result = 0;
}
}
return result;
}
五.添加分隔线和底部的显示总数的布局
public class ContractsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private List<ContractorEntity> mContractorEntities;
private float mX;
private float mY;
private ClickListener mClickListener;
private static final String TAG = "ContractsAdapter";
public interface ClickListener{
void OnClickListener(View view,float x,float y);
void OnLongClickListener(View view,float x,float y);
}
public ContractsAdapter(List<ContractorEntity> contractorEntities) {
mContractorEntities = contractorEntities;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view;
//列表的ViewHolder
if (viewType== Type.CONTRACT){
view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_contract,parent,false);
ViewHolder viewHolder = new ViewHolder(view);
return viewHolder;
//字母分隔的ViewHolder
}else if (viewType==Type.LETTER){
view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_divider,parent,false);
DividerViewHolder viewHolder = new DividerViewHolder(view);
return viewHolder;
}else {
//根View 的ViewHolder
view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_footer,parent,false);
FooterViewHolder viewHolder = new FooterViewHolder(view);
return viewHolder;
}
}
@Override
public void onBindViewHolder(final RecyclerView.ViewHolder holder, final int position) {
if (mContractorEntities.get(position).getType()==Type.CONTRACT){
((ViewHolder)holder).profilePicture.setBackgroundResource(mContractorEntities.get(position).getProfilePicture());
((ViewHolder)holder).name.setText(mContractorEntities.get(position).getName());
((ViewHolder)holder).itemView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View view) {
if (mClickListener!=null){
mClickListener.OnLongClickListener(view,mX,mY);
}
return false;
}
});
((ViewHolder)holder).itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (mClickListener!=null){
mClickListener.OnClickListener(view,mX,mY);
}
}
});
((ViewHolder)holder).itemView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
//在这里获取点击的做标
mX = motionEvent.getX();
mY = motionEvent.getY();
return false;
}
});
//
// ((ViewHolder)holder).itemView.setOnTouchListener(new
View.OnTouchListener() {
// @Override
// public boolean onTouch(View view, MotionEvent motionEvent) {
//
// if (mClickListener != null) {
// mClickListener.OnClickListener(view, motionEvent);
// }
//
// return false;
// }
// });
}
else if (mContractorEntities.get(position).getType()==Type.LETTER){
((DividerViewHolder)holder).letter.setText(mContractorEntities.get(position).getName());
}else{
((FooterViewHolder)holder).footer.setText(mContractorEntities.get(position).getName()+"位联系人");
}
}
@Override
public int getItemCount() {
return mContractorEntities.size();
}
@Override
public int getItemViewType(int position) {
return mContractorEntities.get(position).getType();
}
public void setClickListener(ClickListener clickListener) {
mClickListener = clickListener;
}
class ViewHolder extends RecyclerView.ViewHolder{
ImageView profilePicture;
TextView name;
private ViewHolder(View itemView) {
super(itemView);
profilePicture = itemView.findViewById(R.id.iv_profile_picture);
name = itemView.findViewById(R.id.tv_name);
}
}
class DividerViewHolder extends RecyclerView.ViewHolder{
TextView letter;
public DividerViewHolder(View itemView) {
super(itemView);
letter = itemView.findViewById(R.id.tv_letter);
}
}
class FooterViewHolder extends RecyclerView.ViewHolder{
TextView footer;
public FooterViewHolder(View itemView) {
super(itemView);
footer = itemView.findViewById(R.id.tv_footer);
}
}
}
主要还是根据 mContractorEntities 的元素的中定义的类型结合 getItemViewType这个方法,创建不同的 ViewHolder ,关于这方面的知识网上有很多的例子,这里就不做赘述。
六.为每个 item 的点击事件进行处理。
(1)首先讲一下我们在看微信的通讯中的时候可以看到,“设置备注及标签”这个 window 是会根据点击的位置弹出来的,所以一开始我想到的是使用 setOnTouchListener ,这个可以在 上面的Adapter 被我注释掉的那一块看到,当我使用这个方法的时候虽然可以做到根据手指的点击位置弹出,但是遇到的一个问题就是 我们在微信通讯录可以看到,每一个 item 是可以进行长按弹出 window,背景色保持不变,短按跳到具体的界面,并带有背景色的改变的效果,也就是设置 selector 。但是使用 setOnTouchListener 就实现不了长按(网上的方法没有找到可行的),后来我就想看看 setOnLongClickListener 和 setOnClickListener ,这是传统处理长按和点击方法,但是单纯使用者两个方法虽然解决了长按和点击,但是实现不了跟着 手指点击的位置弹出 window 的效果。因为只有拿到 MotionEvent 才能获取点击的位置,而这两个方法却没有这个参数,接着我的一个想法就是在 setOnTouchListener 拿到 MotionEvent ,然后作为参数传到 上面两个方法里的接口中去,本来以为这应该是解决方法,但是发现点击的时候 window 的弹出位置发生了很大的变化,使用Logcat 打印的三个方法的 MotionEvent 的 getX 和 getY 后发现 getY 发生了很大的变化,但是我的确是将 MotionEvent 赋给一个变量,再将变量穿个两个方法的,可是MotionEvent 却发生了变化,这里可能涉及到事件分发机制的问题,在这里先进行保留,鉴于我只要点击的坐标即可,最后我更改了一下,在 setOnClickListener 中直接获取 float型坐标,然后作为参数传给接口的方法。
private float mX;
private float mY;
.
.
.
.
((ViewHolder)holder).itemView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
//在这里获取点击的做标
mX = motionEvent.getX();
mY = motionEvent.getY();
return false;
}
});
((ViewHolder)holder).itemView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View view) {
if (mClickListener!=null){
mClickListener.OnLongClickListener(view,mX,mY);
}
return false;
}
});
((ViewHolder)holder).itemView.setOnClickListener(new
View.OnClickListener() {
@Override
public void onClick(View view) {
if (mClickListener!=null){
mClickListener.OnClickListener(view,mX,mY);
}
}
});
(2)接着就是 “设置备注及标签” 的显示
//获取PopWindow宽和高
mPopupWindow = new PopupWindow(getLayoutInflater().inflate(R.layout.menu_window, null), 300, 100, true);
mPopupWindow.setOutsideTouchable(true);//设置外部可点击取消
mPopupWindow.setElevation(5);//设置阴影
在 setClickListener 的两个方法根据传过来的位置的参数进行计算
mAdapter.setClickListener(new ContractsAdapter.ClickListener() {
@Override
public void OnClickListener(View view, float x, float y) {
}
@Override
public void OnLongClickListener(final View view, float x, float y) {
view.setBackgroundResource(R.drawable.drawableItemPressed);
DisplayMetrics metrics = getResources().getDisplayMetrics();
int width = metrics.widthPixels;
int xoff = (int) x;
int yoff = 0 - (view.getHeight() - (int) y) - mPopupWindow.getHeight() - 50;
if (x > width / 2) {
xoff = (int) x - width / 2 + 10;
mPopupWindow.setAnimationStyle(R.style.AnimationRight);
} else {
mPopupWindow.setAnimationStyle(R.style.AnimationLeft);
}
mPopupWindow.showAsDropDown(view, xoff, yoff, Gravity.TOP);
mPopupWindow.setOnDismissListener(new PopupWindow.OnDismissListener() {
@Override
public void onDismiss() {
view.setBackgroundResource(R.drawable.item_background_selector);
}
});
}
});
为了解释上面这段代码我们画个图更加容易理解,首先看
这可以看到这个window 的零点坐标在其左上角,在View的 左下角。理解了这个方法的位置参数后我们看看下一张图 item_event.png
这是我们点击每个item时,MotionEvent 的坐标参考系,在有这两张图后,那我们看下面这张图就可以理解了获取弹出的位置的计算方式。 show_loc.png
mPopupWindow.showAsDropDown(View ,x,y)这里的x 是在X 方向的偏移量,y 是在Y方向的偏移量,在图中就是黑色粗线。向上偏移就为负数所以 y 的偏移量为(减去50是为了弹出的时候不被手指遮挡)
int yoff = 0 - (view.getHeight() - (int) y) - mPopupWindow.getHeight() - 50;
还有由于在左边和右边点击时候,window 的弹出动画是不相同的,一个从左下角,一个从右下角,这里需要进行一下判断并设置弹出的动画
(1)在anim 包下创建表示动画的文件
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" >
<scale
android:duration="200"
android:fromXScale="0%"
android:fromYScale="0%"
android:pivotX="0%"
android:pivotY="100%"
android:toXScale="100%"
android:toYScale="100%"/>
</set>
(2)再在styles 中添加 window 的弹出和缩回风格
<style name="AnimationLeft">
<item name="android:windowEnterAnimation">@anim/menu_window_left_enter</item>
<item name="android:windowExitAnimation">@anim/menu_window_left_out</item>
</style>
<style name="AnimationRight">
<item name="android:windowEnterAnimation">@anim/menu_window_right_enter</item>
<item name="android:windowExitAnimation">@anim/menu_window_right_out</item>
</style>
还有一点要注意的是关于 selector 的,在这里也强调一下,如果在selector 中使用了drawable 而这里的drawable 仅仅是颜色的话,在colors 文件中标签 要设置为drawable 而不能是 color .而且selector 只需要设置 state_pressed 这一项就够了。
<!-- item 的点击背景 -->
<drawable name="drawableItemBackground">#fdfdfe</drawable>
<drawable name="drawableItemPressed">#d8d8d8</drawable>
七.添加索引条,并定位到相应的 字母分隔线。
(一)索引的实现在网上也有很多文章,这里的方式也大同小异。
public class IndexBar extends View {
private static final 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","#"};
private Paint mPaint;
private int mHeight;//索引的高度
private int mWidth;//索引条的宽度
private float mLetterHeight ;//绘制的字母的高度
private float mLetterWidth;//绘制的字母的宽度
private int mIndex;//点击的字母的下标
private int mOldIndex;//上一次点击的字母的下标
private TextView mLetterView;//用于绑定中间随索引显示的TextView
private OnLetterChangeListener mLetterChangeListener;
public interface OnLetterChangeListener{
void OnLetterChange(String index);
}
public IndexBar(Context context) {
this(context,null);
}
public IndexBar(Context context, @Nullable AttributeSet attrs) {
this(context,attrs,0);
}
public IndexBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);//设置画笔抗锯齿
mPaint.setColor(Color.parseColor("#FF585858"));
mPaint.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,14f,getResources().getDisplayMetrics()));//设置字体的大小
mPaint.setTypeface(Typeface.DEFAULT);//设置字体的粗细为默认正常粗细
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mHeight = getMeasuredHeight();//获取控件的高度
mWidth = getMeasuredWidth();//获取控件的高度
mLetterHeight = mHeight *1.0f/LETTERS.length;//计算每个字母需要的高度
mLetterWidth = mWidth;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (int i=0;i<LETTERS.length;i++){
String letter = LETTERS[i];
//x 的坐标以View 为坐标系
int x = (int)(mLetterWidth/2.0f-mPaint.measureText(letter)/2.0f);
Rect bounds = new Rect();
mPaint.getTextBounds(letter,0,letter.length(),bounds);
int y = (int)(mLetterHeight/2.0f+bounds.height()/2.0f+mLetterHeight*i);//计算每个字母绘制的y坐标
canvas.drawText(letter,x,y,mPaint);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
//根据点击的位置定位到点击的字母,并实现跳转
mIndex = (int) (event.getY()/mLetterHeight);
//如果点击的位置和之前的不是同一个,跳转到具体的位置
if (mIndex!=mOldIndex&&mIndex>=0&&mIndex<LETTERS.length){
if (mLetterChangeListener!=null){
mLetterChangeListener.OnLetterChange(LETTERS[mIndex]);
mLetterView.setText(LETTERS[mIndex]);
mLetterView.setVisibility(VISIBLE);
this.setBackgroundResource(R.drawable.drawableIndexViewBackground);
}
}
//记录点击的位置
mOldIndex = mIndex;
break;
case MotionEvent.ACTION_MOVE:
mIndex = (int) (event.getY()/mLetterHeight);
if (mIndex!=mOldIndex&&mIndex>=0&&mIndex<LETTERS.length){
if (mLetterChangeListener!=null){
mLetterChangeListener.OnLetterChange(LETTERS[mIndex]);
mLetterView.setText(LETTERS[mIndex]);
mLetterView.setVisibility(VISIBLE);
this.setBackgroundResource(R.drawable.drawableIndexViewBackground);
}
}
mOldIndex = mIndex;
break;
case MotionEvent.ACTION_UP:
mLetterView.setVisibility(INVISIBLE);
this.setBackgroundResource(R.drawable.drawableIndexView);
mOldIndex = -1;
break;
default:
mLetterView.setVisibility(INVISIBLE);
this.setBackgroundResource(R.drawable.drawableIndexView);
break;
}
invalidate();
return true;
}
public void setLetterChangeListener(OnLetterChangeListener letterChangeListener) {
mLetterChangeListener = letterChangeListener;
}
public void setLetterView(TextView letterView) {
mLetterView = letterView;
}
}
(二)定位,并让分类的字母显示在第一项
在RecyclerView 中我们可以看到有这么两个方法
// mRvContracts.scrollToPosition(i);
// mRvContracts.smoothScrollToPosition(i);
名字虽然好听和我们期待的很像,但是这两个方法都不能实现我们想要的效果。对于scrollToPosition 会分为三种情况显示 :
(1)如果i 在可见的第一个item 之前,则i 跳变为可见的第一个 item 。
(2)如果i 在可见的第一个item 之后,在可见的最后一个item 之前,则i 的位置不变。
(3)如果i 在可见的最后一个item 之后,则i 跳变为最后的一个可见的item
这里我也采用了一种在网上看到的方式解决。
private void moveToPosition(int n) {
//先从RecyclerView的LayoutManager中获取第一项和最后一项的Position
int firstItem = mManager.findFirstVisibleItemPosition();
int lastItem = mManager.findLastVisibleItemPosition();
//然后区分情况
if (n <= firstItem) {
//当要置顶的项在当前显示的第一个项的前面时
mRvContracts.scrollToPosition(n);
} else if (n <= lastItem) {
//当要置顶的项已经在屏幕上显示时
int top = mRvContracts.getChildAt(n - firstItem).getTop();
mRvContracts.scrollBy(0, top);
} else {
//当要置顶的项在当前显示的最后一项的后面时
mRvContracts.scrollToPosition(n);
//这里这个变量是用在RecyclerView滚动监听里面的
isMove = true;
}
}
在上面的最后一个else 中我们的i 跳到了最后的一个可见的item ,还需要进行一次滑动。
mRvContracts.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (isMove) {
isMove = false;
//获取要置顶的项在当前屏幕的位置,mIndex是记录的要置顶项在RecyclerView中的位置
int n = mIndex - mManager.findFirstVisibleItemPosition();
if (0 <= n && n < mRvContracts.getChildCount()) {
//获取要置顶的项顶部离RecyclerView顶部的距离
int top = mRvContracts.getChildAt(n).getTop();
//最后的移动
mRvContracts.scrollBy(0, top);
}
}
}
});
所以在索引条的监听接口的实现中,我们做最后一部工作
mIndexBar.setLetterChangeListener(new IndexBar.OnLetterChangeListener() {
@Override
public void OnLetterChange(String index) {
for (int i = 0; i < mContractorEntities.size(); i++) {
if (index.equals(mContractorEntities.get(i).getName())) {
// 没有滚动是因为 要滚动到的位置,已经在屏幕里面了,这时候是不滚动的,
// 只有要滚到的位置没有在屏幕上,才会滚动。
// mManager.scrollToPosition(i);
// mRvContracts.scrollToPosition(i);
// mRvContracts.smoothScrollToPosition(i);
mIndex = i;
moveToPosition(i);
break;
}
}
}
});
就这样一个仿微信的通讯录就完成啦!
Demo 地址:https://github.com/yishengma/WxContracts
[分享一首歌:成全(林宥嘉,刘若英)]