1

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.

Jakob Harteg
  • 9,587
  • 15
  • 56
  • 78

4 Answers4

18

Just to update: The latest support library provides a direct function :

CardView cardView;
cardView.setCardBackgroundColor(color);
vj9
  • 1,546
  • 1
  • 11
  • 18
  • Life saving! I was using `setBackgroundColor` but it was removing my `cornerRadius` and paddings between CardViews. Now it works as expected. – dialex Mar 03 '16 at 19:40
4

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.

JstnPwll
  • 8,585
  • 2
  • 33
  • 56
  • 1
    It seemed more helpful than closing as primarily opinion-based. – JstnPwll Nov 07 '14 at 18:40
  • Subclassing won't be necessary. Just do cardview.setBackgroundDrawable(new RoundRectDrawableWithShadow(context.getResources(), backgroundColor, radius); – Rolf ツ Nov 07 '14 at 18:41
  • This question could of-course be opinion-based but there is a chance this question actually has a real and not opinion based answer. – Rolf ツ Nov 07 '14 at 18:42
  • Well, thanks for policing my answer. Maybe a better use of your time would have been to post your own answer..? – JstnPwll Nov 07 '14 at 18:50
  • I did: But my research into the source code so far seems that there actually is no real reason to have left it out. It seems they also forgot to add a setColor method to the RoundRectDrawableWithShadow class. But why would one need such a method if you have the setBackgroundDrawable method? But I think this information is still to weak on its own to support a real answer. – Rolf ツ Nov 07 '14 at 18:54
  • Awesome job! At least there's enough opinion to warrant lots and lots of commentary. – JstnPwll Nov 07 '14 at 18:58
  • 1
    I'll accept your answer although you didn't know of a reason, nor gave me a working solution. BUT, you did point me in the right direction, and I got it working now, so thank you for that. – Jakob Harteg Nov 09 '14 at 18:12
  • Thanks. Not every question/answer fits the typical pattern... I'm glad it at least got you pointed in the right direction. – JstnPwll Nov 09 '14 at 18:25
2

This is tricky. You need to hack the API to acomplish this feature. The @Justin Powell's answer is right but crashes in API 21. My solution solve that. You need to add this two classes:

MyRoundRectDrawableWithShadow:

package android.support.v7.widget;

import android.content.res.Resources;

public class MyRoundRectDrawableWithShadow extends RoundRectDrawableWithShadow {

  public MyRoundRectDrawableWithShadow(Resources resources, int backgroundColor) {
    super(resources, backgroundColor,
        resources.getDimensionPixelSize(R.dimen.cardview_default_radius),
        resources.getDimensionPixelSize(R.dimen.cardview_default_elevation),
        resources.getDimensionPixelSize(R.dimen.cardview_default_elevation));
  }

  public MyRoundRectDrawableWithShadow(Resources resources, int backgroundColor, float radius) {
    super(resources, backgroundColor, radius,
        resources.getDimensionPixelSize(R.dimen.cardview_default_elevation),
        resources.getDimensionPixelSize(R.dimen.cardview_default_elevation));
  }

  public MyRoundRectDrawableWithShadow(Resources resources, int backgroundColor, float radius,
                                       float shadowSize, float maxShadowSize) {
    super(resources, backgroundColor, radius, shadowSize, maxShadowSize);
  }
}

MyRoundRectDrawable:

package android.support.v7.widget;

import android.content.res.Resources;

public class MyRoundRectDrawable extends RoundRectDrawable {

  public MyRoundRectDrawable(Resources resources, int backgroundColor) {
    super(backgroundColor, resources.getDimensionPixelSize(R.dimen.cardview_default_radius));
  }

  public MyRoundRectDrawable(int backgroundColor, float radius) {
    super(backgroundColor, radius);
  }
}

And then use this code to change the background color:

final Drawable background;
if (Build.VERSION.SDK_INT >= 21) {
  background = new MyRoundRectDrawable(color);
} else {
  background = new MyRoundRectDrawableWithShadow(resources, color);
}
// This is to avoid to use a deprecated method
if (Build.VERSION.SDK_INT >= 16) {
  cardView.setBackground(background);
} else {
  cardView.setBackgroundDrawable(background);
}
Community
  • 1
  • 1
Brais Gabin
  • 5,827
  • 6
  • 57
  • 92
0

Simply write this snippet: cardView.setCardBackgroundColor(getResources().getColor(R.color.colorPrimary)); where you want the code

Kalaiyo5
  • 31
  • 10