Hi I’m trying to implement disabled state for a ca...
# compose
h
Hi I’m trying to implement disabled state for a card component. It involves setting the entire component to grayscale with an alpha of 0.8f. The Figma layer shows blendMode as saturation. So I tried doing something like;
Copy code
Surface(
 modifier = modifier.then(Modifier.withGridWidth(gridWidth)).drawWithCache {
  onDrawWithContent {
   drawContent()
   drawRect(backgroundColor, blendMode = BlendMode.Saturation)
  }
 }.graphicsLayer { alpha = 0.8f }
This does the job but the BlendMode.Saturation is available only API 29 and onwards. Is there any workaround to this please? (Attaching the screenshot for the component)
e
Hmm, this is interesting and I have no idea, pinging @Chris Sinco [G] 😅 as I’m curious myself
c
I think @romainguy would know more
r
With Views you can achieve this fairly easily by using a hardware layer and setting a
ColorMatrixColorFilter
on the layer’s paint to set saturation to 0. Compose does have a
ColorFilter
(https://developer.android.com/reference/kotlin/androidx/compose/ui/graphics/ColorFilter) but unfortunately it seems we don’t let
graphicsLayer
hold a color filter. We should fix this. @Nader Jawad Any thoughts?
A workaround is to go through a bitmap and use the paint’s color filter when blitting the bitmap
n
There are quite a few similar APIs to accomplish similar goals. In the framework the most recent is RenderEffect which is Android 12+. For the sake of compose's RenderEffect API, we may end up back porting a few of these instances using ColorFilter on a Paint that is consumed on View's setLayerPaint API
But for the individual draw call you can configure a ColorFilter directly on the drawRect call to achieve something similar
r
@Nader Jawad but that fill filter the rect itself, not what’s underneath
n
That's right but this is also what the original sample code above is doing
If the goal is to apply it while drawing a bitmap it would be more efficient to apply the ColorFilter directly in that drawBitmap call than on the graphicsLayer itself
I filed a bug internally to make this configurable on a graphicsLayer in the interim
r
No because the code above uses a blend mode so the desaturation affects what’s underneath
and I assume it applied in this case to the star, etc.
n
Another workaround in the meantime could be to reach into the native canvas and use a saveLayerPaint around the drawContent method invocation then restore afterwards
But if the UI components are not overlapping then the effect could be applied on each call similar to how we modulate alpha for non overlapping rendering
h
I was able to achieve a somewhat similar grayscale effect by using a colormatrix this article discusses how to preserve the whites in the source.
Copy code
Modifier.drawWithCache {
  onDrawWithContent {
    val saturationMatrix = ColorMatrix(
      floatArrayOf(
        0.18f, 0.18f, 0.18f, 0f, 118f,
        0.18f, 0.18f, 0.18f, 0f, 118f,
        0.18f, 0.18f, 0.18f, 0f, 118f,
        0f, 0f, 0f, 0.8f, 0f
      )
    )

    val saturationFilter = ColorFilter.colorMatrix(saturationMatrix)
    val paint = Paint().apply {
      colorFilter = saturationFilter
    }
    drawIntoCanvas { canvas ->
      canvas.saveLayer(Rect(0f, 0f, size.width, size.height), paint)
      drawContent()
      canvas.restore()
    }
  }
}
I'm not completely sure about this tho - thoughts on this approach please?
this is the difference between the two implementations - the top one is blendMode.Saturation & the bottom is the one achieved via colorMatrix
r
The matrix you are using isn’t a correct way to preserve tones
setSaturation
uses the proper luminance weights
And it preserves whites…
The matrix you want is:
floatArrayOf(0.213f, 0.715f, 0.072f, 0f, 0f, 0.213f, 0.715f, 0.072f, 0f, 0f, 0.213f, 0.715f, 0.072f, 0f, 0f, 0f, 0f, 0f, 1f, 0f)
(hopefully I typed it correctly 🙂
h
@romainguy thanks a ton for the help - can you briefly describe how did you come up with this matrix please? would like to know if this matrix should be derived from the color scale i'm trying to set the component to (for my case its #2D2E2E
r
0.213, 0.715, 0.072 are the luminance weights for the sRGB color space
The unequal weights come from the fact our eyes don’t have the same sensitivity to all wavelengths (we are more sensitive to “green”)
This should be what ColorMatrix does when you call setSaturation on it
Anyway this matrix simply sets each rgb channel to the same value: the luminance of the source color
And it’ll preserve whites since the sum of the weights is 1
(And there’s no lift)
h
makes sense - thanks a lot! 🙏