hello, more internal stuff question. Is there way ...
# compose
m
hello, more internal stuff question. Is there way to force redraw
LayoutNode
? My usecase is that drawing params (shader) has change but compose don't know about that. So I would like to force a redraw on next frame a composable function.
z
Can you post your code? Updating anything affecting drawing should trigger a redraw on the next frame.
m
I'm trying out new runtime shaders
Copy code
val shader = remember {
        RuntimeShader(SHADER_COLOR)
            .apply { setFloatUniform("iDuration", DURATION) }
    }
    val brush = remember { ShaderBrush(shader) }

    val infiniteAnimation = remember {
        infiniteRepeatable<Float>(
            tween(DURATION.toInt(), easing = LinearEasing),
            RepeatMode.Restart
        )
    }
    val timePassed by rememberInfiniteTransition().animateFloat(
        initialValue = 0f,
        targetValue = DURATION,
        animationSpec = infiniteAnimation
    )
    shader.setFloatUniform("iTime", timePassed)
and here last line wont trigger proper invalidation
my current best hack is
Copy code
Text(
        modifier = Modifier.alpha(1 - (timePassed + 1) / 1000 / DURATION),
        text = text,
        fontFamily = shaderFont,
        onTextLayout = {
            shader.setFloatUniform(
                "iResolution",
                it.size.width.toFloat(),
                it.size.height.toFloat()
            )
        }
    )
to change alpha slightly
s
I guess the most optimal would be to just read
timePassed
state in a draw modifier lambda, which should invalidate draw directly It doesn't have to do anything, just make sure you mention
timePassed
in it
m
I didn't make it obvious but I'm using that brush as my
text
in
Text
Copy code
buildAnnotatedString {
    withStyle(SpanStyle(brush = brush)) {
        append(msg)
    }
}
probably I should just go with
canvas.drawText
in drawModifier as you suggest to make it work, I just thought it can be somehow done usual way
s
You can do it the usual way, and you don't have to draw text directly Draw modifier can just draw nothing and invalidate drawing, just by doing something like
Copy code
.drawBehind {
    timePassed // assuming time passed is a mutable state, just referencing it here is enough to invalidate draw
}
It is quite a bit hacky, but useful to know sometimes :)
m
yeah, I tried that before, for some reason it's not working
Copy code
.drawBehind {
                drawLine(brush, Offset(0f,0f), Offset(timePassed, timePassed))
            },
it draws lines but shader is not updating properly
well it updates for the line, not for the text
message has been deleted
s
Interesting, maybe it doesn't invalidate the correct layer or something...
m
exactly, it has to be some other layer/node/stuff
event this doesn't work
Copy code
.drawWithContent {
                drawLine(brush, Offset(0f,0f), Offset(timePassed, timePassed))
                drawContent()
            },
and without this drawContent text is not drawn, so I'm guessing this is the same layer
s
Hm, I am not an expert in drawing, but brush might be caching uniforms It used to work without changing the brush though with alpha modifier, so maybe there's something else that should affect this layer here I wonder how other shader examples update their uniforms tbh, maybe there's something missing here :)
z
Cc’ing @Halil Ozercan who worked on some text brush support
h
Hey @Mikołaj Kąkol. We are aware of this problem that exists in Brush support for text. Basically animating the shader part of a ShaderBrush may have unexpected behavior. The reason is that BasicText checks whether brush has actually changed to trigger recreating the shader. In this case the brush object doesn't change, it's even the same instance. There is a hacky way for now to get this to work. Simply create your own ShaderBrush by extending it like
object : ShaderBrush
. Then override the equals function to always return false or make a comparison to an older state.
m
hi, yep it worked this way, but it might be inefficient because all text messurments need to be taken again, my
onTextLayout
is called in multiple time, while this alfa trick doesn’t invalidates layout result
h
AFAIK this trick shouldn't measure text multiple times.
onTextLayout
being called multiple times is not a guaranteed sign of multiple measures. Even if you change brush completely, like creating a new one in each composition, Text internally should reuse the layout and apply the new brush in draw phase. Except if you are applying brush as a SpanStyle in annotatedstring.
m
yes I’m using SpanStyle, is there another way to set brush?
oh I see how…
h
Since Brush is experimental it's not part of the material
Text
composable. I'm not sure it will ever be even if it graduates to a stable API. Best way to use Brush for entirety of Text composable is to send it through like the following
Copy code
style = LocalTextStyle.current.copy(brush = ...)
m
it’s super tricky 😄
Copy code
Text(
        text = msg,
        fontFamily = shaderFont,
        style = remember { TextStyle(brush = brush) },
        onTextLayout = {
            shader.setFloatUniform(
                "iResolution",
                it.size.width.toFloat(),
                it.size.height.toFloat()
            )
        }
    )
this doesn’t animate
Copy code
Text(
        text = msg,
        fontFamily = shaderFont,
        style = TextStyle(brush = brush),
        onTextLayout = {
            shader.setFloatUniform(
                "iResolution",
                it.size.width.toFloat(),
                it.size.height.toFloat()
            )
        }
    )
this does, but calls onTextLayout every time
As you said, maybe it is performant, I’ll be writing benchmarks for that, I’ll share blog post one time. It’s not crucial for us right now, just providing some feedback.
h
if you are looking to set the resolution according to text layout size, you should be using the
size
parameter from
ShaderBrush.createShader
. Text stack will call that function with the proper size after layout happens. This means that you won't even lose a frame because the result of onTextLayout always lags behind a single frame. I came up with this solution that should work hack-free* https://gist.github.com/halilozercan/a6fb2b9977b386f9ad6ca6ce7cf3c72f
There are multiple things that need to happen to invalidate the drawing phase. One simple thing that does this without being obvious is recreating the Brush at each composition.
ShaderBrush()
function creates an anonymous instance which does not override
equals
, meaning that each time this function is called, it's going to create a new unequal Brush instance. This is going to cause a re-draw and the internal shader will be reused albeit with the new time parameter applied on it. If you happen to
remember
the Brush, those unequal new instances won't happen, redraw won't happen, latest state of the shader won't be applied. Something similar is also true when Brush is applied on Text. As long as Brush stays the same, redraw will be skipped. So, we need a way to tell the framework that internals of brush has changed. This will trigger a re-draw where brush is used (createShader is called). Hence, the weird
setTime
function in my implementation. It recreates the Brush with new time parameter but reuses the existing shader, passing it along to the next Brush.
m
nice, thank you, I’ll check it 😉