仿微信联系人、手机通讯录功能

先看效果图,右侧字母导航栏滑动显示,列表中的字母item滑动悬停效果

附有下载链接,可运行:https://download.csdn.net/download/qiushuiduren/89962833?spm=1001.2014.3001.5503

    

项目需要引入依赖

 implementation 'com.github.promeg:tinypinyin:1.0.0'

主页面布局文件也很简单

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <!-- 列表,占满屏幕 -->
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:overScrollMode="never"
        android:scrollbars="none" />

    <!-- 固定头item,浮在recyclerview上层的顶部 -->
    <include layout="@layout/header" />

    <!-- 中间字母提示控件,浮在recyclerview上层的右侧 -->
    <com.contacts.demo.view.CenterTipView
        android:id="@+id/tv_center_tip"
        android:layout_width="70dp"
        android:layout_height="70dp"
        android:layout_centerInParent="true"
        android:gravity="center"
        android:visibility="gone"
        app:bgColor="#90808080"
        app:textColor="#FF0080FF"
        app:textSize="60sp"
        app:type="round" />

    <!-- 右侧索引控件,浮在recyclerview上层的中心 -->
    <com.contacts.demo.view.RightIndexView
        android:id="@+id/vg_right_container"
        android:layout_width="25dp"
        android:layout_height="500dp"
        android:layout_alignParentRight="true"
        android:layout_marginEnd="12dp"
        android:layout_marginTop="80dp"
        app:itemTextColor="#151515"
        app:itemTextSize="4sp"
        app:itemTextTouchBgColor="@android:color/white"
        app:itemTouchBgColor="#151515"
        app:rootBgColor="#11000000"
        app:rootTouchBgColor="#25000000" />
</RelativeLayout>

主要实现逻辑就是自定义右侧的字母导航条(RightIndexView,也可以用RecyclerView或者ListView实现)

public class RightIndexView extends ViewGroup {
    private Context mContext;
    private ArrayList<String> list = new ArrayList<>();

    //自定义属性(item的背景默认透明)
    private int rootBgColor;
    private int rootTouchBgColor;
    private int itemTouchBgColor;
    private int itemTextColor;
    private int itemTextTouchBgColor;
    private int itemTextSize;

    //记录上一次touch的位置
    private int mOldViewIndex;

    //root的宽高
    private int mWidth;
    private int mHeight;
    //需要添加进来的item给定的高度
    private int mItemHeight;

    //root上边和下边的内间距
    private int marginTop = 5;
    private int marginBottom = 5;

    //touch move最小距离
    private int mTouchSlop;
    //记录touch的down与move位置,以便计算移动的距离
    private int yDown;
    private int yMove;

    //touch监听
    private OnRightTouchMoveListener onRightTouchMoveListener;

    public void setOnRightTouchMoveListener(OnRightTouchMoveListener onRightTouchMoveListener) {
        this.onRightTouchMoveListener = onRightTouchMoveListener;
    }

    public RightIndexView(Context context) {
        this(context, null);
    }

    public RightIndexView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public RightIndexView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }

    private void init(Context context, AttributeSet attrs) {
        //获取自定义属性
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.RightIndexView);
        int count = typedArray.getIndexCount();
        for (int i = 0; i < count; i++) {
            int attr = typedArray.getIndex(i);
            switch (attr) {
                case R.styleable.RightIndexView_rootBgColor:
                    //容器的背景颜色,没有则使用指定的默认值
                    rootBgColor = typedArray.getColor(attr, Color.parseColor("#80808080"));
                    break;
                case R.styleable.RightIndexView_rootTouchBgColor:
                    //容器touch时的背景颜色
                    rootTouchBgColor = typedArray.getColor(attr, Color.parseColor("#EE808080"));
                    break;
                case R.styleable.RightIndexView_itemTouchBgColor:
                    //item项的touch时背景颜色(item的背景默认透明)
                    itemTouchBgColor = typedArray.getColor(attr, Color.parseColor("#000000"));
                    break;
                case R.styleable.RightIndexView_itemTextColor:
                    //item的文本颜色
                    itemTextColor = typedArray.getColor(attr, Color.parseColor("#FFFFFF"));
                    break;
                case R.styleable.RightIndexView_itemTextTouchBgColor:
                    //item在touch时的文本颜色
                    itemTextTouchBgColor = typedArray.getColor(attr, Color.parseColor("#FF0000"));
                    break;
                case R.styleable.RightIndexView_itemTextSize:
                    //item的文本字体(默认16)
                    itemTextSize = typedArray.getDimensionPixelSize(attr,
                            (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));
                    break;
            }
        }
        //回收属性数组
        typedArray.recycle();
        this.mContext = context;
        //设置容器默认背景
        setBackgroundColor(rootBgColor);
        //获取系统指定的最小move距离
        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    }

    /**
     * 测量子view和自己的大小
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //获取系统测量的参数
        mWidth = MeasureSpec.getSize(widthMeasureSpec);
        mHeight = MeasureSpec.getSize(heightMeasureSpec);
        //第一个元素的top位置
        int top = 5;
        //item个数
        int size = 0;
        if (list != null && list.size() > 0) {
            //获取子孩子个数
            size = list.size();
            //上下各减去5px,除以个数计算出每个item的应有height
            mItemHeight = (mHeight - marginTop - marginBottom) / size;
        }
        /**
         * 此循环只是测量计算textview的上下左右位置数值,保存在其layoutParams中
         */
        for (int i = 0; i < size; i++) {
            TextView textView = (TextView) getChildAt(i);
            LayoutParams layoutParams = (LayoutParams) textView.getLayoutParams();
            layoutParams.height = mItemHeight;//每个item指定应有的高度
            layoutParams.width = mWidth;//宽度为容器宽度
            layoutParams.top = top;//第一个item距上边5px
            top += mItemHeight;//往后每个item距上边+mItemHeight距离
        }
        /**
         * 由于此例特殊,宽度指定固定值,高度也是占满屏幕,不存在wrap_content情况
         * 故不需要根据子孩子的宽高来改动 mWidth 和 mHeight 的值。故最后直接保存初始计算的值
         */
        setMeasuredDimension(mWidth, mHeight);
    }

    /**
     * 根据计算的子孩子值,在容器中布局排版子孩子
     */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        //获取子孩子个数
        int size = getChildCount();
        for (int i = 0; i < size; i++) {
            //得到特性顺序的子孩子
            TextView textView = (TextView) getChildAt(i);
            //拿到孩子中保存的数据
            LayoutParams layoutParams = (LayoutParams) textView.getLayoutParams();
            //给子孩子布局位置(左,上,右,下)
            textView.layout(0, layoutParams.top, layoutParams.width, layoutParams.top + layoutParams.height);
        }
        /**
         * 结束了 onMeasure 和 onLayout 之后,当前容器的职责完成,onDraw 由子孩子自己画
         */
    }

    public void setData(ArrayList<String> list) {
        if (list == null || list.size() <= 0)
            return;
        int size = list.size();
        this.list.addAll(list);
        for (int i = 0; i < size; i++) {
            addView(list.get(i), i);
        }
        //requestLayout();//重新measure和layout
        //invalidate();//重新draw
        //postInvalidate();//重新draw
    }

    private void addView(String firstPinYin, int position) {
        TextView textView = new TextView(mContext);
        textView.setText(firstPinYin);
        textView.setBackgroundColor(Color.TRANSPARENT);
        textView.setTextColor(itemTextColor);
        textView.setTextSize(itemTextSize);
        textView.setGravity(Gravity.CENTER);
        textView.setTag(position);
        addView(textView, position);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //当前手指位置的y坐标
        int y = (int) event.getY();
        //根据当前的y计算当前所在child的索引位置
        int tempIndex = computeViewIndexByY(y);
        if (tempIndex != -1) {
            //两头我留了点距离,不等于-1就代表没出界
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    yDown = y;
                    drawTextView(mOldViewIndex, false);
                    drawTextView(tempIndex, true);
                    mOldViewIndex = tempIndex;
                    if (onRightTouchMoveListener != null) {
                        onRightTouchMoveListener.showTip(tempIndex, ((TextView) getChildAt(tempIndex)).getText().toString(), true);
                    }
                    //设置root touch bg
                    setBackgroundColor(rootTouchBgColor);
                    break;
                case MotionEvent.ACTION_MOVE:
                    yMove = y;
                    int distance = yDown - yMove;
                    if (Math.abs(distance) > mTouchSlop) {
                        //移动距离超出了一定范围
                        if (mOldViewIndex != tempIndex) {
                            //移动超出了当前元素
                            drawTextView(mOldViewIndex, false);
                            drawTextView(tempIndex, true);
                            mOldViewIndex = tempIndex;
                            setBackgroundColor(rootTouchBgColor);
                            if (onRightTouchMoveListener != null) {
                                onRightTouchMoveListener.showTip(tempIndex, ((TextView) getChildAt(tempIndex)).getText().toString(), true);
                            }
                        }
                    }
                    break;
                case MotionEvent.ACTION_UP:
                    drawTextView(mOldViewIndex, false);
                    drawTextView(tempIndex, false);
                    mOldViewIndex = tempIndex;
                    setBackgroundColor(rootBgColor);
                    if (onRightTouchMoveListener != null) {
                        onRightTouchMoveListener.showTip(tempIndex, ((TextView) getChildAt(tempIndex)).getText().toString(), false);
                    }
                    break;
            }
        } else {
            //出界了,可能是上边或者下边出界,恢复上边的元素
            if (list != null && list.size() > 0) {
                drawTextView(mOldViewIndex, false);
                setBackgroundColor(rootBgColor);
                if (onRightTouchMoveListener != null) {
                    onRightTouchMoveListener.showTip(mOldViewIndex, ((TextView) getChildAt(mOldViewIndex)).getText().toString(), false);
                }
            }
        }
        return true;
    }

    /**
     * 依据y坐标、子孩子的高度和容器总高度计算当前textview的索引值
     */
    private int computeViewIndexByY(int y) {
        int returnValue;
        if (y < marginTop || y > (marginTop + mItemHeight * list.size())) {
            returnValue = -1;
        } else {
            int times = (y - marginTop) / mItemHeight;
            int remainder = (y - marginTop) % mItemHeight;
            if (remainder == 0) {
                returnValue = --times;
            } else {
                returnValue = times;
            }
        }
        return returnValue;
    }

    /**
     * 修改右边索引处touch的样式
     *
     * @param index       position
     * @param isDrawStyle 是否设置特有的色彩样式
     */
    private void drawTextView(int index, boolean isDrawStyle) {
        if (index < 0 || index >= list.size())
            return;
        TextView textView = (TextView) getChildAt(index);
        if (textView == null)
            return;
        if (isDrawStyle) {
            textView.setBackgroundColor(itemTouchBgColor);
            textView.setTextColor(itemTextTouchBgColor);
        } else {
            textView.setBackgroundColor(Color.TRANSPARENT);
            textView.setTextColor(itemTextColor);
        }
    }

    public interface OnRightTouchMoveListener {
        void showTip(int position, String content, boolean isShow);
    }

    /**
     * 必须重写的方法
     *
     * @return
     */
    @Override
    protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
        return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
    }

    @Override
    protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
        return new LayoutParams(p);
    }

    @Override
    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new LayoutParams(getContext(), attrs);
    }

    @Override
    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
        return p instanceof LayoutParams;
    }

    public static class LayoutParams extends ViewGroup.LayoutParams {
        public int left;
        public int top;

        public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);
        }

        public LayoutParams(int width, int height) {
            super(width, height);
        }

        public LayoutParams(ViewGroup.LayoutParams source) {
            super(source);
        }
    }
}

联系人列表的适配器如下

public class ContactAdapter extends RecyclerView.Adapter<ContactAdapter.ContactViewHolder> {
    public static final int SHOW_HEADER_VIEW = 1;   //显示header
    public static final int DISMISS_HEADER_VIEW = 2;//隐藏header
    private ArrayList<Contact> list = new ArrayList<>();
    private OnItemClickListener onItemClickListener;

    public ContactAdapter(ArrayList<Contact> list) {
        this.list = list;
    }

    public void updateData(ArrayList<Contact> list) {
        this.list = list;
        notifyDataSetChanged();
    }

    public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
        this.onItemClickListener = onItemClickListener;
    }

    @Override
    public ContactViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        return new ContactViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item, parent, false));
    }

    @Override
    public void onBindViewHolder(final ContactViewHolder holder, final int position) {
        if (list == null || list.size() <= 0)
            return;
        final Contact contact = list.get(position);
        holder.tvHeader.setText(contact.firstPinYin);
        holder.tvName.setText(contact.name);
        holder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                onItemClickListener.onItemClick(holder.getLayoutPosition(), contact);
            }
        });
        if (position == 0) {
            holder.tvHeader.setText(contact.firstPinYin);
            holder.tvHeader.setVisibility(View.VISIBLE);
        } else {
            if (!TextUtils.equals(contact.firstPinYin, list.get(position - 1).firstPinYin)) {
                holder.tvHeader.setVisibility(View.VISIBLE);
                holder.tvHeader.setText(contact.firstPinYin);
                holder.itemView.setTag(SHOW_HEADER_VIEW);
            } else {
                holder.tvHeader.setVisibility(View.GONE);
                holder.itemView.setTag(DISMISS_HEADER_VIEW);
            }
        }
        holder.itemView.setContentDescription(contact.firstPinYin);
    }

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

    public static class ContactViewHolder extends RecyclerView.ViewHolder {
        public TextView tvHeader;
        public TextView tvName;

        public ContactViewHolder(View itemView) {
            super(itemView);
            tvHeader = (TextView) itemView.findViewById(R.id.tv_header);
            tvName = (TextView) itemView.findViewById(R.id.tv_name);
        }
    }

    public interface OnItemClickListener {
        void onItemClick(int position, Contact contact);
    }
}

MainActivity的代码如下

public class MainActivity extends AppCompatActivity implements RightIndexView.OnRightTouchMoveListener {
    private ArrayList<Contact> list = new ArrayList<>();//列表展示的数据
    private ArrayList<String> firstList = new ArrayList<>();//字母索引集合
    private HashSet<String> set = new HashSet<>();//中间临时集合
    private ContactAdapter adapter;
    private LinearLayoutManager layoutManager;
    private RecyclerView recyclerView;
    private TextView tvHeader;//固定头view
    private CenterTipView tipView;//中间字母提示view
    private RightIndexView rightContainer;//右侧索引view

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

        //初始化数据
        initData();

        recyclerView = (RecyclerView) findViewById(R.id.recyclerView);
        //设置布局
        layoutManager = new LinearLayoutManager(this);
        recyclerView.setLayoutManager(layoutManager);
        //添加分割线
        recyclerView.addItemDecoration(new RecyclerViewDivider(this, LinearLayoutManager.VERTICAL));
        adapter = new ContactAdapter(list);
        recyclerView.setAdapter(adapter);
        //item的点击事件
        adapter.setOnItemClickListener(new ContactAdapter.OnItemClickListener() {
            @Override
            public void onItemClick(int position, Contact contact) {
                Toast.makeText(MainActivity.this, contact.name + " - " + contact.pinYin, Toast.LENGTH_SHORT).show();
            }
        });

        recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                super.onScrollStateChanged(recyclerView, newState);
            }

            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                /**
                 * 查找(width>>1,1)点处的view,差不多是屏幕最上边,距顶部1px
                 * recyclerview上层的header所在的位置
                 */
                View itemView = recyclerView.findChildViewUnder(tvHeader.getMeasuredWidth() >> 1, 1);

                /**
                 * recyclerview中如果有item占据了这个位置,那么header的text就为item的text
                 * 很显然,这个tiem是recyclerview的任意item
                 * 也就是说,recyclerview每滑过一个item,tvHeader就被赋了一次值
                 */
                if (itemView != null && itemView.getContentDescription() != null) {
                    tvHeader.setText(String.valueOf(itemView.getContentDescription()));
                }

                /**
                 * 指定可能印象外层header位置的item范围[-tvHeader.getMeasuredHeight()+1, tvHeader.getMeasuredHeight() + 1]
                 * 得到这个item
                 */
                View transInfoView = recyclerView.findChildViewUnder(
                        tvHeader.getMeasuredWidth() >> 1, tvHeader.getMeasuredHeight() + 1);

                if (transInfoView != null && transInfoView.getTag() != null) {
                    int transViewStatus = (int) transInfoView.getTag();
                    int dealtY = transInfoView.getTop() - tvHeader.getMeasuredHeight();
                    if (transViewStatus == ContactAdapter.SHOW_HEADER_VIEW) {
                        /**
                         * 如果这个item有tag参数,而且是显示header的,正好是我们需要关注的item的header部分
                         */
                        if (transInfoView.getTop() > 0) {
                            //说明item还在屏幕内,只是占据了外层header部分空间
                            tvHeader.setTranslationY(dealtY);
                        } else {
                            //说明item已经超出了recyclerview上边界,故此时外层的header的位置固定不变
                            tvHeader.setTranslationY(0);
                        }
                    } else if (transViewStatus == ContactAdapter.DISMISS_HEADER_VIEW) {
                        //如果此项的header隐藏了,即与外层的header无关,外层的header位置不变
                        tvHeader.setTranslationY(0);
                    }
                }
            }
        });

        //固定头item
        tvHeader = (TextView) findViewById(R.id.tv_header);
        if (list != null && list.size() > 0)
            tvHeader.setText(list.get(0).firstPinYin);

        //center tip view
        tipView = (CenterTipView) findViewById(R.id.tv_center_tip);

        //右侧字母表索引
        rightContainer = (RightIndexView) findViewById(R.id.vg_right_container);
        rightContainer.setData(firstList);
        //右侧字母索引容器注册touch回调
        rightContainer.setOnRightTouchMoveListener(this);
    }

    /**
     * 右侧字母表touch回调
     *
     * @param position 当前touch的位置
     * @param content  当前位置的内容
     * @param isShow   显示与隐藏中间的tip view
     */
    @Override
    public void showTip(int position, final String content, boolean isShow) {
        if (isShow) {
            tipView.setVisibility(View.VISIBLE);
            tipView.setText(content);
        } else {
            tipView.setVisibility(View.INVISIBLE);
        }
        for (int i = 0; i < list.size(); i++) {
            if (list.get(i).firstPinYin.equals(content)) {
                recyclerView.stopScroll();
                int firstItem = layoutManager.findFirstVisibleItemPosition();
                int lastItem = layoutManager.findLastVisibleItemPosition();
                if (i <= firstItem) {
                    recyclerView.scrollToPosition(i);
                } else if (i <= lastItem) {
                    int top = recyclerView.getChildAt(i - firstItem).getTop();
                    recyclerView.scrollBy(0, top);
                } else {
                    recyclerView.scrollToPosition(i);
                }
                break;
            }
        }
    }

    /**
     * 初始化数据。此部分略显复杂
     * #数据不分开处理的话,会默认在list的开头。
     * 微信的在最后!!!
     */
    private void initData() {
        if (list == null)
            list = new ArrayList<>();
        Contact contact = null;

        //这儿使用的是静态数据
        int size = Data.data.length;

        //是否有非字母数据(拼音首字符不在26个字母范围当中)
        boolean hasIncognizance = false;
        //装载非字母数据的结合
        ArrayList<Contact> incognizanceList = new ArrayList<>();
        for (int i = 0; i < size; i++) {
            contact = new Contact();
            contact.name = Data.data[i];
            contact.pinYin = PinYinUtil.toPinYin(contact.name);
            contact.firstPinYin = PinYinUtil.firstPinYin(contact.pinYin);
            if (!TextUtils.isEmpty(contact.firstPinYin)) {
                char first = contact.firstPinYin.charAt(0);
                //A(65), Z(90), a(97), z(122) 根据数据的类型分开装进集合
                if (first < 'A' || (first > 'Z' && first < 'a') || first > 'z') {
                    //非字母
                    contact.firstPinYin = "#";
                    //标记含有#集合
                    hasIncognizance = true;
                    //添加数据到#集合
                    incognizanceList.add(contact);
                } else {
                    //字母索引(set可以去重复)
                    set.add(contact.firstPinYin);
                    //添加数据到字母a-z集合
                    list.add(contact);
                }
            }
        }

        //对contact集合数据排序
        Collections.sort(list);

        //把排序后的字母顺序装进字母索引集合
        Iterator<String> iterator = set.iterator();
        while (iterator.hasNext()) {
            firstList.add(iterator.next());
        }
        Collections.sort(firstList);

        //最后加上#
        if (hasIncognizance) {
            //把#装进索引集合
            firstList.add("#");
            //把非字母的contact数据装进数据集合
            list.addAll(incognizanceList);
        }
        //清空中间缓存集合
        incognizanceList.clear();
        set.clear();
    }
}

感兴趣的朋友可以研究下,希望能有帮助

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值