30

I'm having some trouble with CardView transparency and card_elevation. Trying to use a CardView transparent the result is:

enter image description here

Without transparency:

enter image description here

What I'm trying to get is something like this:

enter image description here

Here is my xml:

<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:card_view="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@mipmap/eifell"
    android:padding="10dp"
    tools:context=".MainActivity">

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:paddingTop="5dp"
        android:background="@android:color/transparent">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <android.support.v7.widget.CardView
                android:id="@+id/newsCardView"
                android:layout_width="match_parent"
                android:layout_height="175dp"
                card_view:cardBackgroundColor="#602B608A"
                card_view:cardElevation="5dp">

                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:background="@android:color/transparent">
                </LinearLayout>

            </android.support.v7.widget.CardView>

        </LinearLayout>
    </ScrollView>

</RelativeLayout>
Isquierdo
  • 745
  • 1
  • 10
  • 29
  • 1
    Did you ever get a workaround for this? It looks like cards aren't designed to support background transparency: https://code.google.com/p/android/issues/detail?id=78061 – Steve Blackwell Sep 26 '15 at 19:16
  • 1
    Hello Steve, no I'm just using RelativeLayout with a transparent background without shadow (elevation). When I found something I will post where. – Isquierdo Sep 26 '15 at 23:48
  • @SteveBlackwell That is valid answer of this question. Thanks. – Vaibhav Jani Oct 18 '16 at 06:50
  • Question: Why do CardViews actually behave this way? Is there a Github issue to this effect? – Taslim Oseni Sep 05 '19 at 17:06

4 Answers4

20

I know i am a bit late, but it's just because of card default elevation. Set it to zero to solve your problem.

app:cardElevation="0dp"
Prateek Gupta
  • 835
  • 10
  • 18
6

Try this code:

 <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:card_view="http://schemas.android.com/apk/res-auto"
        android:id="@+id/newsCardView"
        android:layout_width="175dp"
        android:layout_height="175dp"
        card_view:cardBackgroundColor="#602B608A"
        card_view:cardCornerRadius="0dp"
        card_view:cardElevation="5dp">

    </android.support.v7.widget.CardView>

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/imageView"
        android:layout_gravity="left|top"
        android:src="@drawable/fake_image" /> //REPLACE THIS WITH YOUR IMAGE
</FrameLayout>

enter image description here

If it not helped, provide whole xml code of your layout

Sergey Zabelnikov
  • 1,855
  • 1
  • 14
  • 24
5

That effect is not specific to CardView, but rather is an artifact of the method Android uses to render and transform material shadows on all Views since Lollipop and the introduction of Material Design. It can be observed on pretty much any View with the right attributes:

  • A translucent/transparent background,
  • A positive z-offset (elevation + translationZ),
  • A ViewOutlineProvider that sets an Outline with a non-zero alpha.

To illustrate, this mock-up shows a plain <View> with a background that is a solid white round rectangle tinted with a translucent blue:

Screenshot of a mock-up of the question's setup with a plain View.

Tower image modified from "Eiffel Tower in Vintage Sepia" by Lenny K Photography, licensed under CC BY 2.0.

Obviously, the stated criteria present a few different ways to disable the shadow altogether, so if you don't really need that but still need the elevation for z-ordering, for instance, you could simply set the ViewOutlineProvider to null. Or, if perhaps you also need the outline for clipping, you could implement a ViewOutlineProvider to set an Outline with zero alpha. However, if you need all of these things just without the shadow glitch, it seems that a workaround will be required since the platform apparently offers no way to fix that otherwise.*

This answer was originally a handful of "last resort"-type workarounds that were put together under my initial (incorrect) presumption that, except for a few high-level attributes, the shadows API was essentially inaccessible from the SDK. I cannot rightly recommend those old approaches anymore, but the overall method that I ended up with is a bit more complex than I'd intended for this post. However, neither can I rightly turn this answer into just an advertisement for the utility library I've put together from all of this, so I'm going to demonstrate the two core techniques I use to obtain clipped shadows, to give you something that you can get your hands on here, if you'd rather not fiddle with some shady stranger's unvetted GitHub repo.


* After I'd figured out a decently robust technique for the general case, I created this post in order to share those findings. The question there has my reasoning regarding the platform statement, along with links to several other Stack Overflow posts with the same core issue, and the same lack of an actual fix.

Overview

Though the native shadows are pretty limited, Android's procedure for calculating and rendering them is relatively complex: two light sources are considered, an ambient and a spot; the shadows must adjust for any transformations that their Views might undergo, like scaling and rotation; newer versions support separate colors for each light source, per View; etc. For these and other reasons, I decided that it would be preferable to somehow copy the native shadows and clip those, rather than trying to draw correct ones ourselves from scratch.

This leads to the reason for the two separate techniques: the intrinsic shadows are a resultant property of the RenderNode class, but that's not available in the SDK until API level 29 (docs). There are ways to use the equivalent class on older versions, but those are outside the scope of this answer, so an alternate method that uses empty Views is shown here to cover all relevant Android versions in these hands-on samples. The library uses this empty View method as a fallback, should there be any problems using RenderNodes on API levels 28 and below.

For the sake of illustration, we will use the following simplified layout, which we'll imagine ends up looking exactly like the example image above:

<FrameLayout
    android:id="@+id/parent"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@drawable/tower">

    <View
        android:id="@+id/target"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_margin="20dp"
        android:background="@drawable/shape_round_rectangle"
        android:backgroundTint="#602B608A"
        android:elevation="15dp"
        android:outlineProvider="background" />

</FrameLayout>

The tower image is available here, and the shape_round_rectangle drawable is simply:

<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="#FFFFFFFF" />
    <corners android:radius="@dimen/corner_radius" />
</shape>

Where corner_radius is 2dp, in case you want your tests to look similar to the images here for comparison purposes. These values are all incidental, however, and you can use whatever you like, really.

Each example assumes that parent and target are assigned to appropriate vals of the same names; e.g.:

val parent = findViewById<ViewGroup>(R.id.parent)
val target = findViewById<View>(R.id.target)

We're going to draw our clipped shadows in parent's ViewGroupOverlay, as this seems the most straightforward way to compactly demonstrate a general approach. Technically, this should work anywhere that you have access to a hardware-accelerated Canvas – e.g., in a custom ViewGroup's dispatchDraw() override – though it may need some refactoring to apply it elsewhere.

Before beginning with the RenderNode section, it should be noted that each technique needs three basic things:

  • An Outline object to describe the shadow's shape,
  • A Path object of the same shape, to clip out the interior,
  • Some mechanism by which to draw the shadow.

To that end, there is quite a bit of repetition across the two versions in order to keep them straightforward and plainly explanatory. You can obviously rearrange and consolidate things as you see fit, should you be implementing these side by side.

Method #1: RenderNode shadows

(For the sake of brevity, this section assumes Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q where needed.)

For the RenderNode version on API levels 29 and up, Canvas offers the drawRenderNode() function, so we can handle the clipping and drawing both directly. As mentioned, we're doing this in the parent's overlay, and we can get access to the Canvas there with a simple Drawable subclass:

@RequiresApi(Build.VERSION_CODES.Q)
class ClippedShadowDrawable : Drawable() {
    private val outline = Outline().apply { alpha = 1.0F }
    private val clipPath = Path()
    private val renderNode = RenderNode("ClippedShadowDrawable")

    fun update(
        left: Int, top: Int, right: Int, bottom: Int,
        radius: Float, elevation: Float
    ) {
        setBounds(left, top, right, bottom)

        clipPath.rewind()
        clipPath.addRoundRect(
            left.toFloat(),
            top.toFloat(),
            right.toFloat(),
            bottom.toFloat(),
            radius,
            radius,
            Path.Direction.CW
        )

        outline.setRoundRect(0, 0, right - left, bottom - top, radius)
        renderNode.setOutline(outline)
        renderNode.setPosition(left, top, right, bottom)
        renderNode.elevation = elevation

        invalidateSelf()
    }

    override fun draw(canvas: Canvas) {
        if (!canvas.isHardwareAccelerated) return

        canvas.save()
        canvas.enableZ()
        canvas.clipOutPath(clipPath)
        canvas.drawRenderNode(renderNode)
        canvas.disableZ()
        canvas.restore()
    }

    // Unused
    override fun getOpacity() = PixelFormat.TRANSLUCENT
    override fun setAlpha(alpha: Int) {}
    override fun setColorFilter(colorFilter: ColorFilter?) {}
}

In addition to the aforementioned Outline and Path objects, we use a RenderNode here for the actual shadow draw. We also define an update() function that takes relevant values from the target View as a simple way to refresh those properties as needed.

You can see that the magic happens in the draw() override, and I think the code there explains itself quite clearly, though I will mention that the en/disableZ() functions basically tell the underlying native routine that the RenderNode should cast a shadow, if it qualifies.

To put it to use, we'll wire it up to the parent and target described in the Overview section above like so:

val clippedShadowDrawable = ClippedShadowDrawable()

target.setOnClickListener {
    if (target.tag != clippedShadowDrawable) {
        target.outlineProvider = null
        parent.overlay.add(clippedShadowDrawable)
        clippedShadowDrawable.update(
            target.left,
            target.top,
            target.right,
            target.bottom,
            resources.getDimension(R.dimen.corner_radius),
            target.elevation
        )
        target.tag = clippedShadowDrawable
    } else {
        target.outlineProvider = ViewOutlineProvider.BACKGROUND
        parent.overlay.remove(clippedShadowDrawable)
        target.tag = null
    }
}

The OnClickListener on the target will allow you to easily toggle the fix in order to compare and contrast its effect. We set the target's tag to the ClippedShadowDrawable as a convenient flag to indicate whether the fix is currently enabled, hence the if (target.tag != clippedShadowDrawable) check first thing.

To turn the fix on, we need to:

  • Set the target's ViewOutlineProvider to null, to disable the intrinsic shadow,
  • Add our custom Drawable to the parent's overlay,
  • Call its update() function,
  • Set the target's tag to the ClippedShadowDrawable instance as our on flag.

We turn it off by:

  • Restoring the original ViewOutlineProvider on the target,
  • Removing our custom Drawable from the parent's overlay,
  • Setting the tag back to null, flagging it as off.

That's all there is to it, but remember that this is demonstrating only the very basic core technique. The static setup and known values make accounting for the target's state trivial, but things get complicated quickly if you need additional behaviors like adjusting for button presses or animations.

Method #2: View shadows

(Though this one works on all applicable versions, these shadows didn't exist before Lollipop, so you might need some SDK_INT checks for that.)

This approach is a little roundabout. Since we don't have direct access to RenderNodes here, we instead use an empty View for its intrinsic shadow. Unfortunately, we can't just directly draw() a View ourselves and have its shadow work as we would like, so we handle this somewhat passively: we add the empty View to a parent ViewGroup to let it draw via the normal routine while we clip and restore around that in the parent's dispatchDraw().

To that end, our ClippedShadowView itself is actually a custom ViewGroup with another View inside:

class ClippedShadowView(context: Context) : ViewGroup(context) {
    private val outline = Outline().apply { alpha = 1.0F }
    private val clipPath = Path()
    private val shadowView = View(context)

    init {
        addView(shadowView)
        shadowView.outlineProvider = object : ViewOutlineProvider() {
            override fun getOutline(view: View, outline: Outline) {
                outline.set(this@ClippedShadowView.outline)
            }
        }
    }

    fun update(
        left: Int, top: Int, right: Int, bottom: Int,
        radius: Float, elevation: Float
    ) {
        clipPath.rewind()
        clipPath.addRoundRect(
            left.toFloat(),
            top.toFloat(),
            right.toFloat(),
            bottom.toFloat(),
            radius,
            radius,
            Path.Direction.CW
        )

        outline.setRoundRect(0, 0, right - left, bottom - top, radius)
        shadowView.layout(left, top, right, bottom)
        shadowView.elevation = elevation

        shadowView.invalidate()
    }

    override fun dispatchDraw(canvas: Canvas) {
        if (!canvas.isHardwareAccelerated) return

        canvas.save()
        clipOutPath(canvas, clipPath)
        super.dispatchDraw(canvas)
        canvas.restore()
    }

    private fun clipOutPath(canvas: Canvas, path: Path) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            canvas.clipOutPath(path)
        } else {
            @Suppress("DEPRECATION")
            canvas.clipPath(path, Region.Op.DIFFERENCE)
        }
    }

    // Unused
    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {}
}

Unfortunately, there's no way to simply set an Outline on a View, so we have to use a custom ViewOutlineProvider on the shadowView to deliver it through that mechanism instead. After the Outline is refreshed in update(), the shadowView is invalidated, which will cause it to call through to the provider for the updated Outline.

In the dispatchDraw() override, we clip out the Path before calling the super function to let ViewGroup draw the shadowView as it normally would.

We wire this up in almost exactly the same way as the RenderNode setup, but with an additional call to clippedShadowView.layout(), since that's not laid out anywhere else:

val clippedShadowView = ClippedShadowView(target.context)

target.setOnClickListener {
    if (target.tag != clippedShadowView) {
        target.outlineProvider = null
        parent.overlay.add(clippedShadowView)
        clippedShadowView.layout(0, 0, parent.width, parent.height)
        clippedShadowView.update(
            target.left,
            target.top,
            target.right,
            target.bottom,
            resources.getDimension(R.dimen.corner_radius),
            target.elevation
        )
        target.tag = clippedShadowView
    } else {
        target.outlineProvider = ViewOutlineProvider.BACKGROUND
        parent.overlay.remove(clippedShadowView)
        target.tag = null
    }
}

Aftermath

Both methods result in exactly the same thing, taking into consideration the fact that shadows will vary slightly with their screen positions, due to the two-source rendering model:

Screenshot of examples of both fixes, and an example with a fully transparent target.

The top image shows the RenderNode fix in action, the middle is the View method, and the bottom one is what either would look like with a completely transparent target (or no target at all, actually) to make it easier to see exactly how the shadow is being clipped.

ZedAlpha
  • 219
  • 2
  • 8
  • 1
    You did a fantastic job! So much things to research just to be able to do shadow on transparent views. Google should add something native for this goal. – Maxdestroyer Apr 20 '23 at 10:50
-1

I had the similar issue. However, it all worked when I removed the line cardBackgroundColor and used android:background="@color/transparent. When you add or set the cardBackgroundColor, by default there is slight elevation which causes shadow opaque effect.

Zahra
  • 2,231
  • 3
  • 21
  • 41