高仿微信通讯录

2018-03-18  本文已影响106人  海盗的帽子

通讯录是很多社交 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);

                }
            });
        }
    });

为了解释上面这段代码我们画个图更加容易理解,首先看

mPopupWindow.showAsDropDown(View ,0,0)这个方法所表示的 window_loc.png
这可以看到这个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

[分享一首歌:成全(林宥嘉,刘若英)]

上一篇 下一篇

猜你喜欢

热点阅读