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的实现效果:

 具体实现,请查看源码,源码地址

猜你喜欢

转载自blog.csdn.net/jy494495991/article/details/81412788
今日推荐