基于ItemDecoration实现RecyclerView分割线和吸顶效果

用过ListView的朋友都知道,ListView是带有分割线效果的,只需要设置divider属性。RecyclerView本身并没有分割线,很多人为了省事也是粗暴的在item布局里面直接添加分割线,虽然没什么问题,但我觉得不够优雅。当然官方也是提供了分割线的解决方案,就是采用DividerItemDecoration来设置分割线。
我们来分析一下DividerItemDecoration源码

public class DividerItemDecoration extends ItemDecoration {
    public static final int HORIZONTAL = 0;
    public static final int VERTICAL = 1;
    private static final String TAG = "DividerItem";
    private static final int[] ATTRS = new int[]{16843284};
    private Drawable mDivider;
    private int mOrientation;
    private final Rect mBounds = new Rect();

    public DividerItemDecoration(Context context, int orientation) {
        TypedArray a = context.obtainStyledAttributes(ATTRS);
        this.mDivider = a.getDrawable(0);
        if (this.mDivider == null) {
            Log.w("DividerItem", "@android:attr/listDivider was not set in the theme used for this DividerItemDecoration. Please set that attribute all call setDrawable()");
        }

        a.recycle();
        this.setOrientation(orientation);
    }

    public void setOrientation(int orientation) {
        if (orientation != 0 && orientation != 1) {
            throw new IllegalArgumentException("Invalid orientation. It should be either HORIZONTAL or VERTICAL");
        } else {
            this.mOrientation = orientation;
        }
    }

    public void setDrawable(@NonNull Drawable drawable) {
        if (drawable == null) {
            throw new IllegalArgumentException("Drawable cannot be null.");
        } else {
            this.mDivider = drawable;
        }
    }

    public void onDraw(Canvas c, RecyclerView parent, State state) {
        if (parent.getLayoutManager() != null && this.mDivider != null) {
            if (this.mOrientation == 1) {
                this.drawVertical(c, parent);
            } else {
                this.drawHorizontal(c, parent);
            }

        }
    }

    private void drawVertical(Canvas canvas, RecyclerView parent) {
        canvas.save();
        int left;
        int right;
        if (parent.getClipToPadding()) {
            left = parent.getPaddingLeft();
            right = parent.getWidth() - parent.getPaddingRight();
            canvas.clipRect(left, parent.getPaddingTop(), right, parent.getHeight() - parent.getPaddingBottom());
        } else {
            left = 0;
            right = parent.getWidth();
        }

        int childCount = parent.getChildCount();

        for(int i = 0; i < childCount; ++i) {
            View child = parent.getChildAt(i);
            parent.getDecoratedBoundsWithMargins(child, this.mBounds);
            int bottom = this.mBounds.bottom + Math.round(child.getTranslationY());
            int top = bottom - this.mDivider.getIntrinsicHeight();
            this.mDivider.setBounds(left, top, right, bottom);
            this.mDivider.draw(canvas);
        }

        canvas.restore();
    }

    private void drawHorizontal(Canvas canvas, RecyclerView parent) {
        canvas.save();
        int top;
        int bottom;
        if (parent.getClipToPadding()) {
            top = parent.getPaddingTop();
            bottom = parent.getHeight() - parent.getPaddingBottom();
            canvas.clipRect(parent.getPaddingLeft(), top, parent.getWidth() - parent.getPaddingRight(), bottom);
        } else {
            top = 0;
            bottom = parent.getHeight();
        }

        int childCount = parent.getChildCount();

        for(int i = 0; i < childCount; ++i) {
            View child = parent.getChildAt(i);
            parent.getLayoutManager().getDecoratedBoundsWithMargins(child, this.mBounds);
            int right = this.mBounds.right + Math.round(child.getTranslationX());
            int left = right - this.mDivider.getIntrinsicWidth();
            this.mDivider.setBounds(left, top, right, bottom);
            this.mDivider.draw(canvas);
        }

        canvas.restore();
    }

    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
        if (this.mDivider == null) {
            outRect.set(0, 0, 0, 0);
        } else {
            if (this.mOrientation == 1) {
                outRect.set(0, 0, 0, this.mDivider.getIntrinsicHeight());
            } else {
                outRect.set(0, 0, this.mDivider.getIntrinsicWidth(), 0);
            }

        }
    }
}

DividerItemDecoration 是继承自ItemDecoration,Decoration意思为装饰,也就是说这个东西就是条目装饰器,我们来看最重要的两个方法

@Override
    public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.onDraw(c, parent, state);
    }

    @Override
    public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
    }

onDraw我们最了解了,就是绘制。getItemOffsets就是设置偏移量如果不设置这个就可能会造成你绘制的view和item重叠。
也就是说要设置分割线我们就要从这两者下手,创建一个类DividerDecoration继承自ItemDecoration,如下

public class DividerDecoration extends ItemDecoration {

    @Override
    public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.onDraw(c, parent, state);
    }

    @Override
    public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
    }

}

在构造方法里创建画笔和相关设置,在onDraw方法里面进行绘制

public class DividerDecoration extends ItemDecoration {
    private int width;
    private int divider_height;
    private int divider_padding;
    private Paint paint;

    public DividerDecoration(Context context, int resColor, float dividerHeight, float padding) {
        width = context.getResources().getDisplayMetrics().widthPixels;
        paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
        paint.setColor(context.getResources().getColor(resColor));
        divider_height = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dividerHeight, context.getResources().getDisplayMetrics());
        divider_padding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, padding, context.getResources().getDisplayMetrics());
    }

    @Override
    public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.onDraw(c, parent, state);
        int count = parent.getChildCount();
        for (int i = 0; i < count-1; i++) {
            View view = parent.getChildAt(i);
            int top = view.getBottom();
            int bottom = top + divider_height;
            //这里把left和right的值分别增加divider_padding和减去divider_padding
            c.drawRect(divider_padding, top, width - divider_padding, bottom, paint);
        }
    }

    @Override
    public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        outRect.bottom = divider_height;
    }

}

运行结果如下图:


基于ItemDecoration实现RecyclerView分割线和吸顶效果_第1张图片
first.jpg

代码比较简单,接下来我们来实现今天的重点——悬浮吸顶效果。在实现这个效果之前我们先把头部布局绘制出来,头部布局就是绘制背景和文字

@Override
    public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.onDraw(c, parent, state);
        int count = parent.getChildCount();
        for (int i = 0; i < count; i++) {
            View view = parent.getChildAt(i);
            int position = parent.getChildLayoutPosition(view);
            boolean isFirst = mISticky.isFirstPosition(position);
            String text = mISticky.getGroupTitle(position);
            int left = parent.getPaddingLeft();
            int right = parent.getWidth() - parent.getPaddingRight();
            if (isFirst) {
                c.drawRect(left, view.getTop() - mRectHeight, right, view.getTop(), mRectPaint);
                mTextPaint.getTextBounds(text, 0, text.length(), textRect);
                float baseLine = (view.getTop() - mRectHeight) + mRectHeight / 2 + textRect.height() / 2;
                c.drawText(text, left + mTextPaddingLeft, baseLine, mTextPaint);
            } else {
                c.drawRect(left, view.getTop() - 1, right, view.getTop(), mDividerPaint);
            }
        }

    }

mISticky是用于区分头部与普通item的接口,这里做判断如果是头部就绘制背景框和文字这里需要注意文字居中。文字的基准线就是背景框的中间位置y坐标加上文字框的一半。运行效果如下图:


基于ItemDecoration实现RecyclerView分割线和吸顶效果_第2张图片
head.jpg

但是这样没办法让标题悬顶,ItemDecoration里面还有一个方法onDrawOver,它可以绘制一个view覆盖在item之上,假设我们在第一个位置绘制号头布局不就可以实现一直在顶上了吗,我们在onDrawOver再绘制一个

@Override
    public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.onDrawOver(c, parent, state);
        int position = ((LinearLayoutManager) (parent.getLayoutManager())).findFirstVisibleItemPosition();
        String text = mISticky.getGroupTitle(position);
        int top = parent.getPaddingTop();
        int left = parent.getPaddingLeft();
        int right = parent.getWidth() - parent.getPaddingRight();
        c.drawRect(left, top, right, top + mRectHeight, mRectPaint);
        mTextPaint.getTextBounds(text, 0, text.length(), textRect);
        float baseLine = top + mRectHeight / 2 + textRect.height() / 2;
        c.drawText(text, left + mTextPaddingLeft, baseLine, mTextPaint);

    }

但是还有一个问题,顶部布局不能被下面的顶上去。所以当第一个可见的view的下面一个如果是另外一组就需要动态改变这个头布局的绘制。具体实现就是我们先通过findFirstVisibleItemPosition拿到第一个可见的Item的position,那我们就可以根据position+1判断下一个Item是否是另一组的头布局,如果不是,绘制固定布局,如果是,我们根据第一个可见Item的getBottom值改变头部布局的绘制。

@Override
    public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.onDrawOver(c, parent, state);
        int position = ((LinearLayoutManager) (parent.getLayoutManager())).findFirstVisibleItemPosition();
        if (position <= -1 || position >= parent.getAdapter().getItemCount() - 1) {
            return;
        }
        View view = parent.findViewHolderForAdapterPosition(position).itemView;

        String text = mISticky.getGroupTitle(position);
        boolean isFirst = mISticky.isFirstPosition(position + 1);
        int top = parent.getPaddingTop();
        int left = parent.getPaddingLeft();
        int right = parent.getWidth() - parent.getPaddingRight();

        if (isFirst) {
            int bottom = Math.min(mRectHeight, view.getBottom());
            c.drawRect(left, top + bottom - mRectHeight, right, top + bottom, mRectPaint);
            mTextPaint.getTextBounds(text, 0, text.length(), textRect);
            float baseLine = top + bottom - mRectHeight / 2 + textRect.height() / 2;
            c.drawText(text, left + mTextPaddingLeft, baseLine, mTextPaint);
        }else {
            c.drawRect(left, top, right, top + mRectHeight, mRectPaint);
            mTextPaint.getTextBounds(text, 0, text.length(), textRect);
            float baseLine = top + mRectHeight / 2 + textRect.height() / 2;
            c.drawText(text, left + mTextPaddingLeft, baseLine, mTextPaint);
        }


    }

重点在于顶部布局滑动时坐标的计算,至此一个悬浮吸顶的StickyItemDecoration就完成了

public class StickyItemDecoration extends RecyclerView.ItemDecoration {
    private ISticky mISticky;
    private int mRectHeight;//背景高度
    private int mTextPaintSize; //    文字TextSize
    private int mTextPaddingLeft; //    文字到左边的距离
    private Paint mTextPaint;  //文字画笔
    private Paint mRectPaint; //标题背景框画笔
    private Paint mDividerPaint; //分割线画笔

    private final Rect textRect;


    public StickyItemDecoration(Context context, ISticky iSticky) {
        mISticky = iSticky;
        mRectHeight = dp2px(context, 25);
        mTextPaintSize = sp2px(context, 16);
        mTextPaddingLeft = dp2px(context, 14);
        mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mTextPaint.setColor(Color.BLACK);
        mTextPaint.setTextSize(mTextPaintSize);
        mRectPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mRectPaint.setStyle(Paint.Style.FILL);
        mRectPaint.setColor(Color.parseColor("#DDDDDD"));
        mDividerPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mDividerPaint.setStyle(Paint.Style.FILL);
        mDividerPaint.setColor(Color.BLUE);
        textRect = new Rect();
    }

    @Override
    public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.onDraw(c, parent, state);
        int count = parent.getChildCount();
        for (int i = 0; i < count; i++) {
            View view = parent.getChildAt(i);
            int position = parent.getChildLayoutPosition(view);
            boolean isFirst = mISticky.isFirstPosition(position);
            String text = mISticky.getGroupTitle(position);
            int left = parent.getPaddingLeft();
            int right = parent.getWidth() - parent.getPaddingRight();
            if (isFirst) {
                c.drawRect(left, view.getTop() - mRectHeight, right, view.getTop(), mRectPaint);
                mTextPaint.getTextBounds(text, 0, text.length(), textRect);
                float baseLine = (view.getTop() - mRectHeight) + mRectHeight / 2 + textRect.height() / 2;
                c.drawText(text, left + mTextPaddingLeft, baseLine, mTextPaint);
            } else {
                c.drawRect(left, view.getTop() - 1, right, view.getTop(), mDividerPaint);
            }
        }

    }

    @Override
    public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
        super.onDrawOver(c, parent, state);
        int position = ((LinearLayoutManager) (parent.getLayoutManager())).findFirstVisibleItemPosition();
        if (position <= -1 || position >= parent.getAdapter().getItemCount() - 1) {
            return;
        }
        View view = parent.findViewHolderForAdapterPosition(position).itemView;

        String text = mISticky.getGroupTitle(position);
        boolean isFirst = mISticky.isFirstPosition(position + 1);
        int top = parent.getPaddingTop();
        int left = parent.getPaddingLeft();
        int right = parent.getWidth() - parent.getPaddingRight();

        if (isFirst) {
            int bottom = Math.min(mRectHeight, view.getBottom());
            c.drawRect(left, top + bottom - mRectHeight, right, top + bottom, mRectPaint);
            mTextPaint.getTextBounds(text, 0, text.length(), textRect);
            float baseLine = top + bottom - mRectHeight / 2 + textRect.height() / 2;
            c.drawText(text, left + mTextPaddingLeft, baseLine, mTextPaint);
        }else {
            c.drawRect(left, top, right, top + mRectHeight, mRectPaint);
            mTextPaint.getTextBounds(text, 0, text.length(), textRect);
            float baseLine = top + mRectHeight / 2 + textRect.height() / 2;
            c.drawText(text, left + mTextPaddingLeft, baseLine, mTextPaint);
        }


    }

    @Override
    public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView
            parent, @NonNull RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        int pos = parent.getChildLayoutPosition(view);
        if (mISticky.isFirstPosition(pos)) {
            outRect.top = mRectHeight;
        } else {
            outRect.top = 1;
        }

    }

    /**
     * dp转换成px
     */
    private int dp2px(Context context, float dpValue) {
        float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f);
    }

    private int sp2px(Context context, float spValue) {
        final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
        return (int) (spValue * fontScale + 0.5f);
    }
}

不过此方案太依赖于第一个可见的item。如果item的高度 小于标题高度会导致文字快速上移,还有就是无法在标题设置点击事件。

你可能感兴趣的:(基于ItemDecoration实现RecyclerView分割线和吸顶效果)