https://kotlinlang.org logo
#compose
Title
# compose
h

Humaira Naeem Pasha

03/09/2023, 12:27 PM
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

efemoney

03/10/2023, 7:51 AM
Hmm, this is interesting and I have no idea, pinging @Chris Sinco [G] 😅 as I’m curious myself
c

Chris Sinco [G]

03/10/2023, 7:55 AM
I think @romainguy would know more
r

romainguy

03/10/2023, 4:36 PM
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

Nader Jawad

03/10/2023, 4:52 PM
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

romainguy

03/10/2023, 5:16 PM
@Nader Jawad but that fill filter the rect itself, not what’s underneath
n

Nader Jawad

03/10/2023, 5:23 PM
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

romainguy

03/10/2023, 5:37 PM
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

Nader Jawad

03/10/2023, 6:23 PM
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

Humaira Naeem Pasha

03/10/2023, 7:14 PM
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

romainguy

03/10/2023, 8:02 PM
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

Humaira Naeem Pasha

03/11/2023, 6:41 PM
@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

romainguy

03/11/2023, 6:43 PM
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

Humaira Naeem Pasha

03/11/2023, 6:56 PM
makes sense - thanks a lot! 🙏
9 Views