Chad Schultz
Chad Schultz

Reputation: 7860

How can I draw a CardView shadow to a canvas in a RecyclerView ItemDecoration?

I've recently been running into varied issues using a RecyclerView and PagedListAdapter with multiple view types. The reason for different view types was essentially to add section headers. Switching to using ItemDecoration seemed much more stable and a better pattern.

So I thought I'd do the same thing with another RecyclerView, trying to eliminate multiple view types so each row in the RecyclerView corresponded to an item in the underlying PagedList. The problem is, this time it isn't a simple section header TextView. It's a CardView.

I had a bit of trouble getting the width right (this CardView is meant to be MATCH_PARENT). I think I figured that out, but I have another problem. The CardView is drawing, but without the background shadow. I see from StackOverflow questions like Why is my cardview not being drawn onto canvas? that other people have the same problem. It seems elevation shadows are not drawn with regular layout/measure/draw functionality.

How can I get my CardView shadow in the ItemDecoration? Is there a way?

This is what I have currently:

class CardItemDecoration(val adapter: ReservationAdapter) : RecyclerView.ItemDecoration() {
    override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
        super.getItemOffsets(outRect, view, parent, state)

        val position = parent.getChildAdapterPosition(view)

        if (adapter.hasCard && position == 0) {
            outRect.top = getcardView(parent.context, parent).measuredHeight
        }
    }

    override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
        super.onDraw(c, parent, state)

        val left = parent.paddingLeft
        val right = parent.width - parent.paddingRight

        if (adapter.hascard && parent.childCount > 0) {
            val child = parent.getChildAt(0)

            val layout = getCardView(parent.context, parent)

            // Draw in the space made by getItemOffsets()
            layout.layout(left, 0, right, layout.measuredHeight)
            c.save()
            // Adjust Y coordinates, as they'll be different for each row
            val top = child.top - layout.measuredHeight
            c.translate(0f, top.toFloat())
            layout.draw(c)
            c.restore()
        }
    }

    private lateinit var cardView: ViewGroup

    private fun getCardView(context: Context, parent: RecyclerView): View {
        if (!::cardView.isInitialized) {
            cardView = LinearLayout(context)
            LayoutInflater.from(context).inflate(R.layout.call_out_bis_profile, cardView, true)

            cardView.apply {
                findViewById<TextView>(R.id.infoTextView).text = context.getString(R.string.card_description)

            }

            val width = parent.width - parent.paddingLeft - parent.paddingRight
            cardView.measure(View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
                    View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED))
        }
        return cardView
    }
}

Upvotes: 1

Views: 1285

Answers (1)

Annie Aye Myat
Annie Aye Myat

Reputation: 247

This is card view decoration example. This is my reference: https://github.com/bleeding182/recyclerviewItemDecorations

Paint mPaint;

static RoundRectHelper sRoundRectHelper;

Paint mCornerShadowPaint;

Paint mEdgeShadowPaint;

final RectF mPreShadowBounds;

float mCornerRadius;

Path mCornerShadowPath;

float mShadowSize;

private boolean mDirty = true;

private final int mShadowStartColor;

private final int mShadowEndColor;
private float mPadding;


public CardViewDecoration(Resources resources, int backgroundColor, float radius) {
    mShadowStartColor = resources.getColor(R.color.cardview_shadow_start_color);
    mShadowEndColor = resources.getColor(R.color.cardview_shadow_end_color);
    mShadowSize = resources.getDimension(R.dimen.cardview_shadow_size) * SHADOW_MULTIPLIER;

    mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
    mPaint.setColor(backgroundColor);
    mCornerShadowPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
    mCornerShadowPaint.setStyle(Paint.Style.FILL);
    mCornerShadowPaint.setDither(true);
    mCornerRadius = radius;
    mPreShadowBounds = new RectF();
    mEdgeShadowPaint = new Paint(mCornerShadowPaint);

    buildShadowCorners();
}

@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
    Rect bounds = new Rect();
    float edgeShadowTop = -mCornerRadius - mShadowSize;

    RecyclerView.LayoutManager lm = parent.getLayoutManager();
    float size16dp = 16f;
    int padding16dp = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, size16dp, parent.getContext().getResources().getDisplayMetrics());

    for (int i = 0; i < parent.getChildCount(); i++) {
        int save = c.save();

        // using decorated values, remove what we set before
        View child = parent.getChildAt(i);
        bounds.set(lm.getDecoratedLeft(child) + padding16dp - (int) mPadding,
                lm.getDecoratedTop(child),
                lm.getDecoratedRight(child) - padding16dp + (int) mPadding,
                lm.getDecoratedBottom(child));

        RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
        int position = params.getViewAdapterPosition();
        int viewType = parent.getAdapter().getItemViewType(position);


        if (viewType == HeaderItemTestAdapter.HEADER) {
            bounds.top = (int) (bounds.top + padding16dp - mPadding);

            // LT
            c.translate(bounds.left + mCornerRadius, bounds.top + mCornerRadius);
            c.drawPath(mCornerShadowPath, mCornerShadowPaint);
            c.drawRect(0, edgeShadowTop, bounds.width() - 2 * mCornerRadius, -mCornerRadius, mEdgeShadowPaint);

            // RT
            c.rotate(90f);
            c.translate(0, -bounds.width() + 2 * mCornerRadius);
            c.drawPath(mCornerShadowPath, mCornerShadowPaint);
            c.drawRect(0, edgeShadowTop, bounds.height() - mCornerRadius, -mCornerRadius, mEdgeShadowPaint);

            // LBorder
            c.rotate(180f);
            c.translate(-bounds.height(), -bounds.width() + 2 * mCornerRadius);
            c.drawRect(mCornerRadius, edgeShadowTop, bounds.height(), -mCornerRadius, mEdgeShadowPaint);


        } else {
            if (parent.getAdapter().getItemViewType(position + 1) == HeaderItemTestAdapter.HEADER) {
                bounds.bottom = (int) (bounds.bottom - padding16dp + mPadding);

                // last item before next header
                c.rotate(180f);
                c.translate(-bounds.left - bounds.width() + mCornerRadius, -bounds.top - bounds.height() + mCornerRadius);

                c.drawPath(mCornerShadowPath, mCornerShadowPaint);
                c.drawRect(0, edgeShadowTop, bounds.width() - 2 * mCornerRadius, -mCornerRadius, mEdgeShadowPaint);

                // RT / Right border
                c.rotate(90f);
                c.translate(0, -bounds.width() + 2 * mCornerRadius);
                c.drawPath(mCornerShadowPath, mCornerShadowPaint);
                c.drawRect(0, edgeShadowTop, bounds.height() - mCornerRadius, -mCornerRadius, mEdgeShadowPaint);

                // Left border
                c.rotate(180f);
                c.translate(-bounds.height(), -bounds.width() + 2 * mCornerRadius);
                c.drawRect(mCornerRadius, edgeShadowTop, bounds.height(), -mCornerRadius, mEdgeShadowPaint);
            } else {
                // Right border
                c.translate(bounds.left, bounds.top);
                c.rotate(90f);
                c.translate(0, -bounds.width() + mCornerRadius);
                c.drawRect(0, edgeShadowTop, bounds.height(), -mCornerRadius, mEdgeShadowPaint);

                // Left border
                c.rotate(180f);
                c.translate(-bounds.height(), -bounds.width() + 2 * mCornerRadius);
                c.drawRect(0, edgeShadowTop, bounds.height(), -mCornerRadius, mEdgeShadowPaint);
            }
        }
        c.restoreToCount(save);
    }
}

private void buildShadowCorners() {

    mPadding = 0f;

    RectF innerBounds = new RectF(-mCornerRadius, -mCornerRadius, mCornerRadius, mCornerRadius);
    RectF outerBounds = new RectF(innerBounds);
    outerBounds.inset(-mShadowSize, -mShadowSize);

    if (mCornerShadowPath == null) {
        mCornerShadowPath = new Path();
    } else {
        mCornerShadowPath.reset();
    }
    mCornerShadowPath.setFillType(Path.FillType.EVEN_ODD);
    mCornerShadowPath.moveTo(-mCornerRadius, 0);
    mCornerShadowPath.rLineTo(-mShadowSize, 0);
    // outer arc
    mCornerShadowPath.arcTo(outerBounds, 180f, 90f, false);
    // inner arc
    mCornerShadowPath.arcTo(innerBounds, 270f, -90f, false);
    mCornerShadowPath.close();

    float startRatio = mCornerRadius / (mCornerRadius + mShadowSize);
    mCornerShadowPaint.setShader(new RadialGradient(0, 0, mCornerRadius + mShadowSize, new int[]{
            mShadowStartColor, mShadowStartColor, mShadowEndColor}, new float[]{0f, startRatio, 1f},
            Shader.TileMode.CLAMP));

    // we offset the content shadowSize/2 pixels up to make it more realistic.
    // this is why edge shadow shader has some extra space
    // When drawing bottom edge shadow, we use that extra space.
    mEdgeShadowPaint.setShader(new LinearGradient(0, -mCornerRadius + mShadowSize, 0, -mCornerRadius - mShadowSize,
            new int[]{mShadowStartColor, mShadowStartColor, mShadowEndColor}, new float[]{0f, .5f, 1f},
            Shader.TileMode.CLAMP));
}

@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
    super.getItemOffsets(outRect, view, parent, state);
    Resources resources = parent.getContext().getResources();

    float size16dp = 16f;
    int padding16dp = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, size16dp, resources.getDisplayMetrics());

    RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
    int position = params.getViewAdapterPosition();
    int viewType = parent.getAdapter().getItemViewType(position);

    if (viewType == HeaderItemTestAdapter.HEADER) {
        // header
        outRect.set(0, (int) (padding16dp), 0, 0);
    } else {
        if (parent.getAdapter().getItemViewType(position + 1) == HeaderItemTestAdapter.HEADER) {
            // last item before next header
            outRect.set(0, 0, 0, (int) (padding16dp));
        }
    }

    outRect.left = (int) padding16dp;
    outRect.right = (int) padding16dp;
}
}

It really helps me!!

Upvotes: 1

Related Questions