1. 程式人生 > >RecyclerView自定義目錄快速索引

RecyclerView自定義目錄快速索引

快速索引是眾多app中常用的功能,在及時通訊、使用者列表等功能中能夠快速定位,在此將我專案中使用到的索引抽取出來交流分享,效果如下:

一、繪製IndexBar

先自定義IndexBar繼承View;

public class IndexBar extends View {
  

    public IndexBar(Context context) {
        super(context);
   
    }

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

    public IndexBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
      
    }
}

定義所需的屬性:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="IndexrecyclerviewIndexBar">
        <!--index文字大小-->
        <attr name="indexTextSize" format="dimension"></attr>
        <!--index按下的顏色-->
        <attr name="pressBackground" format="color|reference"></attr>
        <!--index按下時文字的顏色-->
        <attr name="pressTextColor" format="color|reference"></attr>
        <!--index文字的顏色-->
        <attr name="indexTextColor" format="color|reference"></attr>
        <!--index文字選中的顏色-->
        <attr name="selectTextColor" format="color|reference"></attr>
      </declare-styleable>
</resources>

修改完善IndexBar;

public class IndexBar extends View {
 
    private Context mContext;
    /**
     * 預設索引
     */
    private static final String[] DEFAULT_INDEX = 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", "#"};

    /**
     * 每個index的高度
     */
    private int indexHeght;
    /**
     * view寬度
     */
    private int mWidth;
    /**
     * view高度
     */
    private int mHeight;
    /**
     * 畫筆
     */
    private Paint mPaint;
    /**
     * 按下時的背景顏色
     */
    private int mPressBackground;
    /**
     * 文字顏色
     */
    private int mTextColor;
    /**
     * 按下時文字的顏色
     */
    private int mPressTextColor;
    /**
     * 文字選中的顏色
     */
    private int mSelectTextColor;

    /**
     * 字型大小
     */
    private int textSize;

    private int DEFAULT_PRESS_COLOR = Color.GRAY;
    private int DEFAULT_BACKGROUND = Color.TRANSPARENT;

    List<String> indexDatas;
 
    public IndexBar(Context context) {
        super(context);
        init(context, null, -1);

    }

    public IndexBar(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs, -1);
    }

    public IndexBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs, defStyleAttr);
    }

    private void init(Context context, AttributeSet attrs, int defStyleAttr) {
        this.mContext = context;
        //預設的TextSize
        int DEFAULT_SIZE = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics());
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.IndexrecyclerviewIndexBar);
        if (typedArray != null) {
            textSize = typedArray.getDimensionPixelSize(R.styleable.IndexrecyclerviewIndexBar_indexTextSize, DEFAULT_SIZE);
            mPressBackground = typedArray.getColor(R.styleable.IndexrecyclerviewIndexBar_pressBackground, DEFAULT_BACKGROUND);
            mTextColor = typedArray.getColor(R.styleable.IndexrecyclerviewIndexBar_indexTextColor, DEFAULT_PRESS_COLOR);
            mPressTextColor = typedArray.getColor(R.styleable.IndexrecyclerviewIndexBar_pressTextColor, mTextColor);
            mSelectTextColor = typedArray.getColor(R.styleable.IndexrecyclerviewIndexBar_selectTextColor, mTextColor);
        }
        initPaint();
        initDatas();
    }

    private void initDatas() {
		indexDatas = Arrays.asList(DEFAULT_INDEX);
    }

    private void initPaint() {
        mPaint = new Paint();
        mPaint.setTextSize(textSize);
        mPaint.setAntiAlias(true);
    }

    boolean isPress;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            isPress = true;
            Drawable background = getBackground();
            if (background != null) {
                color = ((ColorDrawable) background).getColor();
            }
            setBackgroundColor(mPressBackground);
            computePressIndexLocation(event.getX(), event.getY());

        } else if (event.getAction() == MotionEvent.ACTION_MOVE) {
            computePressIndexLocation(event.getX(), event.getY());
        } else {
            isPress = false;
            //手指擡起時背景恢復透明
            setBackgroundColor(color);
            //重置當前位置
            currentIndex = -1;

            if (mOnIndexPressListener != null) {
                mOnIndexPressListener.onMotionEventEnd();
            }
        }
        return true;
    }

    private int currentIndex = -1;

    /**
     * 計算按下的位置
     */
    private void computePressIndexLocation(float x, float y) {
        // 計算按下的區域位置
        currentIndex = (int) ((y - getPaddingTop()) / indexHeght);
        if (currentIndex < 0) {
            currentIndex = 0;
        } else if (currentIndex >= indexDatas.size()) {
            currentIndex = indexDatas.size() - 1;
        }
        invalidateMySelft();
        if (mOnIndexPressListener != null) {
            mOnIndexPressListener.onIndexChange(currentIndex, indexDatas.get(currentIndex));
        }

    }

   

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = w;
        mHeight = h;
        computeIndexHeight();
    }

    /**
     * 計算單個index高度
     */
    private void computeIndexHeight() {
        indexHeght = (mHeight - getPaddingTop() - getPaddingBottom()) / indexDatas.size();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        for (int i = 0; i < indexDatas.size(); i++) {
            String index = indexDatas.get(i);
            Paint.FontMetrics metrics = mPaint.getFontMetrics();
            //計算baseline
            int baseLine = (int) ((indexHeght - metrics.bottom - metrics.top) / 2);
            if (currentIndex == i) {
                mPaint.setColor(mSelectTextColor);
            } else {
                mPaint.setColor(isPress ? mPressTextColor : mTextColor);
            }
            //繪製文字
            canvas.drawText(index, mWidth / 2 - mPaint.measureText(index) / 2,
                        getPaddingTop() + baseLine + indexHeght * i, mPaint);
          
        }

    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //取出寬高的MeasureSpec  Mode 和Size
        int wMode = MeasureSpec.getMode(widthMeasureSpec);
        int wSize = MeasureSpec.getSize(widthMeasureSpec);
        int hMode = MeasureSpec.getMode(heightMeasureSpec);
        int hSize = MeasureSpec.getSize(heightMeasureSpec);
        //最終測量出來的寬高
        int measureWidth = 0, measureHeight = 0;

        //得到合適寬度:
        //存放每個繪製的index的Rect區域
        Rect indexBounds = new Rect();
        String index;//每個要繪製的index內容
        for (int i = 0; i < indexDatas.size(); i++) {
            index = indexDatas.get(i);
            //測量計算文字所在矩形,可以得到寬高
            mPaint.getTextBounds(index, 0, index.length(), indexBounds);
            //迴圈結束後,得到index的最大寬度
			measureWidth = Math.max(indexBounds.width() + getPaddingLeft() + getPaddingRight(), measureWidth);
            //迴圈結束後,得到index的最大高度,然後*size
            measureHeight = Math.max(indexBounds.height(), measureHeight);
        }
        measureHeight *= indexDatas.size();
        if (wMode == MeasureSpec.EXACTLY) {
            measureWidth = wSize;
        } else if (wMode == MeasureSpec.AT_MOST) {
            //wSize此時是父控制元件能給子View分配的最大空間
            measureWidth = Math.min(measureWidth, wSize);
        } else if (wMode == MeasureSpec.UNSPECIFIED) {

        }
        //得到合適的高度:
        if (hMode == MeasureSpec.EXACTLY) {
            measureHeight = hSize;
        } else if (hMode == MeasureSpec.AT_MOST) {
            //wSize此時是父控制元件能給子View分配的最大空間
            measureHeight = Math.min(measureHeight, hSize);
        } else if (hMode == MeasureSpec.UNSPECIFIED) {

        }
        setMeasuredDimension(measureWidth, measureHeight);
    }

 OnIndexPressListener mOnIndexPressListener;

    public void setOnIndexPressListener(OnIndexPressListener mOnIndexPressListener) {
        this.mOnIndexPressListener = mOnIndexPressListener;
    }

    public interface OnIndexPressListener {
        /**
         * @param index 當前選中的位置
         * @param text  選中的文字
         */
        void onIndexChange(int index, String text);

        /**
         * 事件結束時回撥
         */
        void onMotionEventEnd();
    }

  
}

特別說明,由於手指按下的時候改變了IndexBar的背景,擡起時需要恢復背景顏色,因此需要將IndexBar的初始背景顏色做臨時儲存;試下效果:

 現在indexbar只能使用固定的字母索引,在新增上使用源資料內容作為索引,程式碼如下:

  /**
     * 原始資料
     */
    List<? extends BaseIndexBean> sourceDatas;

    /**
     * 設定原始資料
     *
     * @param sourceDatas
     */
    public void setSourceDatas(List<? extends BaseIndexBean> sourceDatas) {
        this.sourceDatas = sourceDatas;
        initIndexDatas();
        invalidateMySelft();

    }

    private void invalidateMySelft() {
        if (isMainThread()) {
            invalidate();
        } else {
            postInvalidate();
        }
    }

    public boolean isMainThread() {
        return Thread.currentThread() == Looper.getMainLooper().getThread();
    }

    /**
     * 初始原始資料 並提取索引
     */
    private void initIndexDatas() {
        if (null == sourceDatas || sourceDatas.isEmpty()) {
            return;
        }
        if (mDataHelper == null) {
            mDataHelper = new IndexDataHelper();
        }
        mDataHelper.cover(sourceDatas);
        //源資料無序
        if (!isOrderly) {
            mDataHelper.sortDatas(sourceDatas);
        }
        if (useDatasIndex) {
            indexDatas = new ArrayList<>();
            mDataHelper.getIndex(sourceDatas, indexDatas);
            computeIndexHeight();
        }
    }
IndexDataHelper程式碼
public class IndexDataHelper implements IDataHelper {


    @Override
    public void cover(List<? extends BaseIndexBean> datas) {
        if (datas == null || datas.isEmpty()) {
            return;
        }
        for (BaseIndexBean data : datas) {
            String pinyinUpper = getUpperPinYin(data.getOrderName());
            data.setPinyin(pinyinUpper);
            data.setFirstLetter(pinyinUpper.substring(0, 1));
        }
    }

    @Override
    public void sortDatas(List<? extends BaseIndexBean> datas) {
        if (datas == null || datas.isEmpty()) {
            return;
        }
        cover(datas);
        Collections.sort(datas, new Comparator<BaseIndexBean>() {
            @Override
            public int compare(BaseIndexBean o1, BaseIndexBean o2) {
                if ("#".equals(o1.getPinyin())) {
                    return 1;
                } else if ("#".equals(o2.getPinyin())) {
                    return -1;
                } else {
                    return o1.getPinyin().compareTo(o2.getPinyin());
                }
            }
        });
    }

    @Override
    public void sortDatasAndGetIndex(List<? extends BaseIndexBean> datas, List<String> indexDatas) {
        if (datas == null || datas.isEmpty()) {
            return;
        }
        sortDatas(datas);
        getIndex(datas, indexDatas);
    }

    @Override
    public void getIndex(List<? extends BaseIndexBean> datas, List<String> indexDatas) {
        for (BaseIndexBean data : datas) {
            //獲取拼音首字母
            String pinyin = data.getIndexTag();
            if (!indexDatas.contains(pinyin)) {
                //如果是A-Z字母開頭
                if (pinyin.matches("[A-Z]")) {
                    indexDatas.add(pinyin);
                } else {//特殊字母這裡統一用#處理
                    indexDatas.add("#");
                }
            }
        }
    }


    /**
     * 獲取拼音 大寫
     *
     * @param text
     * @return
     */
    private String getUpperPinYin(String text) {
        return PinyinUtils.ccs2Pinyin(text).toUpperCase();
    }
}

public interface IDataHelper {
    /**
     * 資料轉換 根據getorderName生成pinyin
     *
     * @param datas
     */
    void cover(List<? extends BaseIndexBean> datas);

    /**
     * 排序
     *
     * @param datas
     */
    void sortDatas(List<? extends BaseIndexBean> datas);

    /**
     * 排序並獲取索引資料
     *
     * @param datas
     */
    void sortDatasAndGetIndex(List<? extends BaseIndexBean> datas, List<String> indexDatas);

    /**
     * 獲取索引
     *
     * @param datas
     * @param indexDatas
     */
    void getIndex(List<? extends BaseIndexBean> datas, List<String> indexDatas);
}

 BaseIndexBean程式碼:


public abstract class BaseIndexBean implements ISupperInterface {
    private String firstLetter;
    private String pinyin;

    public String getPinyin() {
        return pinyin;
    }

    public void setPinyin(String pinyin) {
        this.pinyin = pinyin;
    }

    public String getFirstLetter() {
        return firstLetter;
    }

    public void setFirstLetter(String firstLetter) {
        this.firstLetter = firstLetter;
    }

    @Override
    public String getIndexTag() {
        return firstLetter;
    }

    /**
     * 需要排序的內容
     *
     * @return
     */
    public abstract String getOrderName();
}

ISupperInterface 程式碼:


public interface ISupperInterface {
    /**
     * title的顯示內容
     *
     * @return
     */
    String getIndexTag();
}

  這裡使用介面方式是為了方便在不同地方能夠通過改變IDataHelper,實現不同的排列方式;

PinyinUtils:https://github.com/Blankj/AndroidUtilCode/blob/master/subutil/src/main/java/com/blankj/subutil/util/PinyinUtils.java

在補上recyclerview懸停分割線LevitationDecoration:



public class LevitationDecoration extends RecyclerView.ItemDecoration {

    /**
     * 畫筆
     */
    private Paint mPaint;
    /**
     * title背景
     */
    private int mTitleColor;
    /**
     * title文字顏色
     */
    private int mTextColor;
    /**
     * title文字尺寸
     */
    private int mTextSize;
    /**
     * 左邊距
     */
    private int mTextLeftPadding;
    /**
     * title高度
     */
    private int mTitleHeight;
    /**
     * 上下文
     */
    Context mContext;
    /**
     * recyclerview頭部view數量
     */
    int mHeadCount;
    /**
     * 繪製內容
     */
    List<? extends ISupperInterface> mDatas;

    /**
     * 滑動效果
     */
    public static final int MODE_TRANSLATE = 1;
    /**
     * 重疊效果
     */
    public static final int MODE_OVERLAP = 2;

    @IntDef({MODE_TRANSLATE, MODE_OVERLAP})
    @Retention(RetentionPolicy.SOURCE)
    public @interface MODE {

    }

    int mode = MODE_TRANSLATE;

    public LevitationDecoration(Context context) {
        mContext = context;
        mTitleColor = Color.GRAY;
        mTextColor = Color.BLACK;
        mTextSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 16f, mContext.getResources().getDisplayMetrics());
        //預設設定title左邊距為字型大小,避免itemview的paddingleft為0時title緊靠螢幕
        mTextLeftPadding = mTextSize;
        mTitleHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 30, mContext.getResources().getDisplayMetrics());
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setTextSize(mTextSize);
    }

    public void setTextLeftPadding(int textLeftPadding) {
        this.mTextLeftPadding = textLeftPadding;
    }

    public void setDatas(List<? extends ISupperInterface> datas) {
        this.mDatas = datas;
    }

    public void setMode(@MODE int mode) {
        this.mode = mode;
    }

    public int getHeadCount() {
        return mHeadCount;
    }

    public void setHeadCount(int headCount) {
        this.mHeadCount = headCount;
    }

    public void setTitleColor(@ColorInt int color) {
        this.mTitleColor = color;
    }

    public void setTitleColorResource(@ColorRes int color) {
        this.mTitleColor = mContext.getResources().getColor(color);
    }


    public void setTextColor(@ColorInt int color) {
        this.mTextColor = color;
    }

    public void setTextColorResource(@ColorRes int color) {
        this.mTextColor = mContext.getResources().getColor(color);
    }


    public void setTextSize(float textSize) {
        this.mTextSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
                textSize, mContext.getResources().getDisplayMetrics());
    }

    public void setTitleHeight(float height) {
        this.mTitleHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
                height, mContext.getResources().getDisplayMetrics());
    }

    @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++) {
            View child = parent.getChildAt(i);
            RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
            //獲取child的position
            int position = params.getViewLayoutPosition();
            if (mDatas == null || mDatas.isEmpty() || position > mDatas.size() - 1 || position < 0) {
                return;
            }
            //計算真實位置
            position -= mHeadCount;
            if (position > -1) {
                if (position == 0) {
                    drawTitleArea(c, left, right, child, params, position);
                } else {
                    if (null != mDatas.get(position).getIndexTag()
                            && !mDatas.get(position).getIndexTag().equals(mDatas.get(position - 1).getIndexTag())) {
                        drawTitleArea(c, left, right, child, params, position);

                    }

                }
            }
        }
    }

    /**
     * 繪製title區域
     *
     * @param c
     * @param left
     * @param right
     * @param child
     * @param params
     * @param position
     */

    private void drawTitleArea(Canvas c, int left, int right, View child, RecyclerView.LayoutParams params, int position) {
        mPaint.setColor(mTitleColor);
        c.drawRect(left, child.getTop() - params.topMargin - mTitleHeight, right, child.getTop() - params.topMargin, mPaint);
        mPaint.setColor(mTextColor);
        String text = mDatas.get(position).getIndexTag();
        Rect rect = new Rect();
        mPaint.getTextBounds(text, 0, text.length(), rect);
        c.drawText(text, child.getPaddingLeft() + mTextLeftPadding, child.getTop() - params.topMargin - (mTitleHeight / 2 - rect.height() / 2), mPaint);
    }


    @Override
    public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDrawOver(c, parent, state);
        int position = ((LinearLayoutManager) (parent.getLayoutManager())).findFirstVisibleItemPosition();
        position -= mHeadCount;

        if (mDatas == null || mDatas.isEmpty() || position > mDatas.size() - 1 || position < 0) {
            return;
        }
        View child = parent.findViewHolderForLayoutPosition(position + mHeadCount).itemView;
        //定義一個flag,Canvas是否位移過的標誌
        String text = mDatas.get(position).getIndexTag();
        boolean flag = false;
        if ((position + 1) < mDatas.size()) {
            String nextText = mDatas.get(position + 1).getIndexTag();
            //當前第一個可見的Item的tag,不等於其後一個item的tag,說明懸浮的View要切換了
            if (null != text && !text.equals(nextText)) {
                //當第一個可見的item在螢幕中還剩的高度小於title區域的高度時,我們也該開始做懸浮Title的“交換動畫”
                if (child.getHeight() + child.getTop() < mTitleHeight) {
                    c.save();//每次繪製前 儲存當前Canvas狀態,
                    flag = true;

                    if (mode == MODE_OVERLAP) {
                        //頭部摺疊起來的視效
                        //可與123行 c.drawRect 比較,只有bottom引數不一樣,由於 child.getHeight() + child.getTop() < mTitleHeight,所以繪製區域是在不斷的減小,有種摺疊起來的感覺
                        c.clipRect(parent.getPaddingLeft() + mTextSize, parent.getPaddingTop(), parent.getRight() - parent.getPaddingRight(), parent.getPaddingTop() + child.getHeight() + child.getTop());
                    } else {
                        //上滑時,將canvas上移 (y為負數
                        c.translate(0, child.getHeight() + child.getTop() - mTitleHeight);
                    }

                }
            }
        }
        mPaint.setColor(mTitleColor);
        c.drawRect(parent.getPaddingLeft(), parent.getPaddingTop(),
                parent.getRight() - parent.getPaddingRight(), parent.getPaddingTop() + mTitleHeight, mPaint);
        mPaint.setColor(mTextColor);
        Rect rect = new Rect();
        mPaint.getTextBounds(text, 0, text.length(), rect);
        c.drawText(text, child.getPaddingLeft() + mTextLeftPadding,
                parent.getPaddingTop() + mTitleHeight - (mTitleHeight / 2 - rect.height() / 2),
                mPaint);
        if (flag) {
            c.restore();//恢復畫布到之前儲存的狀態
        }

    }


    @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();
        position -= mHeadCount;
        if (mDatas == null || mDatas.isEmpty() || position > mDatas.size() - 1) {
            return;
        }
        if (position > -1) {
            if (position == 0) {
                outRect.set(0, mTitleHeight, 0, 0);
            } else {//其他的通過判斷
                String text = mDatas.get(position).getIndexTag();
                String lastText = mDatas.get(position - 1).getIndexTag();
                if (null != text && !text.equals(lastText)) {
                    //不為空 且跟前一個tag不一樣了,說明是新的分類,也要title
                    outRect.set(0, mTitleHeight, 0, 0);
                }
            }
        }
    }


}

 在看下完整效果:

當然還有Gridlayoutmanager的實現效果:

 具體實現,請檢視原始碼,原始碼地址