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項目地址

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章