RecyclerView实现顶部悬浮、字母排序、过滤搜索最优雅的方式

来源:互联网 发布:中国为什么网络扫黄 编辑:程序博客网 时间:2024/06/05 08:36

效果:

这里写图片描述
这里写图片描述
这里写图片描述

这篇文章算是之前一篇的升级版,在上一篇的基础上新增了顶部悬停功能、波浪侧边栏和关于多音字的一个处理。
上一篇链接 :
《Android 使用RecyclerView实现(仿微信)的联系人A-Z字母排序和过滤搜索功能》
http://blog.csdn.net/silenceoo/article/details/75661590

主界面布局代码:

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="match_parent"    xmlns:app="http://schemas.android.com/apk/res-auto"    android:orientation="vertical"    android:focusable="true"    android:focusableInTouchMode="true">    <com.xp.wavesidebarrecyclerview.ClearEditText        android:id="@+id/filter_edit"        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:paddingLeft="8dp"        android:background="#bef9b81b"        android:drawableLeft="@drawable/search_bar_icon_normal"        android:hint="请输入关键字"        android:maxLines="1"        android:textSize="15dp" />    <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"            android:layout_width="match_parent"            android:layout_height="match_parent">        </android.support.v7.widget.RecyclerView>        <com.xp.wavesidebar.WaveSideBar            android:id="@+id/sideBar"            android:layout_width="match_parent"            android:layout_height="match_parent"            app:chooseTextColor="@android:color/white"            app:textColor="#969696"            app:backgroundColor="#bef9b81b"            app:textSize="10sp"            app:hintTextSize="32sp"            app:radius="24dp"            app:circleRadius="24dp"/>    </FrameLayout></LinearLayout>

这里的WaveSideBar参考的是:https://github.com/Solartisan/WaveSideBar
WaveSideBar下边的自定义属性也可以不设置,不设置的话就是使用默认值,详细的实现方式可以看源码。

主界面的逻辑代码主要是三个方法:

1、初始化的方法主要是对比较器的初始化,设置监听,对数据排序和对RecyclerView的初始化。

private void initViews() {        mComparator = new PinyinComparator();        mSideBar = (WaveSideBar) findViewById(R.id.sideBar);        //设置右侧SideBar触摸监听        mSideBar.setOnTouchLetterChangeListener(new WaveSideBar.OnTouchLetterChangeListener() {            @Override            public void onLetterChange(String letter) {                //该字母首次出现的位置                int position = mAdapter.getPositionForSection(letter.charAt(0));                if (position != -1) {                    manager.scrollToPositionWithOffset(position, 0);                }            }        });        mRecyclerView = (RecyclerView) findViewById(R.id.rv);        mDateList = filledData(getResources().getStringArray(R.array.date));        // 根据a-z进行排序源数据        Collections.sort(mDateList, mComparator);        //RecyclerView设置manager        manager = new LinearLayoutManager(this);        manager.setOrientation(LinearLayoutManager.VERTICAL);        mRecyclerView.setLayoutManager(manager);        mAdapter = new SortAdapter(this, mDateList);        mRecyclerView.setAdapter(mAdapter);        mDecoration = new TitleItemDecoration(this, mDateList);        //如果add两个,那么按照先后顺序,依次渲染。        mRecyclerView.addItemDecoration(mDecoration);        mRecyclerView.addItemDecoration(new DividerItemDecoration(MainActivity.this, DividerItemDecoration.VERTICAL));        mClearEditText = (ClearEditText) findViewById(R.id.filter_edit);        //根据输入框输入值的改变来过滤搜索        mClearEditText.addTextChangedListener(new TextWatcher() {            @Override            public void onTextChanged(CharSequence s, int start, int before, int count) {                //当输入框里面的值为空,更新为原来的列表,否则为过滤数据列表                filterData(s.toString());            }            @Override            public void beforeTextChanged(CharSequence s, int start, int count,                                          int after) {            }            @Override            public void afterTextChanged(Editable s) {            }        });    }

列表顶部字母索引和悬停的实现使用的是RecyclerView的ItemDecoration。比之前每个item都包含一个字母索引更优雅。

接下来是将数据列表的内容按拼音排序的方法,先将汉字转化成拼音,在用正则表达式分类,下边是代码:

/**     * 为RecyclerView填充数据     *     * @param date     * @return     */    private List<SortModel> filledData(String[] date) {        List<SortModel> mSortList = new ArrayList<>();        for (int i = 0; i < date.length; i++) {            SortModel sortModel = new SortModel();            sortModel.setName(date[i]);            //汉字转换成拼音            String pinyin = PinyinUtils.getPingYin(date[i]);            String sortString = pinyin.substring(0, 1).toUpperCase();            // 正则表达式,判断首字母是否是英文字母            if (sortString.matches("[A-Z]")) {                sortModel.setLetters(sortString.toUpperCase());            } else {                sortModel.setLetters("#");            }            mSortList.add(sortModel);        }        return mSortList;    }

最后就是根据输入的内容进行数据筛选的方法:

/**     * 根据输入框中的值来过滤数据并更新RecyclerView     *     * @param filterStr     */    private void filterData(String filterStr) {        List<SortModel> filterDateList = new ArrayList<>();        if (TextUtils.isEmpty(filterStr)) {            filterDateList = filledData(getResources().getStringArray(R.array.date));        } else {            filterDateList.clear();            for (SortModel sortModel : mDateList) {                String name = sortModel.getName();                if (name.indexOf(filterStr.toString()) != -1 ||                        PinyinUtils.getFirstSpell(name).startsWith(filterStr.toString())                        //不区分大小写                        || PinyinUtils.getFirstSpell(name).toLowerCase().startsWith(filterStr.toString())                        || PinyinUtils.getFirstSpell(name).toUpperCase().startsWith(filterStr.toString())                        ) {                    filterDateList.add(sortModel);                }            }        }        // 根据a-z进行排序        Collections.sort(filterDateList, mComparator);        mDateList.clear();        mDateList.addAll(filterDateList);        mAdapter.notifyDataSetChanged();    }

TitleItemDecoration:

An ItemDecoration allows the application to add a special drawing and layout offset to specific item views from the adapter's data set. This can be useful for drawing dividers between items, highlights, visual grouping boundaries and more.ItemDecoration主要是用来对RecyclerView进行一些修饰,是对adapter数据集中的数据视图增加修饰或空位。经常被用来画分割线、强调效果、可见的分组边界等。

这个类是继承自RecyclerView.ItemDecoration。主要方法:

1、getItemOffsets():绘制间距,绘制标题栏空出间隙。主要逻辑是通过当前view的position判断是否需要在上方空出矩形范围。

@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();        if (position > -1) {            //等于0的时候绘制title            if (position == 0) {                outRect.set(0, mTitleHeight, 0, 0);            } else {                if (null != mData.get(position).getLetters() &&                         !mData.get(position).getLetters().equals(mData.get(position - 1).getLetters())) {                    //字母不为空,并且不等于前一个,绘制title                    outRect.set(0, mTitleHeight, 0, 0);                } else {                    outRect.set(0, 0, 0, 0);                }            }        }    }

2、onDraw():进行标题栏等绘制,即在每组view的上方,即getItemOffset()的区域进行标题栏的绘制

@Override    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {        super.onDraw(c, parent, state);        final int left = parent.getPaddingLeft();        final int right = parent.getWidth() - parent.getPaddingRight();        final int childCount = parent.getChildCount();        for (int i = 0; i < childCount; i++) {            final View child = parent.getChildAt(i);            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child                    .getLayoutParams();            int position = params.getViewLayoutPosition();            if (position > -1) {                if (position == 0) {//等于0的时候绘制title                    drawTitle(c, left, right, child, params, position);                } else {                    if (null != mData.get(position).getLetters() && !mData.get(position)                            .getLetters().equals(mData.get(position - 1).getLetters())) {                        //字母不为空,并且不等于前一个,也要title                        drawTitle(c, left, right, child, params, position);                    }                }            }        }    }

drawTitle():

/**     * 绘制Title区域背景和文字的方法     *最先调用,绘制最下层的title     * @param c     * @param left     * @param right     * @param child     * @param params     * @param position     */    private void drawTitle(Canvas c, int left, int right, View child, RecyclerView.LayoutParams params, int position) {        mPaint.setColor(TITLE_BG_COLOR);        c.drawRect(left, child.getTop() - params.topMargin - mTitleHeight, right, child.getTop() - params.topMargin, mPaint);        mPaint.setColor(TITLE_TEXT_COLOR);        mPaint.getTextBounds(mData.get(position).getLetters(), 0, mData.get(position).getLetters().length(), mBounds);        c.drawText(mData.get(position).getLetters(),                 child.getPaddingLeft(),                 child.getTop() - params.topMargin - (mTitleHeight / 2 - mBounds.height() / 2), mPaint);    }

3、onDrawOver():实现悬浮分组栏,以及悬浮分组栏效果绘制。

对于整个列表的绘制流程,是遵循如下的顺序:
​ ItemDecoration#onDraw() -> ItemView的绘制 -> ItemDecoration#onDrawOver
在onDrawOver中实现可以满足“悬浮”,这个方法里实现了两种效果:一种是下边的字母将上边的顶上去;还有一种是下边的字母直接覆盖上边的字母。

/**     * 最后调用,绘制最上层的title     * @param c     * @param parent     * @param state     */    @Override    public void onDrawOver(Canvas c, final RecyclerView parent, RecyclerView.State state) {        int position = ((LinearLayoutManager) (parent.getLayoutManager())).findFirstVisibleItemPosition();        if (position == -1) return;//在搜索到没有的索引的时候position可能等于-1,所以在这里判断一下        String tag = mData.get(position).getLetters();        View child = parent.findViewHolderForLayoutPosition(position).itemView;        //Canvas是否位移过的标志        boolean flag = false;        if ((position + 1) < mData.size()) {            //当前第一个可见的Item的字母索引,不等于其后一个item的字母索引,说明悬浮的View要切换了            if (null != tag && !tag.equals(mData.get(position + 1).getLetters())) {                //当第一个可见的item在屏幕中剩下的高度小于title的高度时,开始悬浮Title的动画                if (child.getHeight() + child.getTop() < mTitleHeight) {                    c.save();                    flag = true;                    /**                     * 下边的索引把上边的索引顶上去的效果                     */                    c.translate(0, child.getHeight() + child.getTop() - mTitleHeight);                    /**                     * 头部折叠起来的视效(下边的索引慢慢遮住上边的索引)                     */                    /*c.clipRect(parent.getPaddingLeft(),                            parent.getPaddingTop(),                            parent.getRight() - parent.getPaddingRight(),                            parent.getPaddingTop() + child.getHeight() + child.getTop());*/                }            }        }        mPaint.setColor(TITLE_BG_COLOR);        c.drawRect(parent.getPaddingLeft(),                 parent.getPaddingTop(),                 parent.getRight() - parent.getPaddingRight(),                 parent.getPaddingTop() + mTitleHeight, mPaint);        mPaint.setColor(TITLE_TEXT_COLOR);        mPaint.getTextBounds(tag, 0, tag.length(), mBounds);        c.drawText(tag, child.getPaddingLeft(),                parent.getPaddingTop() + mTitleHeight - (mTitleHeight / 2 - mBounds.height() / 2),                mPaint);        if (flag)            c.restore();//恢复画布到之前保存的状态    }

适配器里面的代码很简单,直接上代码吧:

public class SortAdapter extends RecyclerView.Adapter<SortAdapter.ViewHolder> {    private LayoutInflater mInflater;    private List<SortModel> mData;    private Context mContext;    public SortAdapter(Context context, List<SortModel> data) {        mInflater = LayoutInflater.from(context);        mData = data;        this.mContext = context;    }    @Override    public SortAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {        View view = mInflater.inflate(R.layout.item_name, parent,false);        ViewHolder viewHolder = new ViewHolder(view);        viewHolder.tvName = (TextView) view.findViewById(R.id.tvName);        return viewHolder;    }    @Override    public void onBindViewHolder(final SortAdapter.ViewHolder holder, final int position) {        if (mOnItemClickListener != null) {            holder.itemView.setOnClickListener(new View.OnClickListener() {                @Override                public void onClick(View v) {                    mOnItemClickListener.onItemClick(holder.itemView, position);                }            });        }        holder.tvName.setText(this.mData.get(position).getName());        holder.tvName.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View view) {                Toast.makeText(mContext, mData.get(position).getName(),Toast.LENGTH_SHORT).show();            }        });    }    @Override    public int getItemCount() {        return mData.size();    }    //**********************itemClick************************    public interface OnItemClickListener {        void onItemClick(View view, int position);    }    private OnItemClickListener mOnItemClickListener;    public void setOnItemClickListener(OnItemClickListener mOnItemClickListener) {        this.mOnItemClickListener = mOnItemClickListener;    }    //**************************************************************    public static class ViewHolder extends RecyclerView.ViewHolder {        TextView tvName;        public ViewHolder(View itemView) {            super(itemView);        }    }    /**     * 提供给Activity刷新数据     * @param list     */    public void updateList(List<SortModel> list){        this.mData = list;        notifyDataSetChanged();    }    public Object getItem(int position) {        return mData.get(position);    }    /**     * 根据ListView的当前位置获取分类的首字母的char ascii值     */    public int getSectionForPosition(int position) {        return mData.get(position).getLetters().charAt(0);    }    /**     * 根据分类的首字母的Char ascii值获取其第一次出现该首字母的位置     */    public int getPositionForSection(int section) {        for (int i = 0; i < getItemCount(); i++) {            String sortStr = mData.get(i).getLetters();            char firstChar = sortStr.toUpperCase().charAt(0);            if (firstChar == section) {                return i;            }        }        return -1;    }}

PinyinUtils

是一个将中文转化为拼音的工具类,主要提供汉字转拼音的方法和获取首字母的方法,新增了对多音字的处理方法,现在能够获取到所有的多音字的拼音,至于如何显示的问题,就要各位朋友根据需求,做相应的判断:

public class PinyinUtils {    /**     * 获取拼音     *     * @param inputString     * @return     */    public static String getPingYin(String inputString) {        HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat();        format.setCaseType(HanyuPinyinCaseType.LOWERCASE);        format.setToneType(HanyuPinyinToneType.WITHOUT_TONE);        format.setVCharType(HanyuPinyinVCharType.WITH_V);        char[] input = inputString.trim().toCharArray();        String output = "";        try {            for (char curChar : input) {                if (Character.toString(curChar).matches("[\\u4E00-\\u9FA5]+")) {                    String[] temp = PinyinHelper.toHanyuPinyinStringArray(curChar, format);                    output += temp[0];                } else                    output += Character.toString(curChar);            }        } catch (BadHanyuPinyinOutputFormatCombination e) {            e.printStackTrace();        }        return output;    }    /**     * 获取第一个字的拼音首字母     * @param chinese     * @return     */    public static String getFirstSpell(String chinese) {        StringBuffer pinYinBF = new StringBuffer();        char[] arr = chinese.toCharArray();        HanyuPinyinOutputFormat defaultFormat = new HanyuPinyinOutputFormat();        defaultFormat.setCaseType(HanyuPinyinCaseType.LOWERCASE);        defaultFormat.setToneType(HanyuPinyinToneType.WITHOUT_TONE);        for (char curChar : arr) {            if (curChar > 128) {                try {                    String[] temp = PinyinHelper.toHanyuPinyinStringArray(curChar, defaultFormat);                    if (temp != null) {                        pinYinBF.append(temp[0].charAt(0));                    }                } catch (BadHanyuPinyinOutputFormatCombination e) {                    e.printStackTrace();                }            } else {                pinYinBF.append(curChar);            }        }        return pinYinBF.toString().replaceAll("\\W", "").trim();    }    /**     * 汉字转换位汉语拼音首字母,英文字符不变,特殊字符丢失 支持多音字,生成方式如(长沙市长:cssc,zssz,zssc,cssz)     *     * @param chines     *            汉字     * @return 拼音     */    public static String converterToFirstSpell(String chines) {        StringBuffer pinyinName = new StringBuffer();        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 {                    // 取得当前汉字的所有全拼                    String[] str = PinyinHelper.toHanyuPinyinStringArray(                            nameChar[i], defaultFormat);                    if (str != null) {                        for (int j = 0; j < str.length; j++) {                            // 取首字母                            pinyinName.append(str[j].charAt(0));                            if (j != str.length - 1) {                                pinyinName.append(",");                            }                        }                    }                    // else {                    // pinyinName.append(nameChar[i]);                    // }                } catch (BadHanyuPinyinOutputFormatCombination e) {                    e.printStackTrace();                }            } else {                pinyinName.append(nameChar[i]);            }            pinyinName.append(" ");        }        // return pinyinName.toString();        return parseTheChineseByObject(discountTheChinese(pinyinName.toString()));    }    /**     * 汉字转换位汉语全拼,英文字符不变,特殊字符丢失     * 支持多音字,生成方式如(重当参:zhongdangcen,zhongdangcan,chongdangcen     * ,chongdangshen,zhongdangshen,chongdangcan)     *     * @param chines     *            汉字     * @return 拼音     */    public static String converterToSpell(String chines) {        StringBuffer pinyinName = new StringBuffer();        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 {                    // 取得当前汉字的所有全拼                    String[] str = PinyinHelper.toHanyuPinyinStringArray(                            nameChar[i], defaultFormat);                    if (str != null) {                        for (int j = 0; j < str.length; j++) {                            pinyinName.append(str[j]);                            if (j != str.length - 1) {                                pinyinName.append(",");                            }                        }                    }                } catch (BadHanyuPinyinOutputFormatCombination e) {                    e.printStackTrace();                }            } else {                pinyinName.append(nameChar[i]);            }            pinyinName.append(" ");        }        // return pinyinName.toString();        return parseTheChineseByObject(discountTheChinese(pinyinName.toString()));    }    /**     * 去除多音字重复数据     *     * @param theStr     * @return     */    private static List<Map<String, Integer>> discountTheChinese(String theStr) {        // 去除重复拼音后的拼音列表        List<Map<String, Integer>> mapList = new ArrayList<>();        // 用于处理每个字的多音字,去掉重复        Map<String, Integer> onlyOne;        String[] firsts = theStr.split(" ");        // 读出每个汉字的拼音        for (String str : firsts) {            onlyOne = new Hashtable<>();            String[] china = str.split(",");            // 多音字处理            for (String s : china) {                Integer count = onlyOne.get(s);                if (count == null) {                    onlyOne.put(s, new Integer(1));                } else {                    onlyOne.remove(s);                    count++;                    onlyOne.put(s, count);                }            }            mapList.add(onlyOne);        }        return mapList;    }    /**     * 解析并组合拼音,对象合并方案(推荐使用)     *     * @return     */    private static String parseTheChineseByObject(            List<Map<String, Integer>> list) {        Map<String, Integer> first = null; // 用于统计每一次,集合组合数据        // 遍历每一组集合        for (int i = 0; i < list.size(); i++) {            // 每一组集合与上一次组合的Map            Map<String, Integer> temp = new Hashtable<>();            // 第一次循环,first为空            if (first != null) {                // 取出上次组合与此次集合的字符,并保存                for (String s : first.keySet()) {                    for (String s1 : list.get(i).keySet()) {                        String str = s + s1;                        temp.put(str, 1);                    }                }                // 清理上一次组合数据                if (temp != null && temp.size() > 0) {                    first.clear();                }            } else {                for (String s : list.get(i).keySet()) {                    String str = s;                    temp.put(str, 1);                }            }            // 保存组合数据以便下次循环使用            if (temp != null && temp.size() > 0) {                first = temp;            }        }        String returnStr = "";        if (first != null) {            // 遍历取出组合字符串            for (String str : first.keySet()) {                returnStr += (str + ",");            }        }        if (returnStr.length() > 0) {            returnStr = returnStr.substring(0, returnStr.length() - 1);        }        return returnStr;    }}

就介绍到这里吧,代码还是比较多,就不全部贴出来了,有兴趣的朋友可以下载完整代码,欢迎star。

点击下载完整代码

阅读全文
2 0
原创粉丝点击