1. 程式人生 > >RecyclerView實現頂部懸浮、字母排序、過濾搜尋最優雅的方式

RecyclerView實現頂部懸浮、字母排序、過濾搜尋最優雅的方式

效果:

這裡寫圖片描述
這裡寫圖片描述
這裡寫圖片描述

這篇文章算是之前一篇的升級版,在上一篇的基礎上新增了頂部懸停功能、波浪側邊欄和關於多音字的一個處理。
上一篇連結 :
《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。