自定义View实现通讯录和索引联动,如丝般顺滑

在这里插入图片描述

1,右边索引导航我自定义一个View:WordsNavigator.java

package com.txhl.testapp.cus;

import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

import com.txhl.testapp.R;
import com.txhl.testapp.act.MailListActivity;
import com.txhl.testapp.listener.OnWordsChangeListener;
import com.txhl.testapp.utils.ScreenUtils;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * Created by chen.yingjie on 2019/4/29
 * description
 */
public class WordsNavigator extends View {
    private int mRealWidth;
    private int mRealHeight;
    private int mWidth;// 画布宽
    private int mHeight;// 画布高
    private int mEachHeight;// 每个字母平均分配到的占位高,宽和画布一样。
    private int mTouchIndex = 0;

    private Paint wordsPaint;// 字母画笔
    private Paint selectedPaint;// 背景圈画笔
    private Rect mRect;
    private OnWordsChangeListener onShowLetterListener;

    private int colorTrans;
    private int colorNormal;
    private int colorChecked;
    private int colorCheckedBg;

    public void setOnShowLetterListener(OnWordsChangeListener onShowLetterListener) {
        this.onShowLetterListener = onShowLetterListener;
    }

    private List<String> letterLists;

    public WordsNavigator(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        letterLists = new ArrayList<>();

        if (getContext() instanceof MailListActivity) {
            MailListActivity activity = (MailListActivity) getContext();
            activity.setOnRecyclerViewScrollListener(new MailListActivity.OnRecyclerViewScrollListener() {
                @Override
                public void onScroll(String firstWords) {
                    int wordsIndex = letterLists.indexOf(firstWords);
                    mTouchIndex = wordsIndex;
                    invalidate();
                }
            });
        }

        colorTrans = getContext().getResources().getColor(R.color.transparent);
        colorNormal = getContext().getResources().getColor(R.color.black);
        colorChecked = getContext().getResources().getColor(R.color.white);
        colorCheckedBg = getContext().getResources().getColor(R.color.red);

        mRect = new Rect();
        wordsPaint = new Paint();
        wordsPaint.setStrokeWidth(0);
        wordsPaint.setAntiAlias(true);
        wordsPaint.setTextSize(ScreenUtils.dip2px(getContext(), 10));

        selectedPaint = new Paint();
        selectedPaint.setStrokeWidth(0);
        wordsPaint.setAntiAlias(true);
        selectedPaint.setStyle(Paint.Style.FILL);
    }

    public void setLetters(List<String> letterLists) {
        this.letterLists = letterLists;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        final int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        switch (widthMode) {
            case MeasureSpec.UNSPECIFIED:
            case MeasureSpec.AT_MOST:
                break;
            case MeasureSpec.EXACTLY:
                mRealWidth = widthSize;
                break;
        }
        switch (heightMode) {
            case MeasureSpec.UNSPECIFIED:
            case MeasureSpec.AT_MOST:
                break;
            case MeasureSpec.EXACTLY:
                mRealHeight = heightSize;
                break;
        }
        setMeasuredDimension(mRealWidth, mRealHeight);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawColor(colorTrans);
        mWidth = canvas.getWidth();
        mHeight = canvas.getHeight();
        mEachHeight = mHeight / letterLists.size();

        for (int i = 0; i < letterLists.size(); i++) {
            final String _latter = letterLists.get(i);
            wordsPaint.getTextBounds(_latter, 0, 1, mRect);
            final int letterWidth = mRect.width();
            final int letterHeight = mRect.height();

            if (mTouchIndex == i) {
                wordsPaint.setColor(colorChecked);
                selectedPaint.setColor(colorCheckedBg);
            } else {
                wordsPaint.setColor(colorNormal);
                selectedPaint.setColor(colorTrans);
            }
            // 画选中背景圈,x:画笔宽的一半,即画布水平终点;y:每个字母高乘个数减去字母高的一半;radius:半径为画布宽的三分之一。
            canvas.drawCircle(mWidth / 2, mEachHeight * (i + 1) - letterHeight / 2, mWidth / 3, selectedPaint);
            // 画字母,x,y分别为字母的左上角起点,背景圈的圆心取决于字母的起点。
            canvas.drawText(_latter, mWidth / 2 - letterWidth / 2, (i + 1) * mEachHeight, wordsPaint);
        }
    }

    @SuppressLint("ClickableViewAccessibility")
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        final int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                refreshLetterIndex(y);
                break;
            case MotionEvent.ACTION_MOVE:
                refreshLetterIndex(y);
                break;
        }
        return true;
    }

    private void refreshLetterIndex(int y) {
        //y坐标 / 每个字母高度 = 当前字母下标
        int index = y / mEachHeight;
        if (index > letterLists.size() || index < 0) return;
        if (index != mTouchIndex) {
            mTouchIndex = index;
            //回调选中的字母
            if (onShowLetterListener != null) {
                onShowLetterListener.wordsChange(letterLists.get(mTouchIndex));
            }
            invalidate();
        }
    }
}


这里定义了一个接口为了在手指滑动索引的时候通知activity滚动item。

public interface OnWordsChangeListener {
    void wordsChange(String words);
}

2,用RecyclerView来显示列表数据,给它设置滚动监听,当滚动列表的时候要让索引跟着重绘,这里通过LinearLayoutManager找到第一个可见item的position,查出对于的首字母,通过OnRecyclerViewScrollListener接口给到WordsNavigator,让其重绘。
3,让RecyclerView滚动到指定item,用的是它的LayoutManager的startSmoothScroll方法,需要一个Scroller:

public class TopSmoothScroller extends LinearSmoothScroller {

    public TopSmoothScroller(Context context) {
        super(context);
    }
    @Override
    protected int getHorizontalSnapPreference() {
        return SNAP_TO_START;//具体见源码注释
    }
    @Override
    protected int getVerticalSnapPreference() {
        return SNAP_TO_START;//具体见源码注释
    }
}

4,activity

package com.txhl.testapp.act;

import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.support.annotation.NonNull;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;

import com.txhl.testapp.R;
import com.txhl.testapp.cus.TopSmoothScroller;
import com.txhl.testapp.cus.WordsNavigator;
import com.txhl.testapp.cus.widget.SectionListView;
import com.txhl.testapp.listener.OnWordsChangeListener;
import com.txhl.testapp.utils.PinYinUtils;
import com.txhl.testapp.utils.SortUtils;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import butterknife.BindView;
import butterknife.ButterKnife;

public class MailListActivity extends AppCompatActivity {

    private final int CHANGE_WORDS = 1;

    @BindView(R.id.recyclerView)
    RecyclerView recyclerView;
    @BindView(R.id.tvHintWord)
    TextView tvHintWord;
    @BindView(R.id.wordsNavigator)
    WordsNavigator wordsNavigator;

    private String[] names = new String[]{
            "露娜", "李白", "韩信", "太乙真人", "李元芳", "阿珂", "夏侯惇", "关羽", "张飞", "刘备", "貂蝉", "吕布", "王昭君", "武则天",
            "百里守约", "百里玄策", "司马懿", "孙策", "干将莫邪", "裴擒虎", "张良", "诸葛亮", "达摩", "蒙奇", "曹操", "钟馗", "钟无艳",
            "程咬金", "米莱狄", "狄仁杰", "后羿", "大乔", "小乔", "刘邦", "杨玉环", "马可波罗", "狂铁", "苏烈", "赵云", "公孙离", "鬼谷子",
            "成吉思汗", "哪吒", "杨戬", "嬴政", "女娲", "周瑜", "弈星", "扁鹊", "甄姬墨子", "高渐离", "亚瑟", "姜子牙", "宫本武藏",
            "牛魔", "庄周", "蔡文姬", "黄忠", "鲁班七号", "铠", "妲己", "白起", "安其拉", "不知火舞", "芈月", "项羽", "刘禅", "橘右京",
            "兰陵王", "典韦", "元歌", "明世隐", "雅典娜", "娜可露露", "东皇太一", "花木兰", "孙尚香", "孙膑", "虞姬", "孙悟空", "老夫子"
    };

    private List<String> letterLists = new ArrayList<>(Arrays.asList("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 String[] newNames;
    private LinearLayoutManager linearLayoutManager;
    private TopSmoothScroller smoothScroller;
    private MailAdapter mailAdapter;
    private OnRecyclerViewScrollListener onRecyclerViewScrollListener;

    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what) {
                case CHANGE_WORDS:
                    tvHintWord.setVisibility(View.GONE);
                    break;
            }
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_mail_list);
        ButterKnife.bind(this);

        wordsNavigator.setLetters(letterLists);

        wordsNavigator.setOnShowLetterListener(new OnWordsChangeListener() {
            @Override
            public void wordsChange(String words) {
                mHandler.removeMessages(CHANGE_WORDS);
                tvHintWord.setVisibility(View.VISIBLE);
                tvHintWord.setText(words);
                mHandler.sendEmptyMessageDelayed(CHANGE_WORDS, 500);
                scrollToWords(words);
            }
        });

        newNames = SortUtils.sortChar(names);
        linearLayoutManager = new LinearLayoutManager(this);
        smoothScroller = new TopSmoothScroller(this);
        mailAdapter = new MailAdapter();
        recyclerView.setAdapter(mailAdapter);
        recyclerView.setLayoutManager(linearLayoutManager);
        recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);
                RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
                if (layoutManager instanceof LinearLayoutManager) {
                    LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager;
                    int firstVisibleItemPosition = linearLayoutManager.findFirstVisibleItemPosition();
                    String firstWords = PinYinUtils.getFirstWords(names[firstVisibleItemPosition]);
                    onRecyclerViewScrollListener.onScroll(firstWords);
                }
            }
        });
    }

    private void scrollToWords(String words) {
        for (int i = 0; i < newNames.length; i++) {
            String firstWrods = PinYinUtils.getFirstWords(newNames[i]);
            if (words.equals(firstWrods)) {
                // 找到列表中汉字首字母和按下的字母相同的汉字
                smoothScroller.setTargetPosition(i);
                linearLayoutManager.startSmoothScroll(smoothScroller);
                return;
            }
        }
    }

    class MailAdapter extends RecyclerView.Adapter<MailAdapter.MailHolder> {

        @NonNull
        @Override
        public MailHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
            View view = View.inflate(MailListActivity.this, R.layout.item_mail_list, null);
            return new MailHolder(view);
        }

        @Override
        public void onBindViewHolder(MailHolder viewHolder, int position) {
            String lastFirstChar = "";// 上一个首汉字
            String lastFirstWords = "";// 上一个首汉字的首字母
            String firstChar = "";// 当前首汉字
            String firstWords = "";// 当前首汉字的首字母
            firstChar = newNames[position].substring(0, 1);
            firstWords = PinYinUtils.getFirstWords(firstChar);// 多音字,只取第一个字母。
            if (position > 0) {
                lastFirstChar = newNames[position - 1].substring(0, 1);
                lastFirstWords = PinYinUtils.getFirstWords(lastFirstChar);
            }
            if (firstWords.equals(lastFirstWords)) {
                viewHolder.tvWords.setVisibility(View.GONE);
            } else {
                viewHolder.tvWords.setVisibility(View.VISIBLE);
                viewHolder.tvWords.setText(firstWords);
            }
            viewHolder.tvName.setText(newNames[position]);
        }

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

        class MailHolder extends RecyclerView.ViewHolder {
            @BindView(R.id.tvWords)
            TextView tvWords;
            @BindView(R.id.tvName)
            TextView tvName;
            @BindView(R.id.tvMsg)
            TextView tvMsg;
            @BindView(R.id.ivPhoto)
            ImageView ivPhoto;

            public MailHolder(@NonNull View itemView) {
                super(itemView);
                ButterKnife.bind(this, itemView);
            }
        }

    }

    public interface OnRecyclerViewScrollListener {
        void onScroll(String firstWords);
    }

    public void setOnRecyclerViewScrollListener(OnRecyclerViewScrollListener onRecyclerViewScrollListener) {
        this.onRecyclerViewScrollListener = onRecyclerViewScrollListener;
    }
}

用pinyin4j.jar来取首字母,可能有多音字,pinyinName是一个类似(比如重 pinyinName=c,z),这里只取第一个:

public static String getFirstWords(String chines) {
        StringBuffer pinyinName = new StringBuffer();
        char[] nameChar = chines.toCharArray();
        HanyuPinyinOutputFormat defaultFormat = new HanyuPinyinOutputFormat();
        defaultFormat.setCaseType(HanyuPinyinCaseType.UPPERCASE);
        defaultFormat.setToneType(HanyuPinyinToneType.WITHOUT_TONE);
        for (int i = 0; i < nameChar.length; i++) {
            if (nameChar[i] > 128) {
                try {
                    String[] strs = PinyinHelper.toHanyuPinyinStringArray(nameChar[i], defaultFormat);
                    if (strs != null) {
                        for (int j=0; j<strs.length; j++) {
                            pinyinName.append(strs[j].substring(0,1));
                            if (j != strs.length - 1) {
                                pinyinName.append(",");
                            }
                        }
                    }
                } catch (BadHanyuPinyinOutputFormatCombination e) {
                    e.printStackTrace();
                }
            } else {
                pinyinName.append(nameChar[i]);
            }
        }
        return pinyinName.toString().substring(0, 1);
    }

排序方式有很多:

 * 按首字母排序
     * @param strs
     * @return
     */
    public static String[] sortChar(String[] strs) {
        Comparator<Object> com = Collator.getInstance(Locale.CHINA);
        List<String> list = Arrays.asList(strs);
        Collections.sort(list, com);
        return list.toArray(new String[0]);
    }

5,布局
activity_mail_list

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

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

    <com.txhl.testapp.cus.WordsNavigator
        android:id="@+id/wordsNavigator"
        android:layout_marginRight="12dp"
        android:layout_marginTop="100dp"
        android:layout_marginBottom="40dp"
        android:background="@color/transparent"
        android:layout_gravity="right"
        android:layout_width="30dp"
        android:layout_height="match_parent" />

    <TextView
        android:id="@+id/tvHintWord"
        android:text="a"
        android:background="@color/gray"
        android:visibility="gone"
        android:layout_centerInParent="true"
        android:gravity="center"
        android:layout_gravity="center"
        android:textSize="30dp"
        android:textColor="@color/red"
        android:layout_width="60dp"
        android:layout_height="80dp" />

</FrameLayout>

item_mail_list

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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="wrap_content"
    android:orientation="vertical">

    <TextView
        android:id="@+id/tvWords"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="5dp"
        android:textSize="16sp"
        android:background="@color/bg_focus"
        android:text="A"
        android:textColor="@color/blue" />

    <LinearLayout
        android:padding="10dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <ImageView
            android:id="@+id/ivPhoto"
            android:layout_gravity="center_vertical"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@mipmap/ic_launcher_round" />

        <LinearLayout
            android:layout_gravity="center_vertical"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:orientation="vertical">

            <TextView
                android:id="@+id/tvName"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginLeft="5dp"
                android:text=""
                android:textColor="@color/black"
                android:textSize="18sp"/>

            <TextView
                android:id="@+id/tvMsg"
                android:layout_width="wrap_content"
                
                android:layout_height="wrap_content"
                android:layout_marginLeft="5dp"
                android:maxLines="2"
                android:text="我是一个小学生,你不要欺负我,否则我会哭的!"
                android:textColor="@color/gray"
                android:textSize="13sp" />
        </LinearLayout>
    </LinearLayout>
</LinearLayout>

补充:测试发现字母导航器在滑动的时候不跟手,没有那种如丝般顺滑的手感,经过分析原因是,在手指滑动导航器的时候,会触发recyclerView的onScroll监听,这个监听里又不断的告诉导航器你该重绘了,但是如果我们滑动导航器,这时候是不应该再让RecyclerView告诉导航器重绘的,所以导致导航器不断的重绘导致滑动像大鼻涕一样粘稠。故我们在activity里定义一个boolean值来控制是否需要重绘导航器,而且这个值默认为true,否则第一次进页面不会绘制导航器,导致其看不见。

MailListActivity.java

private boolean needInvalidate = true;// 是否需要重绘字母导航

是手指滑动recyclerView的时候需要重绘导航器
recyclerView.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                needInvalidate = true;
                return false;
            }
        });

在导航器滑动导致recyclerView滑动的时候不需要重绘导航器
wordsNavigator.setOnShowLetterListener(new OnWordsChangeListener() {
            @Override
            public void wordsChange(String words) {
                needInvalidate = false;

                mHandler.removeMessages(CHANGE_WORDS);
                tvHintWord.setVisibility(View.VISIBLE);
                tvHintWord.setText(words);
                mHandler.sendEmptyMessageDelayed(CHANGE_WORDS, 500);
                scrollToWords(words);
            }
        });

控制导航器是否重绘
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);
                if (!needInvalidate) return;
                RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
                if (layoutManager instanceof LinearLayoutManager) {
                    LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager;
                    int firstVisibleItemPosition = linearLayoutManager.findFirstVisibleItemPosition();
                    String firstWords = PinYinUtils.getFirstWords(names[firstVisibleItemPosition]);
                    onRecyclerViewScrollListener.onScroll(firstWords);
                }
            }
        });
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值