自定义 View 之联系人字母索引及定位效果

博主声明:

转载请在开头附加本文链接及作者信息,并标记为转载。本文由博主 威威喵 原创,请多支持与指教。

本文首发于此   博主威威喵  |  博客主页https://blog.csdn.net/smile_running

    我们来实现这样的一个小Demo,联系人的快速检索功能,左侧带有字母快速定位,滑动列表时字母相继切换的效果。那么我们来看看实现的Demo,显而易见,这是使用列表控件,而我使用的是ListView控件。至于ListView的一些用法,您可以看这博主的这篇文章ListView使用技巧、优化和用法拓展,掌握ListView,这篇文章我们就不说明它的使用姿势了

效果图

《自定义 View 之联系人字母索引及定位效果》

    想来想去,我就搞了个王者荣耀的英雄名称来作为我们的联系人。因为模拟器上也没有联系人,也懒得搞,手机的话这个真实的联系人也就不放出来,这也是个人隐私嘛。搞这么多英雄的名字还挺累的,大家将就着看,其实原理都一样。所以,这不是我们的关键,重要的是如何实现这种效果呢?

  • 思路及实现

一、左侧字母检索器

       左边是一个自定义View,通过获得屏幕的高度,将26个字母逐一从上往下绘制。通过点击事件绘制不同的颜色,再通过监听右边列表的滑动检索字母选择器中的相对应字母下标,然后绘制改变字母的颜色。

二、列表显示字母分类

       列表添加数据,主要说一下头部的字母分类实现。通过获取汉字的原拼音,再检索出首字母。在适配器中判断当前字母是否与前一个字母相同。相同则归类,不相同则分类。

三、中间的字母提示

        这个应该是最简单的了,在我们的自定义View里通过接口回调获取点击或滑动切换时所显示的字母,然后在设置到一个TextView上,通过定时器将它延迟隐藏。

    大致思路就是这样了,其实自己使劲的想一想,还是可以实现出这样的效果的,也不是特别难。思路讲完了,那么我们就上代码吧,由于代码比较多,所以这里只贴比较核心的地方来看看,若有需要请到我的Github上查看完整的项目。

一、左侧字母检索器 

/**
 * @Created by xww.
 * @Creation time 2018/8/18.
 */

public class ContactIndexView extends View {
    private int mRealWidth;
    private int mRealHeight;
    private int mWidth;
    private int mHeight;
    private int mEachHeight;

    private int mTouchIndex = 0;

    private Paint mPaint;
    private Rect mRect;
    private onShowLetterListener onShowLetterListener = null;

    private int colorBackground;
    private int colorNormal;
    private int colorChecked;

    public interface onShowLetterListener {
        void showLatter(String letter);
    }

    public void setOnShowLetter(onShowLetterListener showLetterListener) {
        this.onShowLetterListener = showLetterListener;
    }

    private String[] letters = new String[]{"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"};

    @TargetApi(Build.VERSION_CODES.M)
    private void init() {
        EventBus.getDefault().register(this);
        colorBackground = getContext().getResources().getColor(R.color.colorDivider, null);
        colorNormal = getContext().getResources().getColor(R.color.colorWhite, null);
        colorChecked = getContext().getResources().getColor(R.color.colorPrimaryDark, null);

        mRect = new Rect();
        mPaint = new Paint();
        mPaint.setStrokeWidth(3f);
        mPaint.setAntiAlias(true);
        mPaint.setTextSize(38f);
    }

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

    @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(colorBackground);
        mWidth = canvas.getWidth();
        mHeight = canvas.getHeight() - getStatusbarHeight();
        mEachHeight = mHeight / letters.length;

        for (int i = 0; i < letters.length; i++) {
            final String _latter = letters[i];
            mPaint.getTextBounds(_latter, 0, 1, mRect);
            final int letterWidth = mRect.width();
            final int letterHeight = mRect.height();

            if (mTouchIndex == i) {
                mPaint.setColor(colorChecked);
            } else {
                mPaint.setColor(colorNormal);
            }
            canvas.drawText(_latter, mWidth / 2 - letterWidth / 2, (i + 1) * mEachHeight - letterHeight / 2, mPaint);
        }
    }

    @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;
    }


    @Subscribe
    public void onListScrollEvent(ScrollEvent event) {
        if (event.isLast()) {
            return;
        }
        for (int i = 0; i < letters.length; i++) {
            if (event.getLetter().equals(letters[i])) {
                mTouchIndex = i;
                invalidate();
                return;
            }
        }
    }

    private void refreshLetterIndex(int y) {
        //y坐标 / 每个字母高度 = 当前字母下标
        int index = y / mEachHeight;
        if (index != mTouchIndex) {
            mTouchIndex = index;
            //回调选中的字母
            if (onShowLetterListener != null) {
                onShowLetterListener.showLatter(letters[mTouchIndex]);
            }
            invalidate();
        }
    }

    private int getStatusbarHeight() {
        int resId = getContext().getResources().getIdentifier("status_bar_height", "dimen", "android");
        return resId > 0 ? getContext().getResources().getDimensionPixelSize(resId) : 0;
    }
}

    代码挺长的,最重要的是计算字母所在的x,y坐标,坐标计算准了,才可以绘制到控件的中心去,否则你会发现字母将会偏右或偏左等,显示效果比较难看。

            final String _latter = letters[i];
            mPaint.getTextBounds(_latter, 0, 1, mRect);
            final int letterWidth = mRect.width();
            final int letterHeight = mRect.height();

    这段代码片段,它主要是给文本绘制了一个边框,然后我们从边框的宽、高间接得到文本的宽和高。

二、列表显示字母分类

/**
 * @Created by xww.
 * @Creation time 2018/8/18.
 */

public class MainActivity extends AppCompatActivity {

    private ListView mListView;
    private ContactIndexView mIndexView;
    private TextView mShowTextView;

    private CountDownTimer mCountDownTimer;
    private ContactAdapter mAdapter;
    private ArrayList<Contact> mContacts;
    private String mLetter;

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

    @SuppressWarnings("unchecked")
    private <T extends View> T $(int id) {
        View view = findViewById(id);
        return (T) view;
    }

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

    private void initView() {
        mListView = $(R.id.list_contact);
        mIndexView = $(R.id.view_contact);
        mShowTextView = $(R.id.tv_show_letter);

        initData();
        initListener();
    }

    private void initData() {
        mContacts = new ArrayList<>();

        for (String name : NAME) {
            mContacts.add(new Contact(name));
        }

        Collections.sort(mContacts, (o1, o2) -> o1.getPinyin().compareTo(o2.getPinyin()));
        mAdapter = new ContactAdapter(mContacts);
        mListView.setAdapter(mAdapter);
    }

    @SuppressWarnings("UnusedAssignment")
    private void initListener() {
        mIndexView.setOnShowLetter(letter -> {

            mShowTextView.setText(letter);
            mLetter = letter;

            if (mCountDownTimer == null) {
                mCountDownTimer = new CountDownTimer(1500, 500) {
                    @Override
                    public void onTick(long millisUntilFinished) {
                        mShowTextView.setVisibility(View.VISIBLE);
                    }

                    @Override
                    public void onFinish() {
                        mShowTextView.setVisibility(View.INVISIBLE);
                        if (mCountDownTimer != null) {
                            mCountDownTimer.cancel();
                            mCountDownTimer = null;
                        }
                    }
                }.start();
            } else {
                mCountDownTimer.start();
            }

            for (int i = 0; i < mContacts.size(); i++) {
                final String letterName = mContacts.get(i).getPinyin().substring(0, 1);
                if (letterName.equals(mLetter)) {
                    mListView.setSelection(i);
                    return;
                }
            }
        });

        mListView.setOnScrollListener(new AbsListView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(AbsListView view, int scrollState) {}

            @Override
            public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
                String letter = mContacts.get(firstVisibleItem).getPinyin().substring(0, 1);
                boolean isLast = false;
                if (firstVisibleItem + visibleItemCount == totalItemCount) {
                    isLast = true;
                }
                EventBus.getDefault().post(new ScrollEvent(letter, isLast));
            }
        });
    }
}

    我们的Activity没什么好说的,就是添加数据和一些监听事件。主要注意列表的滑动监听,我们要判断是否已经滑动到了最底部,如果在列表最底部,我们不再处理滑动监听事件。其实,我之前用的是自己随机组的一些名字,发现有点影响美观,所以就换了。

三、列表适配器的逻辑代码

/**
 * @Created by xww.
 * @Creation time 2018/8/18.
 */

public class ContactAdapter extends BaseAdapter {

    private ArrayList<Contact> mContacts;

    ContactAdapter(ArrayList<Contact> contacts) {
        this.mContacts = contacts;
    }

    @Override
    public int getCount() {
        return mContacts == null ? 0 : mContacts.size();
    }

    @Override
    public Object getItem(int position) {
        return mContacts.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder holder;
        if (convertView == null) {
            convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.adapter_contact, parent, false);
            holder = new ViewHolder(convertView);
            convertView.setTag(holder);
        } else {
            holder = (ViewHolder) convertView.getTag();
        }

        final String name = mContacts.get(position).getName();
        final String pinyin = mContacts.get(position).getPinyin().substring(0, 1);
        if (position == 0) {
            holder.tvPinYin.setVisibility(View.VISIBLE);
        } else {
            final String prePinyin = mContacts.get(position - 1).getPinyin().substring(0, 1);
            if (pinyin.equals(prePinyin)) {
                holder.tvPinYin.setVisibility(View.GONE);
            } else {
                holder.tvPinYin.setVisibility(View.VISIBLE);
            }
        }

        holder.tvName.setText(name);
        holder.tvPinYin.setText(pinyin);
        return convertView;
    }

    private static class ViewHolder {
        private TextView tvName;
        private TextView tvPinYin;

        ViewHolder(View itemView) {
            tvName = itemView.findViewById(R.id.tv_name);
            tvPinYin = itemView.findViewById(R.id.tv_pinyin);
        }
    }
}

    我们看关键代码:

        if (position == 0) {
            holder.tvPinYin.setVisibility(View.VISIBLE);
        } else {
            final String prePinyin = mContacts.get(position - 1).getPinyin().substring(0, 1);
            if (pinyin.equals(prePinyin)) {
                holder.tvPinYin.setVisibility(View.GONE);
            } else {
                holder.tvPinYin.setVisibility(View.VISIBLE);
            }
        }

    这段是实现列表头部的字母分类的,我们知道第一个字母永远不会重复,而从第二个字母开始,我们判断是否与前一个字母一样来决定字母的显示与否。

四、汉字转拼音的一个工具类

/**
 * @Created by xww.
 * @Creation time 2018/8/18.
 */

final class PinYinUtils {
    public static String getPinYin(String hanzi) {
        StringBuilder pinyin = new StringBuilder();

        HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat();
        format.setCaseType(HanyuPinyinCaseType.UPPERCASE);
        format.setToneType(HanyuPinyinToneType.WITHOUT_TONE);

        char[] arr = hanzi.toCharArray();
        for (char anArr : arr) {
            if (Character.isWhitespace(anArr)) continue;

            if (anArr > 127) {
                try {
                    String[] pinyinArr = PinyinHelper.toHanyuPinyinStringArray(anArr, format);

                    if (pinyinArr != null) {
                        pinyin.append(pinyinArr[0]);
                    } else {
                        pinyin.append(anArr);
                    }
                } catch (BadHanyuPinyinOutputFormatCombination e) {
                    e.printStackTrace();
                    pinyin.append(anArr);
                }
            } else {
                pinyin.append(anArr);
            }
        }
        return pinyin.toString();
    }
}

    这个工具栏比较固定 ,那么所有的关键是的代码基本就这些了。这是我个人写的一个联系人的Demo,个人认为界面还不错,哈哈。

    原文作者:威威貓
    原文地址: https://blog.csdn.net/smile_Running/article/details/81813069
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞