Android使用RecyclerView实现仿微信联系人列表

现在联系人列表基本都是按照字母或者拼音来进行分类,右边有一排字母供用户快速定位到指定的字母位置,效果图如下:


OK,输入的联系人类型可能有很多种,比如汉字、英文、数字、特殊符号等等,其中汉字会转化成拼音,完后和英文一起进行分类,分类的原则是首字母排序,而数字、特殊符号等,统一放到“#”分类中,下面来看具体的实现。


1. 最右边字母栏的实现

我们先来看最右边的这一排字母,这个布局很简单,从上到下,第一个是一个箭头,用来滑动到联系人列表的顶部,下面依次是26个英文字母,最后是一个#字符,那实现这样一个布局,最简单的方法肯定就是自定义一个LinearLayout了,如果在xml里面一个个画那就太蛋疼了,源码如下:

LetterView.java

public class LetterView extends LinearLayout {
    private Context mContext;
    private CharacterClickListener mListener;

    public LetterView(Context context, AttributeSet attrs) {
        super(context, attrs);

        mContext = context;

        setOrientation(VERTICAL);

        initView();
    }

    private void initView() {
        addView(buildImageLayout());

        for (char i = 'A'; i <= 'Z'; i++) {
            final String character = i + "";
            TextView tv = buildTextLayout(character);

            addView(tv);
        }

        addView(buildTextLayout("#"));
    }

    private TextView buildTextLayout(final String character) {
        LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT, 1);

        TextView tv = new TextView(mContext);
        tv.setLayoutParams(layoutParams);
        tv.setGravity(Gravity.CENTER);
        tv.setClickable(true);

        tv.setText(character);

        tv.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mListener != null) {
                    mListener.clickCharacter(character);
                }
            }
        });
        return tv;
    }

    private ImageView buildImageLayout() {
        LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT, 1);

        ImageView iv = new ImageView(mContext);
        iv.setLayoutParams(layoutParams);

        iv.setBackgroundResource(R.mipmap.arrow);

        iv.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mListener != null) {
                    mListener.clickArrow();
                }
            }
        });
        return iv;
    }

    public void setCharacterListener(CharacterClickListener listener) {
        mListener = listener;
    }

    public interface CharacterClickListener {
        void clickCharacter(String character);

        void clickArrow();
    }
}

首先,在构造函数中,我们声明了setOrientation为VERTICAL,也就是垂直布局,完后在initView中构造我们的布局,第一步先是调用buildImageLayout方法,也就是构造我们最上面的箭头,这里很简单了,不多说,注意LinearLayout.LayoutParams这里的最后一个参数是weight的值,这里和下面都设成1,就能保证我们所有字母和箭头以及#都能平均的分配布局的高度。

这里的CharacterClickListener是我们定义的一个接口,它用来在字母、箭头或#被点击时回调,其中clickCharacter方法是点击字母或#时候的回调,而clickArrow是点击箭头时的回调。完后调用addView方法,就将箭头加入到LinearLayout布局中来。

接下来,一个for循环,将A到Z的26个英文字母都加入,这个和上面类似的,最后加上#,这个最右边一排的布局就算完成了。


2. 主布局的实现

联系人的列表可以有多种实现方式,用ListView,ExpandableListView,RecyclerView甚至自定义都可以实现,我们这里采用相对比较新的RecyclerView来实现,那主布局就很简单了,源码如下:

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.support.v7.widget.RecyclerView
        android:id="@+id/contact_list"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        />

    <com.example.contactview.LetterView
        android:id="@+id/letter_view"
        android:layout_width="30dp"
        android:layout_height="match_parent"
        android:layout_alignParentRight="true"
        />
</RelativeLayout>
一个相对布局,在第一步中定义好的LetterView放在最右边,完后再加上一个RecyclerView就OK


3. MainActivity的实现

MainActivity也比较简单,往RecyclerView的Adapter中填充数据,并定义点击第一步中箭头、字母或#时的回调即可,源码如下:

MainActivity.java

public class MainActivity extends Activity {
    private RecyclerView contactList;
    private String[] contactNames;
    private LinearLayoutManager layoutManager;
    private LetterView letterView;
    private ContactAdapter adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        contactNames = new String[] {"张三丰", "郭靖", "黄蓉", "黄老邪", "赵敏", "123", "天山童姥", "任我行", "于万亭", "陈家洛", "韦小宝", "$6", "穆人清", "陈圆圆", "郭芙", "郭襄", "穆念慈", "东方不败", "梅超风", "林平之", "林远图", "灭绝师太", "段誉", "鸠摩智"};
        contactList = (RecyclerView) findViewById(R.id.contact_list);
        letterView = (LetterView) findViewById(R.id.letter_view);
        layoutManager = new LinearLayoutManager(this);
        adapter = new ContactAdapter(this, contactNames);

        contactList.setLayoutManager(layoutManager);
        contactList.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL_LIST));
        contactList.setAdapter(adapter);

        letterView.setCharacterListener(new LetterView.CharacterClickListener() {
            @Override
            public void clickCharacter(String character) {
                layoutManager.scrollToPositionWithOffset(adapter.getScrollPosition(character), 0);
            }

            @Override
            public void clickArrow() {
                layoutManager.scrollToPositionWithOffset(0, 0);
            }
        });
    }
}

这里我们将要显示的联系人,放在一个字符串数组里面,完后设置Recycler的LayoutManager为LinearLayoutManager,默认方向为垂直,完后设置分割线为DividerItemDecoration,这个在谷歌官方的Sample中已经提供实现了,我们直接copy过来,地址为https://android.googlesource.com/platform/development/+/master/samples/Support7Demos/src/com/example/android/supportv7/widget/decorator/DividerItemDecoration.java,我们简单看下它的实现:

DividerItemDecoration.java

public class DividerItemDecoration extends RecyclerView.ItemDecoration {
    private static final int[] ATTRS = new int[]{
            android.R.attr.listDivider
    };
    public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;
    public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;
    private Drawable mDivider;
    private int mOrientation;
    public DividerItemDecoration(Context context, int orientation) {
        final TypedArray a = context.obtainStyledAttributes(ATTRS);
        mDivider = a.getDrawable(0);
        a.recycle();
        setOrientation(orientation);
    }
    public void setOrientation(int orientation) {
        if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) {
            throw new IllegalArgumentException("invalid orientation");
        }
        mOrientation = orientation;
    }
    @Override
    public void onDraw(Canvas c, RecyclerView parent) {
        if (mOrientation == VERTICAL_LIST) {
            drawVertical(c, parent);
        } else {
            drawHorizontal(c, parent);
        }
    }
    public void drawVertical(Canvas c, RecyclerView parent) {
        final int left = parent.getPaddingLeft();
        final int right = parent.getWidth() - parent.getPaddingRight();
        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = parent.getChildAt(i);
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
                    .getLayoutParams();
            final int top = child.getBottom() + params.bottomMargin +
                    Math.round(ViewCompat.getTranslationY(child));
            final int bottom = top + mDivider.getIntrinsicHeight();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);
        }
    }
    public void drawHorizontal(Canvas c, RecyclerView parent) {
        final int top = parent.getPaddingTop();
        final int bottom = parent.getHeight() - parent.getPaddingBottom();
        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = parent.getChildAt(i);
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
                    .getLayoutParams();
            final int left = child.getRight() + params.rightMargin +
                    Math.round(ViewCompat.getTranslationX(child));
            final int right = left + mDivider.getIntrinsicHeight();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);
        }
    }
    @Override
    public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
        if (mOrientation == VERTICAL_LIST) {
            outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
        } else {
            outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
        }
    }
}
首先在构造函数中,获取一个Drawable对象,这里使用的是android.R.attr.listDivider,这是Android源码里就有的,我们不需要自己来定义,完后根据接收的方向参数,来定义是水平还是垂直方向,在onDraw中,根据方向来调用不同的方法来画分割线,我们这里因为是垂直方向,所以调用的是drawVertical方法。

首先根据parent的属性,获取left和right,因为是垂直方向,所以所有分割线的left和right都是一样的,接下来获取child的数量,完后for循环,在循环中,获取child,完后获取child的LayoutParams,通过LayoutParams,我们就可以计算分割线的top和bottom,分割线是在child的下面的,所以对于分割线的top来说,首先要获取child的bottom位置,完后加上可能存在的bottomMargin,最后加上可能存在的y轴相对偏移量,这就是最终的分割线的top,而分割线的bottom则直接用top加上分割线的高度即可。最后通过setBounds方法来设定分割线的边界,最后画在canvas上面。

drawHorizontal方法是类似的,不再赘述。而最后的getItemOffsets方法,是用来定义child之间的偏移量的,我们来看一段英文:

We need to provide offsets between list items so that we’re not drawing dividers on top of our child views. getItemOffsets is called for each child of your RecyclerView.

所以这个offsets,就是用来定义child之间的间距的,对于垂直方向来说,这个间距就是分割线的高度,第一个child上面间距是为0的,所以这里我们的top为0,而将bottom赋值为分割线的高度。

设置完分割线,我们最后要设置Recycler的adapter对象,这个最后讲,完后就是实现点击箭头、字母或者#时的回调方法,这里我们调用了scrollToPositionWithOffset方法,它的第一个参数是要滑动到Recycler的第几个child,而第二个参数是相对第几个child的top的偏移值,这里我们滑动到指定child,不偏移,所以第二个参数就是0了。


4. Adapter的实现

Adapter是这里要实现的核心部分,观察实现效果,字母和具体的联系人,是两种布局,所以我们的Recycler中,也要定义两种布局,先上代码:

ContactAdapter.java

public class ContactAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    private LayoutInflater mLayoutInflater;
    private Context mContext;
    private String[] mContactNames; // 联系人名称字符串数组
    private List<String> mContactList; // 联系人名称List(转换成拼音)
    private List<Contact> resultList; // 最终结果(包含分组的字母)
    private List<String> characterList; // 字母List

    public enum ITEM_TYPE {
        ITEM_TYPE_CHARACTER,
        ITEM_TYPE_CONTACT
    }

    public ContactAdapter(Context context, String[] contactNames) {
        mContext = context;
        mLayoutInflater = LayoutInflater.from(context);
        mContactNames = contactNames;

        handleContact();
    }

    private void handleContact() {
        mContactList = new ArrayList<>();
        Map<String, String> map = new HashMap<>();

        for (int i = 0; i < mContactNames.length; i++) {
            String pinyin = Utils.getPingYin(mContactNames[i]);
            map.put(pinyin, mContactNames[i]);
            mContactList.add(pinyin);
        }
        Collections.sort(mContactList, new ContactComparator());

        resultList = new ArrayList<>();
        characterList = new ArrayList<>();

        for (int i = 0; i < mContactList.size(); i++) {
            String name = mContactList.get(i);
            String character = (name.charAt(0) + "").toUpperCase(Locale.ENGLISH);
            if (!characterList.contains(character)) {
                if (character.hashCode() >= "A".hashCode() && character.hashCode() <= "Z".hashCode()) { // 是字母
                    characterList.add(character);
                    resultList.add(new Contact(character, ITEM_TYPE.ITEM_TYPE_CHARACTER.ordinal()));
                } else {
                    if (!characterList.contains("#")) {
                        characterList.add("#");
                        resultList.add(new Contact("#", ITEM_TYPE.ITEM_TYPE_CHARACTER.ordinal()));
                    }
                }
            }

            resultList.add(new Contact(map.get(name), ITEM_TYPE.ITEM_TYPE_CONTACT.ordinal()));
        }
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if (viewType == ITEM_TYPE.ITEM_TYPE_CHARACTER.ordinal()) {
            return new CharacterHolder(mLayoutInflater.inflate(R.layout.item_character, parent, false));
        } else {
            return new ContactHolder(mLayoutInflater.inflate(R.layout.item_contact, parent, false));
        }
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        if (holder instanceof CharacterHolder) {
            ((CharacterHolder) holder).mTextView.setText(resultList.get(position).getmName());
        } else if (holder instanceof ContactHolder) {
            ((ContactHolder) holder).mTextView.setText(resultList.get(position).getmName());
        }
    }

    @Override
    public int getItemViewType(int position) {
        return resultList.get(position).getmType();
    }

    @Override
    public int getItemCount() {
        return resultList == null ? 0 : resultList.size();
    }

    public class CharacterHolder extends RecyclerView.ViewHolder {
        TextView mTextView;

        CharacterHolder(View view) {
            super(view);

            mTextView = (TextView) view.findViewById(R.id.character);
        }
    }

    public class ContactHolder extends RecyclerView.ViewHolder {
        TextView mTextView;

        ContactHolder(View view) {
            super(view);

            mTextView = (TextView) view.findViewById(R.id.contact_name);
        }
    }

    public int getScrollPosition(String character) {
        if (characterList.contains(character)) {
            for (int i = 0; i < resultList.size(); i++) {
                if (resultList.get(i).getmName().equals(character)) {
                    return i;
                }
            }
        }

        return -1; // -1不会滑动
    }
}

首先定义了一个枚举,里面有两种类型,ITEM_TYPE_CHARACTER表示字母,ITEM_TYPE_CONTACT表示具体的联系人。完后在handleContact方法里来对联系人进行排序和分类的操作。

将传过来的联系人字符串数组,通过Utils.getPingYin方法进行一次处理,这个方法如下:

Utils.java

public class Utils {
    public static String getPingYin(String inputString) {
        HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat();
        format.setCaseType(HanyuPinyinCaseType.LOWERCASE);
        format.setToneType(HanyuPinyinToneType.WITHOUT_TONE);
        format.setVCharType(HanyuPinyinVCharType.WITH_V);
        char[] input = inputString.trim().toCharArray();
        String output = "";
        try {
            for (char curchar : input) {
                if (java.lang.Character.toString(curchar).matches("[\\u4E00-\\u9FA5]+")) {
                    String[] temp = PinyinHelper.toHanyuPinyinStringArray(curchar, format);
                    output += temp[0];
                } else {
                    output += java.lang.Character.toString(curchar);
                }
            }
        } catch (BadHanyuPinyinOutputFormatCombination e) {
            e.printStackTrace();
        }
        return output;
    }
}

这里用到了一个第三方的jar包,pinyin4j-2.5.0.jar,它可以在中文和拼音之间进行转换,官方网址为: http://pinyin4j.sourceforge.net/,我们这里传过来的联系人可能是中文,为了进行排序和分组,我们必须先将他们转化为拼音,getPingYin就是实现这样一个功能,其中要注意的是,正则表达式"[\\u4E00-\\u9FA5]+",这里是判断是否是中文,是中文我们才进行转换,不是则不处理。

转换后,我们将拼音存入mContactList里,注意,我们同时还要将值存入一个map里,这个map的key是转换后的值,value是转换之前的值,需要这个map是因为我们在联系人列表中最终显示的是中文,而不是拼音,拼音只是用来排序和分组用的。

完后,我们使用一个ContactComparator来对联系人进行排序,源码如下:

ContactComparator.java

public class ContactComparator  implements Comparator<String> {

    @Override
    public int compare(String o1, String o2) {
        int c1 = (o1.charAt(0) + "").toUpperCase().hashCode();
        int c2 = (o2.charAt(0) + "").toUpperCase().hashCode();

        boolean c1Flag = (c1 < "A".hashCode() || c1 > "Z".hashCode()); // 不是字母
        boolean c2Flag = (c2 < "A".hashCode() || c2 > "Z".hashCode()); // 不是字母
        if (c1Flag && !c2Flag) {
            return 1;
        } else if (!c1Flag && c2Flag) {
            return -1;
        }

        return c1 - c2;
    }

}

这是自定义的一个Comparator,首先我们获取首字母,因为排序是按照首字母来排序的,完后转换为大写,这是避免同样的首字母,因为大小写不同而排序,完后获取hashCode,也就是ascii码,比如A的ascii码为65,完后,我们判断是否是字母,这是为了处理特殊情况,比如123,或者特殊字符这样的联系人,这些联系人全部划分到#中,而在效果图中,#分组是在最下面的,所以在Comparator中,它们比所有的字母都要大,最后,如果两个都是字母,则直接通过ascii码值来进行比较即可。

排序完成后,我们就要来进行分组了,首先,我们来定义一个Contact对象,源码如下:

Contact.java

public class Contact implements Serializable {
    private String mName;
    private int mType;

    public Contact(String name, int type) {
        mName = name;
        mType = type;
    }

    public String getmName() {
        return mName;
    }

    public int getmType() {
        return mType;
    }

}

这个实体类很简单,就2个参数,联系人姓名和分类,这个分类就是我们上面定义的枚举中的类型,具体的联系人类型为ITEM_TYPE_CONTACT,而分组的字母以及#的类型为ITEM_TYPE_CHARACTER,注意,mContactList中是不包含字母和#的,所以我们定义了两个ArrayList,其中resultList用来放包括字母和#的最终的结果,而characterList用来放字母和#

循环排序后的mContactList,首先判读characterList中是否有这个字母,如果没有,则在characterList中加入,同时在resultList中加入上面定义的Contact对象,也就是分组的字母对象,而如果不是字母,那说明是特殊字符或者数字这些特殊情况,我们需要判断此时在characterList是否已经有#了,如果没有要加上,当然,也要在resultList中加上#的分组,最后,加上具体的联系人,经过这个操作后,resultList中就是我们最终的结果了。

接下来,在onCreateViewHolder中,我们根据枚举的类型,返回了两种不同的ViewHolder,两种布局文件如下:

item_character.xml

<?xml version="1.0" encoding="utf-8"?>

<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/character"
    android:layout_width="match_parent"
    android:layout_height="40dp"
    android:paddingTop="10dp"
    android:paddingBottom="10dp"
    android:paddingLeft="20dp"
    android:background="#CCC"/>

item_contact.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="wrap_content"
    android:padding="15dp"
    android:gravity="center_vertical">

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@mipmap/ic_launcher"/>

    <TextView
        android:id="@+id/contact_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="15dp"
        android:textSize="20sp"
        android:padding="5dp"
        />
</LinearLayout>
这都很简单,不多说,接下来,在onBindViewHolder方法中,我们就可以根据上面得到的resultList来赋值了。

完后,getItemViewType方法中,我们根据我们定义的枚举类型,来区分不同的布局。至于getItemCount和两个ViewHolder的定义,都很简单,也不说了。

最后,我们提供了一个getScrollPosition方法,这个方法是用来点击最右边的字母栏时进行滑动RecyclerView的,我们根据传进来的参数,也就是点击的具体字母或#,判断在characterList中是否存在,如果不存在,说明这个字母没有对应的联系人,直接返回-1,也就不会滑动RecyclerView,如果存在,则我们找出它具体的位置,也就是position返回,在MainActivity中就可以根据这个position来滑动RecyclerView了。


OK,整个的流程都分析完了,大家如果需要自己扩展,比如点击联系人能修改联系人的备注名称等等,在理解了的基础上都会比较简单了。

源码下载

GitHub项目地址

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值