通讯录粘性头布局

一、效果图展示

无图不BB,先上图

image

二、功能与准备

2.1 功能

  1. 按照拼音顺序对好友进行排序,英文数字符号归为#
  2. 右侧字母导航条,既可拖动也可点击
  3. 粘性头布局
  4. 搜索(全拼+简拼)

2.2 准备

需要导入文字转拼音的库
com.belerweb:pinyin4j:2.5.1'

三、开工

3.1 右侧字母的索引

  1. 字母的绘画
   private static final String[] DEFAULT_INDEX_ITEMS = {"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", "#"};

  @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        String index;
        //y的位置是baseline线的位置 加上mTopMargin
        //循环画上所有的字母
        for (int i = 0; i < mIndexItems.size(); i++) {
            index = mIndexItems.get(i);
            Paint.FontMetrics fm = mPaint.getFontMetrics();
            canvas.drawText(index,
                    (mWidth - mPaint.measureText(index)) / 2,
                    mItemHeight / 2 + (fm.bottom - fm.top) / 2 - fm.bottom + mItemHeight * i + mTopMargin,
                    i == mCurrentIndex ? mTouchedPaint : mPaint);
        }
    }

  1. 字母列表的触摸效果
 @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_MOVE:
                float y = event.getY();
                //得到字母个数
                int indexSize = mIndexItems.size();
                //计算按压的位置
                int touchIndex = (int) (y / mItemHeight);
                //小于0的话那就默认第一个,大于他的个数就是最后一个
                if (touchIndex < 0) {
                    touchIndex = 0;
                } else if (touchIndex >= indexSize) {
                    touchIndex = indexSize - 1;
                }

                if (mOnIndexChangedListener != null && touchIndex >= 0 && touchIndex < indexSize) {

                    if (touchIndex != mCurrentIndex) {
                        mCurrentIndex = touchIndex;
                        if (mCenterTextView!=null){
                            mCenterTextView.setText(mIndexItems.get(touchIndex));
                            mCenterTextView.setVisibility(VISIBLE);
                        }
                        mOnIndexChangedListener.onIndexChanged(mIndexItems.get(touchIndex), touchIndex);
                        invalidate();
                    }

                }

                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:

                if (mCenterTextView!=null){
                    mCenterTextView.setVisibility(VISIBLE);
                }

                mCurrentIndex=-1;

                invalidate();

                break;

        }
        return true;
    }

    //显示居中的字母View
    public SideIndexBar setCenterTextView(TextView view){
        this.mCenterTextView = view;
        return this;
    }

    public SideIndexBar setOnIndexChangedListener(OnIndexTouchedChangedListener listener) {
        this.mOnIndexChangedListener = listener;
        return this;
    }
    //改变位置的接口
    public interface OnIndexTouchedChangedListener {
        void onIndexChanged(String index, int position);
    }

3.2、通讯录分组

  1. 首先先判断是否是同组的第一个
//判断该是否是同组的第一个
    private boolean isFirst(int position) {
        if (mStrings == null) {
            return false;
        }
        if (mStrings.isEmpty()) {
            return false;
        }
        if (position <= 0) {
            return true;
        } else {
            return !mStrings.get(position).getSection().equals(mStrings.get(position - 1).getSection());
        }

    }

  1. 再绘制子Item之间的距离
    //getItemOffsets 可以实现类似于padding的效果
    //
    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        int position = ((RecyclerView.LayoutParams) view.getLayoutParams()).getViewLayoutPosition();
        //mSectionHeight是粘性头部的高度,是同组的第一个就设置top
        if (isFirst(position)) {
            outRect.top = mSectionHeight;
        } else {
            outRect.top = 0;
        }

    }

  1. 绘制每组头部的背景和文字
    //实现类似绘制背景的效果,内容在上面
    //绘制背景和文字
    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDraw(c, parent, state);

        int left = parent.getPaddingLeft();
        int right = parent.getWidth() - parent.getPaddingRight();
        int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = parent.getChildAt(i);
            RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child
                    .getLayoutParams();
            int position = params.getViewLayoutPosition();
            //是第一个就绘制背景以及文字
            if (isFirst(position)) {
                String name = mStrings.get(position).getSection();

                c.drawRect(left, child.getTop() - params.topMargin - mSectionHeight, right, child.getTop() - params.topMargin, mBgPaint);
                Paint.FontMetrics fm = mTextPaint.getFontMetrics();
                c.drawText(name, child.getPaddingLeft(), (child.getTop() - (mSectionHeight / 2 - (fm.descent - fm.ascent) / 2 + fm.descent) - params.topMargin), mTextPaint);
            }
        }

    }

  1. 绘制粘性头部,粘性头部就是滑动范围还在该组时,在最上方显示该组头部,其实就是头部覆盖在内容上。

    //onDrawOver 绘制在内容的上面,覆盖内容
    //这个是实现粘性头部的关键
    @Override
    public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDrawOver(c, parent, state);
        //拿到屏幕上显示的第一个item的位置
        int pos = ((LinearLayoutManager) (parent.getLayoutManager())).findFirstVisibleItemPosition();
        if (pos < 0) return;
        if (mStrings == null || mStrings.isEmpty()) return;
        String section =mStrings.get(pos).getSection();
        View child = parent.findViewHolderForLayoutPosition(pos).itemView;

        boolean flag = false;
        //添加一个平移替换效果
        if ((pos + 1) < mStrings.size()) {
            if (null != section && !section.equals(mStrings.get(pos + 1).getSection())) {
                //如果子item的高度+加距离顶部的距离 小于 section的高度,则进行平移
                if (child.getHeight() + child.getTop() < mSectionHeight) {
                    c.save();
                    flag = true;
                    c.translate(0, child.getHeight() + child.getTop() - mSectionHeight);
                }
            }
        }
        c.drawRect(parent.getPaddingLeft(),
                parent.getPaddingTop(),
                parent.getRight() - parent.getPaddingRight(),
                parent.getPaddingTop() + mSectionHeight, mBgPaint);
        Paint.FontMetrics fm = mTextPaint.getFontMetrics();
        c.drawText(section,
                child.getPaddingLeft(),
                parent.getPaddingTop() + mSectionHeight -(mSectionHeight / 2 - (fm.descent - fm.ascent) / 2 + fm.descent),
                mTextPaint);
        if (flag)
            c.restore();
    }

3.3 数据整理排序

  1. 先看Bean类
public class Star implements Comparable<Star> {

    private String name;
    private String pinyin; //拼音
    private String jianpin;//简拼

    /***
     * 获取悬浮栏文本,(#、定位、热门 需要特殊处理)
     * @return
     */
    public String getSection() {
        String s= pinyin;
        if (TextUtils.isEmpty(s)) {
            return "#";
        } else {
            String c = s.substring(0, 1);
            Pattern p = Pattern.compile("[a-zA-Z]");
            Matcher m = p.matcher(c);
            if (m.matches()) {
                return c.toUpperCase();
            } else {
                return "#";
            }

        }
    }
    //排序 #都往后放
    @Override
    public int compareTo(@NonNull Star o) {
        if (getSection().equals("#")&&!o.getSection().equals("#")){
            return 1;
        }else if (!getSection().equals("#")&&o.getSection().equals("#")){
            return -1;
        }else {
            return getSection().compareToIgnoreCase(o.getSection());
        }
    }

//省略get···set方法
}

  1. 简拼和全拼的获取时通过
 //获取全拼
    public String getPinYi(String chines) {

        sb.setLength(0);
        char[] nameChar = chines.toCharArray();
        HanyuPinyinOutputFormat defaultFormat = new HanyuPinyinOutputFormat();
        defaultFormat.setCaseType(HanyuPinyinCaseType.LOWERCASE);
        defaultFormat.setToneType(HanyuPinyinToneType.WITHOUT_TONE);
        for (int i = 0; i < nameChar.length; i++) {
            if (nameChar[i] > 128) {
                try {
                    sb.append(PinyinHelper.toHanyuPinyinStringArray(nameChar[i], defaultFormat)[0]);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            } else {
                sb.append(nameChar[I]);
            }
        }
        return sb.toString();
    }

    //获取简拼
    public String getPinYinHeadChar(String chines) {

        sb.setLength(0);
        char[] chars = chines.toCharArray();
        HanyuPinyinOutputFormat defaultFormat = new HanyuPinyinOutputFormat();
        defaultFormat.setCaseType(HanyuPinyinCaseType.LOWERCASE);
        defaultFormat.setToneType(HanyuPinyinToneType.WITHOUT_TONE);
        for (int i = 0; i < chars.length; i++) {
            if (chars[i] > 128) {
                try {
                    sb.append(PinyinHelper.toHanyuPinyinStringArray(chars[i], defaultFormat)[0].charAt(0));
                } catch (Exception e) {
                    e.printStackTrace();
                }
            } else {
                sb.append(chars[I]);
            }
        }
        return sb.toString();
    }

3.4 搜索结果处理

    @Override
    public void afterTextChanged(Editable s) {
        String keyword = s.toString();
        if (TextUtils.isEmpty(keyword)) {
            mClearAllBtn.setVisibility(View.GONE);
            mEmptyView.setVisibility(View.GONE);
            mResults = mAllCities;
            ((SectionDividerDecoration) (mRecyclerView.getItemDecorationAt(0))).setData(mResults);
            mAdapter.updateData(mResults);
        } else {
            mClearAllBtn.setVisibility(View.VISIBLE);
            //search是匹配结果,下面显示
            mResults = search(keyword, mAllCities);
            ((SectionDividerDecoration) (mRecyclerView.getItemDecorationAt(0))).setData(mResults);
            if (mResults == null || mResults.isEmpty()) {
                mEmptyView.setVisibility(View.VISIBLE);
            } else {
                mEmptyView.setVisibility(View.GONE);
                mAdapter.updateData(mResults);
            }
        }
        mRecyclerView.scrollToPosition(0);
    }

   public List search(String name, List<Star> list) {
        List results = new ArrayList();

        String patten = Pattern.quote(name);
        Pattern pattern = Pattern.compile(patten, Pattern.CASE_INSENSITIVE);
        for (int i = 0; i < list.size(); i++) {
            //根据拼音
            Matcher matcherPin = pattern.matcher((list.get(i)).getPinyin());
            //根据简拼
            Matcher jianPin = pattern.matcher((list.get(i)).getJianpin());
            //根据名字
            Matcher matcherName = pattern.matcher((list.get(i)).getName());
            if (matcherPin.find() || matcherName.find() || jianPin.find()) {

                results.add(list.get(i));
            }
        }
        return results;
    }

3.5 列表跟随索引移动

在Fragment中实现索引的接口
在实现里写上

  /**
    * 滚动RecyclerView到索引位置
    *
    * @param index
    */
   public void scrollToSection(String index) {
       if (mData == null || mData.isEmpty()) return;
       if (TextUtils.isEmpty(index)) return;
       int size = mData.size();
       for (int i = 0; i < size; i++) {
           if (TextUtils.equals(index.substring(0, 1), mData.get(i).getSection().substring(0, 1))) {
               if (mLayoutManager != null) {
                   mLayoutManager.scrollToPositionWithOffset(i, 0);
                   return;
               }
           }
       }
   }
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容