'Android CardView with weird border when transparent

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>


Solution 1:[1]

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"

Solution 2:[2]

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

Solution 3:[3]

That effect is not specific to CardView, but rather is an artifact of the method Android uses to render and transform elevation 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, and a ViewOutlineProvider that gives an Outline with a non-zero alpha. For example, 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 return 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", if you'd rather not use 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 method for calculating and rendering them is relatively complex: two light sources are considered, an ambient and a spot; the shadows must undergo the same transformations that the Views do, like scaling and rotation; newer versions support separate colors for each 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 methods: 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 instead is presented 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 this simple layout throughout, which we'll imagine ends up looking exactly like our 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="@color/translucent_blue"
        android:elevation="15dp"
        android:outlineProvider="background" />

</FrameLayout>

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)

The shape_round_rectangle drawable is simply:

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

The corner_radius value is 2dp, translucent_blue is #602B608A, and the tower image is available here, 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.

For these examples, we're going to draw our copies 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 example, it should be noted that each method 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,
  • And some mechanism by which to draw the shadow.

To that end, there is quite a bit of code repetition across the two examples 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 method 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) {
        clipPath.reset()
        clipPath.addRoundRect(
            left.toFloat(),
            top.toFloat(),
            right.toFloat(),
            bottom.toFloat(),
            radius,
            radius,
            Path.Direction.CW
        )

        setBounds(left, top, right, bottom)

        renderNode.setPosition(left, top, right, bottom)
        outline.setRoundRect(0, 0, right - left, bottom - top, radius)
        renderNode.setOutline(outline)
        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()
    }

    override fun setAlpha(alpha: Int) {}

    override fun setColorFilter(colorFilter: ColorFilter?) {}

    override fun getOpacity() = PixelFormat.TRANSLUCENT
}

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 all of those objects as needed.

You can see that the magic happens in the draw() override, and I think that the code there explains itself quite clearly, though I will mention that the en/disableZ() functions are basically what tell the underlying native routine that this RenderNode should draw 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 = ViewOutlineProvider.BACKGROUND
        parent.overlay.remove(clippedShadowDrawable)
        target.tag = null
    } else {
        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
    }
}

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 simple flag to indicate whether the fix is currently enabled; hence the if (target.tag == clippedShadowDrawable) check first thing.

If the fix is currently enabled, we turn it off by:

  • Restoring the original ViewOutlineProvider on the target,
  • Removing our custom Drawable from the parent's overlay,
  • And setting the tag back to null.

To turn it on instead, we need to:

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

That's all there is to it, but remember that this is demonstrating only the very basic core technique. The static setup and the click listener make accounting for the target's current 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 method works on all applicable versions, these shadows didn't exist before Lollipop, so you might need some SDK_INT checks for that.)

This method is a little roundabout: since we don't have direct access to RenderNodes here, we use an empty View for its intrinsic shadow instead. Unfortunately, we can't just directly draw() a View ourselves and have its shadow work as we would like, so we have to do this passively; i.e., we have to add that View to a parent to let it draw in the normal routine, while we do the clipping 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([email protected])
            }
        }
    }

    fun update(left: Int, top: Int, right: Int, bottom: Int, radius: Float, elevation: Float) {
        clipPath.reset()
        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

        val count = canvas.save()
        clipOutPath(canvas, clipPath)
        super.dispatchDraw(canvas)
        canvas.restoreToCount(count)
    }

    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)
        }
    }

    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 shadow View to deliver it through that mechanism instead. After the Outline is refreshed in update(), the shadow View 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 method to let ViewGroup draw the shadow child as it normally would.

We wire this up in very much the same way as the RenderNode setup, except for one important extra step: the clippedShadowView.layout() call. We are responsible for laying out anything we put into a View's overlay, so we must do that ourselves. The RenderNode method's Drawable is being sized correctly with the setBounds() call in its update(), and similarly here for the shadow View with the shadowView.layout() call, but the ClippedShadowView itself is not laid out anywhere else.

val clippedShadowView = ClippedShadowView(target.context)

target.setOnClickListener {
    if (target.tag == clippedShadowView) {
        target.outlineProvider = ViewOutlineProvider.BACKGROUND
        parent.overlay.remove(clippedShadowView)
        target.tag = null
    } else {
        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
    }
}

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, just to make it easier to see exactly how the shadow is being clipped.

Solution 4:[4]

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.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Prateek Gupta
Solution 2 Sergey Zabelnikov
Solution 3
Solution 4 Zahra