先看效果图





Android 仿微信聊天 android仿微信聊天列表界面_User


这里写图片描述


这是比较常见的效果了吧
列表根据首字符的拼音字母来排序,且可以通过侧边栏的字母索引来进行定位

实现这样一个效果并不难,只要自定义一个索引View,然后引入一个可以对汉字进行拼音解析的jar包——pinyin4j-2.5.0即可

首先,先来定义侧边栏控件View,只要直接画出来即可
字母选中项会变为红色,且滑动时背景会变色,此时SideBar并不包含居中的提示文本

public class SideBar extends View {

    private Paint paint = new Paint();

    private int choose = -1;

    private boolean showBackground;

    public static String[] letters = {"#", "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 OnChooseLetterChangedListener onChooseLetterChangedListener;

    public SideBar(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    public SideBar(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public SideBar(Context context) {
        super(context);
    }

    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (showBackground) {
            canvas.drawColor(Color.parseColor("#D9D9D9"));
        }
        int height = getHeight();
        int width = getWidth();
        //平均每个字母占的高度
        int singleHeight = height / letters.length;
        for (int i = 0; i < letters.length; i++) {
            paint.setColor(Color.BLACK);
            paint.setAntiAlias(true);
            paint.setTextSize(25);
            if (i == choose) {
                paint.setColor(Color.parseColor("#FF2828"));
                paint.setFakeBoldText(true);
            }
            float x = width / 2 - paint.measureText(letters[i]) / 2;
            float y = singleHeight * i + singleHeight;
            canvas.drawText(letters[i], x, y, paint);
            paint.reset();
        }
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        int action = event.getAction();
        float y = event.getY();
        int oldChoose = choose;
        int c = (int) (y / getHeight() * letters.length);
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                showBackground = true;
                if (oldChoose != c && onChooseLetterChangedListener != null) {
                    if (c > -1 && c < letters.length) {
                        onChooseLetterChangedListener.onChooseLetter(letters[c]);
                        choose = c;
                        invalidate();
                    }
                }
                break;
            case MotionEvent.ACTION_MOVE:
                if (oldChoose != c && onChooseLetterChangedListener != null) {
                    if (c > -1 && c < letters.length) {
                        onChooseLetterChangedListener.onChooseLetter(letters[c]);
                        choose = c;
                        invalidate();
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
                showBackground = false;
                choose = -1;
                if (onChooseLetterChangedListener != null) {
                    onChooseLetterChangedListener.onNoChooseLetter();
                }
                invalidate();
                break;
        }
        return true;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return super.onTouchEvent(event);
    }

    public void setOnTouchingLetterChangedListener(OnChooseLetterChangedListener onChooseLetterChangedListener) {
        this.onChooseLetterChangedListener = onChooseLetterChangedListener;
    }

    public interface OnChooseLetterChangedListener {

        void onChooseLetter(String s);

        void onNoChooseLetter();

    }

}

SideBar只是画出了侧边栏索引条而已,不包含居中的提示文本,这个在另一个布局添加即可

public class HintSideBar extends RelativeLayout implements SideBar.OnChooseLetterChangedListener {

    private TextView tv_hint;

    private SideBar.OnChooseLetterChangedListener onChooseLetterChangedListener;

    public HintSideBar(Context context, AttributeSet attrs) {
        super(context, attrs);
        LayoutInflater.from(context).inflate(R.layout.view_hint_side_bar, this);
        initView();
    }

    private void initView() {
        SideBar sideBar = (SideBar) findViewById(R.id.sideBar);
        tv_hint = (TextView) findViewById(R.id.tv_hint);
        sideBar.setOnTouchingLetterChangedListener(this);
    }

    @Override
    public void onChooseLetter(String s) {
        tv_hint.setText(s);
        tv_hint.setVisibility(VISIBLE);
        if (onChooseLetterChangedListener != null) {
            onChooseLetterChangedListener.onChooseLetter(s);
        }
    }

    @Override
    public void onNoChooseLetter() {
        tv_hint.setVisibility(INVISIBLE);
        if (onChooseLetterChangedListener != null) {
            onChooseLetterChangedListener.onNoChooseLetter();
        }
    }

    public void setOnChooseLetterChangedListener(SideBar.OnChooseLetterChangedListener onChooseLetterChangedListener) {
        this.onChooseLetterChangedListener = onChooseLetterChangedListener;
    }
}

HintSideBar通过回调接口来更新居中TextView的文本内容和可见性

使用到的布局

<?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">

    <com.czy.demo.SideBar
        android:id="@+id/sideBar"
        android:layout_width="30dp"
        android:layout_height="match_parent"
        android:layout_alignParentRight="true" />

    <TextView
        android:id="@+id/tv_hint"
        android:layout_width="70dp"
        android:layout_height="70dp"
        android:layout_centerInParent="true"
        android:background="#4b0e0e0e"
        android:gravity="center"
        android:textColor="#ffffff"
        android:textSize="30sp"
        android:visibility="invisible" />

</RelativeLayout>

此时就完成了索引View的绘制,不过定位功能还需要再通过回调接口来完成

引入jar包后,先来设定一个工具类,包含一个可以解析字符串的方法,返回值为首字符对应的拼音首字母或者为包含一个空格的char类型数据

public class Utils {

    /**
     * 如果字符串的首字符为汉字,则返回该汉字的拼音大写首字母
     * 如果字符串的首字符为字母,也转化为大写字母返回
     * 其他情况均返回' '
     *
     * @param str 字符串
     * @return 首字母
     */
    public static char getHeadChar(String str) {
        if (str != null && str.trim().length() != 0) {
            char[] strChar = str.toCharArray();
            char headChar = strChar[0];
            //如果是大写字母则直接返回
            if (Character.isUpperCase(headChar)) {
                return headChar;
            } else if (Character.isLowerCase(headChar)) {
                return Character.toUpperCase(headChar);
            }
            // 汉语拼音格式输出类
            HanyuPinyinOutputFormat hanYuPinOutputFormat = new HanyuPinyinOutputFormat();
            hanYuPinOutputFormat.setCaseType(UPPERCASE);
            hanYuPinOutputFormat.setToneType(WITHOUT_TONE);
            if (String.valueOf(headChar).matches("[\\u4E00-\\u9FA5]+")) {
                try {
                    String[] stringArray = PinyinHelper.toHanyuPinyinStringArray(headChar, hanYuPinOutputFormat);
                    if (stringArray != null && stringArray[0] != null) {
                        return stringArray[0].charAt(0);
                    }
                } catch (BadHanyuPinyinOutputFormatCombination e) {
                    return ' ';
                }
            }
        }
        return ' ';
    }

}

然后再定义一个实体类,包含用户名,电话,用户名首字符的拼音首字母等三个属性
需要实现Comparable 接口,用于排序

public class User implements Comparable {

    private String userName;

    private String phone;

    private char headLetter;

    public User(String userName, String phone) {
        this.userName = userName;
        this.phone = phone;
        headLetter = Utils.getHeadChar(userName);
    }

    public String getUserName() {
        return userName;
    }

    public String getPhone() {
        return phone;
    }

    public char getHeadLetter() {
        return headLetter;
    }

    @Override
    public boolean equals(Object object) {
        if (this == object) {
            return true;
        }
        if (object == null || getClass() != object.getClass()) {
            return false;
        }
        User that = (User) object;
        return getUserName().equals(that.getUserName()) && getPhone().equals(that.getPhone());
    }

    @Override
    public int compareTo(Object object) {
        if (object instanceof User) {
            User that = (User) object;
            if (getHeadLetter() == ' ') {
                if (that.getHeadLetter() == ' ') {
                    return 0;
                }
                return -1;
            }
            if (that.getHeadLetter() == ' ') {
                return 1;
            } else if (that.getHeadLetter() > getHeadLetter()) {
                return -1;
            } else if (that.getHeadLetter() == getHeadLetter()) {
                return 0;
            }
            return 1;
        } else {
            throw new ClassCastException();
        }
    }

}

主布局文件如下

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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/rv_userList"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <com.czy.demo.HintSideBar
        android:id="@+id/hintSideBar"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="right" />

</FrameLayout>

联系人列表使用的是RecyclerView,还需要定义一个Adapter

public class UserAdapter extends RecyclerView.Adapter<UserAdapter.UserHolder> {

    private List<User> userList;

    private LayoutInflater inflater;

    public UserAdapter(Context context) {
        inflater = LayoutInflater.from(context);
        userList = new ArrayList<>();
    }

    @Override
    public UserHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = inflater.inflate(R.layout.item_user, parent, false);
        return new UserHolder(view);
    }

    @Override
    public void onBindViewHolder(UserHolder holder, int position) {
        holder.tv_userName.setText(userList.get(position).getUserName());
        holder.tv_phone.setText(userList.get(position).getPhone());
    }

    public void setData(List<User> userList) {
        this.userList.clear();
        this.userList = userList;
    }

    public int getFirstPositionByChar(char sign) {
        if (sign == '#') {
            return 0;
        }
        for (int i = 0; i < userList.size(); i++) {
            if (userList.get(i).getHeadLetter() == sign) {
                return i;
            }
        }
        return -1;
    }

    @Override
    public int getItemCount() {
        return userList.size();
    }

    class UserHolder extends RecyclerView.ViewHolder {

        public TextView tv_userName;

        public TextView tv_phone;

        public UserHolder(View itemView) {
            super(itemView);
            tv_userName = (TextView) itemView.findViewById(R.id.tv_userName);
            tv_phone = (TextView) itemView.findViewById(R.id.tv_phone);
        }
    }

}

以下方法用于获取联系人列表中第一个首字符为sign的item的位置

public int getFirstPositionByChar(char sign)

主Activity代码如下

public class MainActivity extends AppCompatActivity implements SideBar.OnChooseLetterChangedListener {

    private List<User> userList;

    private UserAdapter adapter;

    private RecyclerView rv_userList;

    private LinearLayoutManager manager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        supportRequestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.activity_main);
        HintSideBar hintSideBar = (HintSideBar) findViewById(R.id.hintSideBar);
        rv_userList = (RecyclerView) findViewById(R.id.rv_userList);
        hintSideBar.setOnChooseLetterChangedListener(this);
        manager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
        rv_userList.setLayoutManager(manager);
        userList = new ArrayList<>();
        adapter = new UserAdapter(this);
        initData();
        adapter.setData(userList);
        rv_userList.setAdapter(adapter);
    }

    @Override
    public void onChooseLetter(String s) {
        int i = adapter.getFirstPositionByChar(s.charAt(0));
        if (i == -1) {
            return;
        }
        manager.scrollToPositionWithOffset(i, 0);
    }

    @Override
    public void onNoChooseLetter() {

    }
}

initData()用于向Adapter填充数据

public void initData() {
        User user1 = new User("陈", "12345678");
        User user2 = new User("赵", "12345678");
        ...
        userList.add(user1);
        userList.add(user2);
        ...
        Collections.sort(userList);
        adapter.notifyDataSetChanged();
    }

这样,整个效果就都完成了

代码我已上传到GitHub——HintSideBar