Search code examples
androidandroid-5.0-lollipopandroid-cardview

How to change background color of CardView programmatically


Is there a reason why Google decided not to make a method to dynamically change the background color of a CardView?

Snipped here enter image description here


WORKAROUND

The simple line of code @Justin Powell suggested does not work for me. On Android 5.0 that is. But it did get me in the right direction.

This code, (MyRoundRectDrawableWithShadow being a copy of this)

        card.setBackgroundDrawable(new MyRoundRectDrawableWithShadow(context.getResources(),
                color,
                card.getRadius(),
                card.getCardElevation(),
                card.getMaxCardElevation()));

... gave me this error,

java.lang.NullPointerException: Attempt to invoke interface method 'void com.example.app.MyRoundRectDrawableWithShadow$RoundRectHelper.drawRoundRect(android.graphics.Canvas, android.graphics.RectF, float, android.graphics.Paint)' on a null object reference
        at com.example.app.MyRoundRectDrawableWithShadow.draw(MyRoundRectDrawableWithShadow.java:172)

Which simply says that there is an Interface getting called that is null. I then checked out the CardView source, to find out how it did it. I found that the following piece of code initializes the interface in some static way (I don't really understand why, please explain if you know), which I then call once at class init, and you can then set the card of the color with the above chunk of code.

final RectF sCornerRect = new RectF();
MyRoundRectDrawableWithShadow.sRoundRectHelper
                = new MyRoundRectDrawableWithShadow.RoundRectHelper() {
            @Override
            public void drawRoundRect(Canvas canvas, RectF bounds, float cornerRadius,
                                      Paint paint) {
                final float twoRadius = cornerRadius * 2;
                final float innerWidth = bounds.width() - twoRadius;
                final float innerHeight = bounds.height() - twoRadius;
                sCornerRect.set(bounds.left, bounds.top,
                        bounds.left + cornerRadius * 2, bounds.top + cornerRadius * 2);
                canvas.drawArc(sCornerRect, 180, 90, true, paint);
                sCornerRect.offset(innerWidth, 0);
                canvas.drawArc(sCornerRect, 270, 90, true, paint);
                sCornerRect.offset(0, innerHeight);
                canvas.drawArc(sCornerRect, 0, 90, true, paint);
                sCornerRect.offset(-innerWidth, 0);
                canvas.drawArc(sCornerRect, 90, 90, true, paint);
                //draw top and bottom pieces
                canvas.drawRect(bounds.left + cornerRadius, bounds.top,
                        bounds.right - cornerRadius, bounds.top + cornerRadius,
                        paint);
                canvas.drawRect(bounds.left + cornerRadius,
                        bounds.bottom - cornerRadius, bounds.right - cornerRadius,
                        bounds.bottom, paint);
                //center
                canvas.drawRect(bounds.left, bounds.top + cornerRadius,
                        bounds.right, bounds.bottom - cornerRadius, paint);
            }
        };

This solution does however spawn a new problem. Not sure what happens on pre-lollipop, but when the CardView is first initialized it appears to create a RoundRectDrawable as the background from the attributes you've set in XML. When we then change the color with the above code, we set a MyRoundRectDrawableWithShadow as the background, and if you then want to change the color once again, the card.getRadius(), card.getCardElevation() etc, will no longer work.

This therefore first tries to parse the background it gets from the CardView as a MyRoundRectDrawableWithShadow from which it then gets the values if it succeeds (which it will the second+ time you change the color). But, if it fails (which is will on the first color change, because the background is a different class) it will get the values directly from the CardView itself.

    float cardRadius;
    float maxCardElevation;

    try{
        MyRoundRectDrawableWithShadow background = (MyRoundRectDrawableWithShadow)card.getBackground();
        cardRadius = background.getCornerRadius();
        maxCardElevation = background.getMaxShadowSize();
    }catch (ClassCastException classCastExeption){
        cardRadius = card.getRadius();
        maxCardElevation = card.getMaxCardElevation();
    }

    card.setBackgroundDrawable(
            new MyRoundRectDrawableWithShadow(context.getResources(),
                    Color.parseColor(note.getColor()),
                    cardRadius,
                    card.getCardElevation(),
                    maxCardElevation));

Hope that made sense, I'm not a native English speaker... As mentioned, this was only tested on Lollipop.


Solution

  • I don't know of any particular reasoning.

    However, if you're interested in hacking around this omission...

    All the CardView does with this attribute is create a rounded rectangle drawable using the color, and then assigns it as the background of the CardView. If you really want to set the color programmatically, you could create a copy of RoundRectDrawableWithShadow, and then do this:

    mCardView.setBackgroundDrawable(new MyRoundRectDrawableWithShadow(getResources(), color, radius));
    

    You cannot subclass RoundRectDrawableWithShadow or use it directly because it is not public.