現在聯繫人列表基本都是按照字母或者拼音來進行分類,右邊有一排字母供用戶快速定位到指定的字母位置,效果圖如下:
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,整個的流程都分析完了,大家如果需要自己擴展,比如點擊聯繫人能修改聯繫人的備註名稱等等,在理解了的基礎上都會比較簡單了。