基于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;
    }

}

運行結果如下圖:


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坐標加上文字框的一半。運行效果如下圖:


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的高度 小于標題高度會導致文字快速上移,還有就是無法在標題設置點擊事件。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容