$30 off During Our Annual Pro Sale. View Details »

Custom Drawing in Compose - Londroid December 2022

Custom Drawing in Compose - Londroid December 2022

Presented at Londroid 2022, custom drawing in Compose covers examples of how to draw custom designs in your Jetpack Compose Apps.

Covering things like:
Modifier.drawBehind
Modifier.drawWithContent
Modifier.drawWithCache
AGSL shaders, Paths and Animations.

Rebecca Franks

December 08, 2022
Tweet

More Decks by Rebecca Franks

Other Decks in Technology

Transcript

  1. Custom Drawing in
    Compose
    Rebecca Franks (she/her)


    Developer Relations Engineer


    @riggaroo

    View Slide

  2. goo.gle/compose-jetlagged-talk

    View Slide

  3. View Slide

  4. goo.gle/compose-jetlagged

    View Slide

  5. • Offset + Size of each rect is based on
    duration of sleep period


    • Rounded Rectangles joined together
    with lines in between periods


    • Gradient across the expanded bars
    Custom Drawing
    Sleep Score Emoji
    Sleep Periods


    with different durations

    View Slide

  6. Drawing in Compose
    Canvas(


    onDraw = {


    drawRoundRect(Color.Blue)


    }


    )
    Canvas Composable Modifiers
    Modifier.drawBehind { }


    Modifier.drawWithContent { }


    Modifier.drawWithCache { }


    View Slide

  7. Canvas(


    onDraw = {


    drawRoundRect(Color.Blue)


    }


    )
    Canvas Composable

    View Slide

  8. @Composable


    fun Canvas(modifier: Modifier, onDraw: DrawScope.() -> Unit) =


    Spacer(modifier.drawBehind(onDraw))


    Canvas implementation
    Just a spacer 🕵

    View Slide

  9. Drawing in Compose 🎨
    Draws behind the Composables
    content.


    Can specify the order of draw
    calls.


    Modifier.drawBehind Modifier.drawWithContent
    Objects that are used during
    drawing are cached, until size
    changes or state variables read
    inside change.


    Modifier.drawWithCache

    View Slide

  10. • Top left has the coordinate of [0,0]


    • Bottom right has the coordinate of [width, height].


    • Draw calls are performed with pixel measurements
    (not dp).


    • Relative to the parent Composable
    Co-ordinate System
    [width,height]
    [0,0] X axis
    Y axis
    x
    y
    [0,height]
    [width,0]

    View Slide

  11. @Composable


    fun SleepBar(


    sleepData: SleepDayData,


    modifier: Modifier = Modifier


    ) {


    Spacer(


    modifier = modifier


    .drawWithCache {


    onDrawBehind {


    // this: DrawScope


    }


    }


    .fillMaxWidth()


    .height(48.dp)


    )


    }


    View Slide

  12. DrawScope
    Declarative, stateless drawing API to draw shapes, paths etc without needing to
    maintain your own state.

    View Slide

  13. interface DrawScope {


    val center: Offset


    val size: Size


    fun drawLine()


    fun drawRect()


    fun drawImage()


    fun drawRoundRect()


    fun drawPath()


    // etc


    }

    View Slide

  14. @Composable


    fun SleepBar(


    sleepData: SleepDayData,


    modifier: Modifier = Modifier


    ) {


    Spacer(


    modifier = modifier


    .drawWithCache {


    onDrawBehind {


    // this: DrawScope


    }


    }


    .fillMaxWidth()


    .height(24.dp)


    )


    }


    View Slide

  15. @Composable


    fun SleepBar(


    sleepData: SleepDayData,


    modifier: Modifier = Modifier


    ) {


    Spacer(


    modifier = modifier


    .drawWithCache {




    onDrawBehind {




    }


    }


    .fillMaxWidth()


    .height(24.dp)


    )


    }


    View Slide

  16. @Composable


    fun SleepBar(


    sleepData: SleepDayData,


    modifier: Modifier = Modifier


    ) {


    Spacer(


    modifier = modifier


    .drawWithCache {


    val brush =


    Brush.verticalGradient(listOf(YellowVariant, Yellow /*etc*/))


    onDrawBehind {


    drawRoundRect(brush, cornerRadius = CornerRadius(10.dp.toPx()))


    }


    }


    .fillMaxWidth()


    .height(24.dp)


    )


    }


    View Slide

  17. fun drawRoundRect(


    brush: Brush,


    topLeft: Offset = Offset.Zero,


    size: Size = this.size.offsetSize(topLeft),


    cornerRadius: CornerRadius = CornerRadius.Zero,


    alpha: Float = 1.0f,


    style: DrawStyle = Fill,


    colorFilter: ColorFilter? = null,


    blendMode: BlendMode = DefaultBlendMode


    )
    Default values

    View Slide

  18. View Slide

  19. View Slide

  20. Draw Text ✍
    From
    Com
    pose 1.3+

    View Slide

  21. @Composable


    fun SleepBar(


    sleepData: SleepDayData,


    modifier: Modifier = Modifier


    ) {


    Spacer(


    modifier = modifier


    .drawWithCache {


    val brush =


    Brush.verticalGradient(listOf(YellowVariant, Yellow /*etc*/))


    onDrawBehind {


    drawRoundRect(brush, cornerRadius = CornerRadius(10.dp.toPx()))


    }


    }


    .fillMaxWidth()


    .height(24.dp)


    )


    }


    View Slide

  22. @Composable


    fun SleepBar(


    sleepData: SleepDayData,


    modifier: Modifier = Modifier


    ) {


    Spacer(


    modifier = modifier


    .drawWithCache {


    val brush =


    Brush.verticalGradient(listOf(YellowVariant, Yellow /*etc*/))


    onDrawBehind {


    drawRoundRect(brush, cornerRadius = CornerRadius(10.dp.toPx()))


    }


    }


    .fillMaxWidth()


    .height(24.dp)


    )


    }


    View Slide

  23. @Composable


    fun SleepBar(


    sleepData: SleepDayData,


    modifier: Modifier = Modifier


    ) {


    val textMeasurer = rememberTextMeasurer()




    Spacer(


    modifier = modifier


    .drawWithCache {


    val brush =


    Brush.verticalGradient(listOf(YellowVariant, Yellow /*etc*/))




    onDrawBehind {


    drawRoundRect(brush, cornerRadius = CornerRadius(10.dp.toPx()))




    }


    }


    .fillMaxWidth()


    .height(24.dp)


    )


    }


    View Slide

  24. @Composable


    fun SleepBar(


    sleepData: SleepDayData,


    modifier: Modifier = Modifier


    ) {


    val textMeasurer = rememberTextMeasurer()




    Spacer(


    modifier = modifier


    .drawWithCache {


    val brush =


    Brush.verticalGradient(listOf(YellowVariant, Yellow /*etc*/))


    val textResult = textMeasurer.measure(AnnotatedString("😴"))


    onDrawBehind {


    drawRoundRect(brush, cornerRadius = CornerRadius(10.dp.toPx()))




    }


    }


    .fillMaxWidth()


    .height(24.dp)


    )


    }


    View Slide

  25. @Composable


    fun SleepBar(


    sleepData: SleepDayData,


    modifier: Modifier = Modifier


    ) {


    val textMeasurer = rememberTextMeasurer()




    Spacer(


    modifier = modifier


    .drawWithCache {


    val brush =


    Brush.verticalGradient(listOf(YellowVariant, Yellow /*etc*/))


    val textResult = textMeasurer.measure(AnnotatedString("😴"))


    onDrawBehind {


    drawRoundRect(brush, cornerRadius = CornerRadius(10.dp.toPx()))


    drawText(textResult)


    }


    }


    .fillMaxWidth()


    .height(24.dp)


    )


    }


    View Slide

  26. View Slide

  27. View Slide

  28. View Slide

  29. Animate height on
    click ✨

    View Slide

  30. @Composable


    fun SleepBar(


    sleepData: SleepDayData,


    modifier: Modifier = Modifier


    ) {


    val textMeasurer = rememberTextMeasurer()




    Spacer(


    modifier = modifier


    .drawWithCache {


    // Our drawing code


    }


    .fillMaxWidth()


    .height(24.dp)


    )


    }


    View Slide





  31. Spacer(


    modifier = modifier


    .drawWithCache {


    // Our drawing code


    }


    .fillMaxWidth()


    .height(24.dp)


    )


    }


    View Slide





  32. var isExpanded by remember {


    mutableStateOf(false)


    }


    Spacer(


    modifier = modifier


    .drawWithCache {


    // Our drawing code


    }


    .fillMaxWidth()


    .height(24.dp)


    )


    }


    View Slide





  33. var isExpanded by remember {


    mutableStateOf(false)


    }


    Spacer(


    modifier = modifier


    .drawWithCache {


    // Our drawing code


    }


    .fillMaxWidth()


    .height(24.dp)


    )


    }


    View Slide





  34. var isExpanded by remember {


    mutableStateOf(false)


    }


    Spacer(


    modifier = modifier


    .drawWithCache {


    // Our drawing code


    }


    .fillMaxWidth()


    .height(24.dp)


    .clickable {


    isExpanded = !isExpanded


    }


    )


    }


    View Slide





  35. var isExpanded by remember {


    mutableStateOf(false)


    }


    Spacer(


    modifier = modifier


    .drawWithCache {


    // Our drawing code


    }


    .fillMaxWidth()


    .height(24.dp)


    .clickable {


    isExpanded = !isExpanded


    }


    )


    }


    View Slide





  36. var isExpanded by remember {


    mutableStateOf(false)


    }


    val transition = updateTransition(targetState = isExpanded)


    val height by transition.animateDp { targetExpanded ->


    if (targetExpanded) 110.dp else 24.dp


    }


    Spacer(


    modifier = modifier


    .drawWithCache {


    // Our drawing code


    }


    .fillMaxWidth()


    .height(24.dp)


    .clickable {


    isExpanded = !isExpanded


    }


    )


    }


    View Slide





  37. var isExpanded by remember {


    mutableStateOf(false)


    }


    val transition = updateTransition(targetState = isExpanded)


    val height by transition.animateDp { targetExpanded ->


    if (targetExpanded) 110.dp else 24.dp


    }


    Spacer(


    modifier = modifier


    .drawWithCache {


    // Our drawing code


    }


    .fillMaxWidth()


    .height(height)


    .clickable {


    isExpanded = !isExpanded


    }


    )


    }


    View Slide

  38. View Slide

  39. Text Offset
    Animation

    View Slide





  40. var isExpanded by remember {


    mutableStateOf(false)


    }


    val transition = updateTransition(targetState = expanded)


    val height by transition.animateDp { targetExpanded ->


    if (targetExpanded) 110.dp else 24.dp


    }


    Spacer(


    modifier = Modifier


    .drawWithCache {


    val brush =


    Brush.verticalGradient(listOf(YellowVariant, Yellow /*etc*/))


    val textResult = textMeasurer.measure(AnnotatedString("😴"))


    onDrawBehind {


    drawRoundRect(brush, cornerRadius = CornerRadius(10.dp.toPx()))


    drawText(textResult)


    }


    }


    .fillMaxWidth()


    .height(height)


    .clickable {



    View Slide



  41. val transition = updateTransition(targetState = expanded)


    val height by transition.animateDp { targetExpanded ->


    if (targetExpanded) 110.dp else 24.dp


    }


    Spacer(


    modifier = Modifier


    .drawWithCache {


    val brush =


    Brush.verticalGradient(listOf(YellowVariant, Yellow /*etc*/))


    val textResult = textMeasurer.measure(AnnotatedString("😴"))


    onDrawBehind {


    drawRoundRect(brush, cornerRadius = CornerRadius(10.dp.toPx()))


    drawText(textResult)


    }


    }


    .fillMaxWidth()


    .height(height)


    .clickable {


    expanded = !expanded




    View Slide



  42. val transition = updateTransition(targetState = expanded)


    val height by transition.animateDp { targetExpanded ->


    if (targetExpanded) 110.dp else 24.dp


    }


    val textOffset by transition.animateFloat { targetExpanded ->


    if (targetExpanded) 1f else 0f


    }


    Spacer(


    modifier = Modifier


    .drawWithCache {


    val brush =


    Brush.verticalGradient(listOf(YellowVariant, Yellow /*etc*/))


    val textResult = textMeasurer.measure(AnnotatedString("😴"))


    onDrawBehind {


    drawRoundRect(brush, cornerRadius = CornerRadius(10.dp.toPx()))


    drawText(textResult)


    }


    }


    .fillMaxWidth()


    .height(height)


    .clickable {



    View Slide



  43. val transition = updateTransition(targetState = expanded)


    val height by transition.animateDp { targetExpanded ->


    if (targetExpanded) 110.dp else 24.dp


    }


    val textOffset by transition.animateFloat { targetExpanded ->


    if (targetExpanded) 1f else 0f


    }


    Spacer(


    modifier = Modifier


    .drawWithCache {


    val brush =


    Brush.verticalGradient(listOf(YellowVariant, Yellow /*etc*/))


    val textResult = textMeasurer.measure(AnnotatedString("😴"))


    onDrawBehind {


    drawRoundRect(brush, cornerRadius = CornerRadius(10.dp.toPx()))


    drawText(textResult)


    }


    }


    .fillMaxWidth()




    View Slide



  44. val transition = updateTransition(targetState = expanded)


    val height by transition.animateDp { targetExpanded ->


    if (targetExpanded) 110.dp else 24.dp


    }


    val textOffset by transition.animateFloat { targetExpanded ->


    if (targetExpanded) 1f else 0f


    }


    Spacer(


    modifier = Modifier


    .drawWithCache {


    val brush =


    Brush.verticalGradient(listOf(YellowVariant, Yellow /*etc*/))


    val textResult = textMeasurer.measure(AnnotatedString("😴"))


    onDrawBehind {


    drawRoundRect(brush, cornerRadius = CornerRadius(10.dp.toPx()))


    translate(left = -textOffset * textResult.size.width) {


    drawText(textResult)


    }


    }


    }


    .fillMaxWidth()




    View Slide

  45. Draw Paths ➰

    View Slide

  46. View Slide

  47. val path = Path()


    var previousPeriod: SleepPeriod? = null


    sleepData.sleepPeriods.forEach { period ->


    previousPeriod = period


    }


    return path


    View Slide

  48. if (previousPeriod != null) {


    path.lineTo(x = /..., y = /...) // STEP 1


    }


    val path = Path()


    var previousPeriod: SleepPeriod? = null


    sleepData.sleepPeriods.forEach { period ->


    previousPeriod = period


    }


    return path


    View Slide

  49. path.addRect(rect = /...) // STEP 2


    val path = Path()


    var previousPeriod: SleepPeriod? = null


    sleepData.sleepPeriods.forEach { period ->


    if (previousPeriod != null) {


    path.lineTo(x = /..., y = /...) // STEP 1


    }


    previousPeriod = period


    }


    return path


    View Slide

  50. val path = Path()


    var previousPeriod: SleepPeriod? = null


    sleepData.sleepPeriods.forEach { period ->


    if (previousPeriod != null) {


    path.lineTo(x = /..., y = /...) // STEP 1


    }


    path.addRect(rect = /...) // STEP 2


    path.moveTo(x = /..., y = /…) // STEP 3


    previousPeriod = period


    }


    return path


    gradi

    View Slide

  51. val path = Path()


    var previousPeriod: SleepPeriod? = null


    sleepData.sleepPeriods.forEach { period ->


    if (previousPeriod != null) {


    path.lineTo(x = /..., y = /...) // STEP 1


    }


    path.addRect(rect = /...) // STEP 2


    path.moveTo(x = /..., y = /…) // STEP 3


    previousPeriod = period


    }


    return path


    gradi

    View Slide

  52. val path = generateSleepPath(/*pass data*/)


    onDrawBehind {


    drawPath(path, brush = gradientBrush)


    }


    View Slide

  53. val path = generateSleepPath(/*pass data*/)


    onDrawBehind {


    drawPath(path, brush = gradientBrush)


    }


    View Slide

  54. val path = generateSleepPath(/*pass data*/)


    onDrawBehind {


    // default arg: style = Fill


    drawPath(path, brush = gradientBrush)


    }


    View Slide

  55. val path = generateSleepPath(/*pass data*/)


    onDrawBehind {


    // default arg: style = Fill


    drawPath(path, brush = gradientBrush)


    // call draw again with Stroke style


    drawPath(path, brush = gradientBrush


    style = Stroke(/**/),


    )


    }


    View Slide

  56. val path = generateSleepPath(/*pass data*/)


    onDrawBehind {


    // default arg: style = Fill


    drawPath(path, brush = gradientBrush)


    // call draw again with Stroke style


    drawPath(path, brush = gradientBrush


    style = Stroke(lineThicknessPx,


    cap = StrokeCap.Round,


    join = StrokeJoin.Round,


    pathEffect = PathEffect.cornerPathEffect(


    cornerRadiusPx * expandedProgress)


    )


    )


    }


    View Slide

  57. val path = generateSleepPath(/*pass data*/)


    onDrawBehind {


    // default arg: style = Fill


    drawPath(path, brush = gradientBrush)


    // call draw again with Stroke style


    drawPath(path, brush = gradientBrush


    style = Stroke(/**/),


    )


    }


    View Slide

  58. View Slide

  59. View Slide

  60. Custom ShaderBrush
    with AGSL
    Using AGSL and want to use it with DrawScope in Compose…


    From
    Android T+
    • Based on GLSL (OpenGL)


    • Android T+


    • Runs on GPU so very efficient (parallel pixel calculations)

    View Slide

  61. Taken from an


    online shader tool

    View Slide

  62. @Composable


    private fun JetLaggedScreen() {


    Column() {


    JetLaggedHeader()


    JetLaggedSleepSummary()


    }


    JetLaggedHeaderTabs()


    TimeGraph()


    }


    View Slide

  63. @Language("AGSL")
    val SHADER = """
    uniform float2 resolution;
    uniform float time;
    layout(color) uniform half4 color;
    float4 main(in float2 fragCoord) {
    // do some math to calculate colour per pixel
    return float4(color, 1.0);
    }
    """.trimIndent()
    For each pixel in the area,


    main() is called with the


    coordinate of where its calling from

    View Slide

  64. @Composable


    private fun JetLaggedScreen() {


    Column(modifier = Modifier


    .drawWithCache {


    }) {


    JetLaggedHeader()


    JetLaggedSleepSummary()


    }


    }

    View Slide

  65. @Composable


    private fun JetLaggedScreen() {


    Column(modifier = Modifier


    .drawWithCache {


    val shader = RuntimeShader(WAVE_BAR_SHADER_AGSL)




    }) {


    JetLaggedHeader()


    JetLaggedSleepSummary()


    }


    }

    View Slide

  66. @Composable


    private fun JetLaggedScreen() {


    Column(modifier = Modifier


    .drawWithCache {


    val shader = RuntimeShader(WAVE_BAR_SHADER_AGSL)


    val shaderBrush = ShaderBrush(shader)


    }) {


    JetLaggedHeader()


    JetLaggedSleepSummary()


    }


    }

    View Slide

  67. @Composable


    private fun JetLaggedScreen() {


    Column(modifier = Modifier


    .drawWithCache {


    val shader = RuntimeShader(WAVE_BAR_SHADER_AGSL)


    val shaderBrush = ShaderBrush(shader)




    shader.setFloatUniform("iResolution", size.width, size.height)


    }) {


    JetLaggedHeader()


    JetLaggedSleepSummary()


    }


    }

    View Slide

  68. @Composable


    private fun JetLaggedScreen() {


    Column(modifier = Modifier


    .drawWithCache {


    val shader = RuntimeShader(WAVE_BAR_SHADER_AGSL)


    val shaderBrush = ShaderBrush(shader)




    shader.setFloatUniform("iResolution", size.width, size.height)


    onDrawBehind {


    drawRect(shaderBrush)


    }


    }) {


    JetLaggedHeader()


    JetLaggedSleepSummary()


    }


    }

    View Slide

  69. View Slide

  70. @Composable


    private fun JetLaggedScreen() {


    Column(modifier = Modifier


    .drawWithCache {


    val shader = RuntimeShader(WAVE_BAR_SHADER_AGSL)


    val shaderBrush = ShaderBrush(shader)




    shader.setFloatUniform("resolution", size.width, size.height)




    onDrawBehind {


    drawRect(shaderBrush)


    }


    }) {


    JetLaggedHeader()


    JetLaggedSleepSummary()


    }


    }

    View Slide

  71. @Composable


    private fun JetLaggedScreen() {


    Column(modifier = Modifier


    .drawWithCache {


    val shader = RuntimeShader(WAVE_BAR_SHADER_AGSL)


    val shaderBrush = ShaderBrush(shader)




    shader.setFloatUniform("resolution", size.width, size.height)


    onDrawBehind {


    drawRect(shaderBrush)


    }


    }) {


    JetLaggedHeader()


    JetLaggedSleepSummary()




    View Slide

  72. @Composable


    private fun JetLaggedScreen() {


    val time by produceState(0f) {


    while(true) {


    withInfiniteAnimationFrameMillis {


    value = it / 1000f


    }


    }


    }


    Column(modifier = Modifier


    .drawWithCache {


    val shader = RuntimeShader(WAVE_BAR_SHADER_AGSL)


    val shaderBrush = ShaderBrush(shader)


    shader.setFloatUniform("resolution", size.width, size.height)




    onDrawBehind {


    drawRect(shaderBrush)


    }


    }) {


    JetLaggedHeader()


    JetLaggedSleepSummary()




    View Slide

  73. @Composable


    private fun JetLaggedScreen() {


    val time by produceState(0f) {


    while(true) {


    withInfiniteAnimationFrameMillis {


    value = it / 1000f


    }


    }


    }


    Column(modifier = Modifier


    .drawWithCache {


    val shader = RuntimeShader(WAVE_BAR_SHADER_AGSL)


    val shaderBrush = ShaderBrush(shader)


    shader.setFloatUniform("resolution", size.width, size.height)


    onDrawBehind {


    shader.setFloatUniform("time", time)


    drawRect(shaderBrush)


    }


    }) {


    JetLaggedHeader()



    View Slide

  74. View Slide

  75. Accessing Canvas
    Sometimes you might want to have access to it…

    View Slide

  76. @Composable


    private fun JetLaggedApp() {


    val paint = remember { Paint() }


    JetLaggedScreen()


    }


    View Slide

  77. @Composable


    private fun JetLaggedApp() {


    val paint = remember { Paint() }


    JetLaggedScreen(modifier = Modifier


    .drawWithContent {


    drawContent()


    })


    }


    View Slide

  78. @Composable


    private fun JetLaggedApp() {


    val paint = remember { Paint() }


    JetLaggedScreen(modifier = Modifier


    .drawWithContent {


    val matrix = ColorMatrix().apply {


    setToSaturation(0f)


    }


    paint.colorFilter = colorMatrix(matrix)


    drawIntoCanvas { canvas ->


    canvas.saveLayer(size.toRect(), paint)


    [email protected]()


    canvas.restore()


    }


    })


    }


    View Slide

  79. 🧛

    View Slide

  80. Modifier.drawWithContent
    Determine the drawing order yourself


    * Useful for BlendMode operations


    * Transformations to the content yourself (ie clipping to a complex path)


    * Or drawing on top of content

    View Slide

  81. Column(


    modifier = Modifier


    .fillMaxSize()


    .pointerInput("dragging") {


    detectDragGestures { change, dragAmount ->


    pointerOffset += dragAmount


    }


    }


    .onSizeChanged {


    pointerOffset = Offset(it.width / 2f, it.height /2f)


    }


    .drawWithContent {


    drawContent()


    drawRect(Brush.radialGradient(


    listOf(Color.Transparent, Color.Black),


    center = pointerOffset,


    radius = 100.dp.toPx(),


    ))


    }


    ) {


    // content goes here


    }

    View Slide

  82. Modifier.graphicsLayer{ }
    Modifier used to apply transformations to a Composable, such as:


    • scaleX, scaleY


    • rotationX, rotationY, rotationZ


    • alpha


    • translationX, translationY


    *Note: Will not change the layout size and placement of the composable

    View Slide

  83. Image(painterResource(id = R.drawable.sun_icon),


    contentDescription = "",


    modifier = Modifier


    .size(320.dp)


    )


    View Slide

  84. val infinite = rememberInfiniteTransition()


    val rotation by infinite.animateFloat(


    initialValue = 0f,


    targetValue = 360f,


    animationSpec = infiniteRepeatable(


    animation = tween(5000, easing = LinearEasing),


    )


    )


    Image(painterResource(id = R.drawable.sun_icon),


    contentDescription = "",


    modifier = Modifier


    .size(320.dp)


    )


    View Slide

  85. val infinite = rememberInfiniteTransition()


    val rotation by infinite.animateFloat(


    initialValue = 0f,


    targetValue = 360f,


    animationSpec = infiniteRepeatable(


    animation = tween(5000, easing = LinearEasing),


    )


    )


    Image(painterResource(id = R.drawable.sun_icon),


    contentDescription = "",


    modifier = Modifier


    .size(320.dp)


    .graphicsLayer {


    rotationZ = rotation


    }


    )


    View Slide

  86. View Slide

  87. graphicsLayer: RenderEffects
    Intermediate rendering step used to render drawing commands with a
    corresponding visual effect


    * BlurEffect - apply blur


    * RuntimeShader: Custom AGSL shader applied to whole composable

    View Slide

  88. @Language("AGSL")


    val WOBBLE_SHADER = """


    uniform float2 resolution;


    uniform float time;


    uniform shader contents;


    vec4 main(in vec2 fragCoord) {


    vec2 uv = fragCoord.xy / resolution.xy * 0.8 + 0.1;




    uv += sin(time * vec2(1.0, 2.0) + uv* 2.0) * 0.01;




    return contents.eval(uv * resolution.xy);


    }


    """.trimIndent()


    View Slide

  89. val shader = RuntimeShader(WOBBLE_SHADER)


    Column(Modifier


    .onSizeChanged { size ->


    shader.setFloatUniform(“resolution",


    size.width.toFloat(),


    size.height.toFloat()


    )


    }


    .graphicsLayer {


    renderEffect = android.graphics.RenderEffect


    .createRuntimeShaderEffect(


    shader,


    "contents"


    )


    .asComposeRenderEffect()


    }


    ) {


    // your content here


    }

    View Slide

  90. val shader = RuntimeShader(WOBBLE_SHADER)


    Column(Modifier


    .onSizeChanged { size ->


    shader.setFloatUniform(“resolution",


    size.width.toFloat(),


    size.height.toFloat()


    )


    }


    .graphicsLayer {


    renderEffect = android.graphics.RenderEffect


    .createRuntimeShaderEffect(


    shader,


    "contents"


    )


    .asComposeRenderEffect()


    }


    ) {


    // your content here


    }

    View Slide

  91. graphicsLayer:
    CompositingStrategy
    New in Compose 1.4+ (alpha right now)


    Refers to how the layer will be composited (put together) with existing content on
    screen.


    View Slide

  92. Offscreen
    Forces an offscreen
    buffer.
    Auto ModulateAlpha
    The default mode - no
    offscreen buffer.


    * Unless alpha or
    RenderEffects are set


    Each draw call has
    the alpha applied
    individually.

    View Slide

  93. Image(painter = dogImagePainter,
    modifier = Modifier
    .drawWithCache {
    onDrawWithContent {
    drawContent()
    drawCircle(
    Color.Black,
    blendMode = BlendMode.Clear
    )
    drawCircle(
    Color.Red
    )
    }
    }
    )

    View Slide

  94. Image(painter = dogImagePainter,
    modifier = Modifier
    .drawWithCache {
    onDrawWithContent {
    drawContent()
    drawCircle(
    Color.Black,
    blendMode = BlendMode.Clear
    )
    drawCircle(
    Color.Red
    )
    }
    }
    )

    View Slide

  95. CompositingStrategy.Auto
    Image(painter = dogImagePainter,
    modifier = Modifier
    .graphicsLayer {
    compositingStrategy = Auto
    }
    .drawWithCache {
    onDrawWithContent {
    drawContent()
    drawCircle(
    Color.Black,
    blendMode = BlendMode.Clear
    )
    drawCircle(
    Color.Red
    )
    }
    }
    )

    View Slide

  96. CompositingStrategy.Auto
    CompositingStrategy.Offscreen
    Image(painter = dogImagePainter,
    modifier = Modifier
    .graphicsLayer {
    compositingStrategy = Offscreen
    }
    .drawWithCache {
    onDrawWithContent {
    drawContent()
    drawCircle(
    Color.Black,
    blendMode = BlendMode.Clear
    )
    drawCircle(
    Color.Red
    )
    }
    }
    )

    View Slide

  97. Having (even more)
    fun with Paths
    goo.gle/compose-gradient-along-path

    View Slide

  98. private object HelloPath {
    val path = PathParser().parsePathString(
    "M13.63 248.31C13.63 248.31 51.84 206.67 84.21 169.31C140" +
    ".84 103.97 202.79 27.66 150.14 14.88C131.01 10.23 116.36 29.88 107.26
    45.33C69.7 108.92 58.03 214.33 57.54 302.57C67.75 271.83 104.43 190.85 140.18
    193.08C181.47 195.65 145.26 257.57 154.53 284.39C168.85 322.18 208.22 292.83 229.98
    277.45C265.92 252.03 288.98 231.22 288.98 200.45C288.98 161.55 235.29 174.02 223.3
    205.14C213.93 229.44 214.3 265.89 229.3 284.14C247.49 306.28 287.67 309.93 312.18
    288.46C337 266.71 354.66 234.56 368.68 213.03C403.92 158.87 464.36 86.15 449.06
    30.03C446.98 22.4 440.36 16.57 432.46 16.26C393.62 14.75 381.84 99.18 375.35
    129.31C368.78 159.83 345.17 261.31 373.11 293.06C404.43 328.58 446.29 262.4 464.66
    231.67C468.66 225.31 472.59 218.43 476.08 213.07C511.33 158.91 571.77 86.19 556.46
    30.07C554.39 22.44 547.77 16.61 539.87 16.3C501.03 14.79 489.25 99.22 482.76
    129.35C476.18 159.87 452.58 261.35 480.52 293.1C511.83 328.62 562.4 265.53 572.64
    232.86C587.34 185.92 620.94 171.58 660.91 180.29C616 166.66 580.86 199.67 572.64
    233.16C566.81 256.93 573.52 282.16 599.25 295.77C668.54 332.41 742.8 211.69 660.91
    180.29C643.67 181.89 636.15 204.77 643.29 227.78C654.29 263.97 704.29 268.27 733.08 256"
    )
    }

    View Slide

  99. val painter = rememberVectorPainter(
    viewportHeight = bounds.height,
    viewportWidth = bounds.width,
    defaultWidth = bounds.width.dp,
    defaultHeight = bounds.height.dp,
    autoMirror = true
    ) { _, _ ->
    Path(
    HelloPath.path.toNodes(),
    stroke = Brush.linearGradient(colors)
    )
    }
    Image(painter, contentDescription = "hello")

    View Slide

  100. val path = remember {
    HelloPath.path.toPath()
    }
    val bounds = path.getBounds()
    val totalLength = remember {
    val pathMeasure = PathMeasure()
    pathMeasure.setPath(path, false)
    pathMeasure.length
    }
    val lines = remember {
    path.asAndroidPath().flatten(0.5f)
    }

    View Slide

  101. val totalLength = remember {
    val pathMeasure = PathMeasure()
    pathMeasure.setPath(path, false)
    pathMeasure.length
    }
    val lines = remember {
    path.asAndroidPath().flatten(0.5f)
    }
    Box(modifier = Modifier.fillMaxSize()) {
    Canvas(modifier = Modifier
    .align(Alignment.Center),
    onDraw = {
    lines.forEach { line ->
    drawLine(
    brush = SolidColor(Color.Black),
    start = Offset(line.start.x, line.start.y),
    end = Offset(line.end.x, line.end.y))
    }
    })
    }

    View Slide

  102. val totalLength = remember {
    val pathMeasure = PathMeasure()
    pathMeasure.setPath(path, false)
    pathMeasure.length
    }
    val lines = remember {
    path.asAndroidPath().flatten(30f)
    }
    Box(modifier = Modifier.fillMaxSize()) {
    Canvas(modifier = Modifier
    .align(Alignment.Center),
    onDraw = {
    lines.forEach { line ->
    drawLine(
    brush = SolidColor(Color.Black),
    start = Offset(line.start.x, line.start.y),
    end = Offset(line.end.x, line.end.y))
    }
    })
    }

    View Slide

  103. val totalLength = remember {
    val pathMeasure = PathMeasure()
    pathMeasure.setPath(path, false)
    pathMeasure.length
    }
    val lines = remember {
    path.asAndroidPath().flatten(0.5f)
    }
    Box(modifier = Modifier.fillMaxSize()) {
    Canvas(modifier = Modifier
    .align(Alignment.Center),
    onDraw = {
    lines.forEach { line ->
    val startColor = interpolateColors(line.startFraction, colors)
    val endColor = interpolateColors(line.endFraction, colors)
    drawLine(
    brush = Brush.linearGradient(listOf(startColor, endColor)),
    start = Offset(line.start.x, line.start.y),
    end = Offset(line.end.x, line.end.y))
    }
    })
    }

    View Slide

  104. private fun interpolateColors(
    progress: Float,
    colorsInput: List,
    ): Color {
    val scaledProgress = progress * (colorsInput.size - 1)
    val oldColor = colorsInput[scaledProgress.toInt()]
    val newColor = colorsInput[(scaledProgress + 1f).toInt()]
    val newScaledProgressValue = scaledProgress - floor(progress)
    return lerp(
    start = oldColor,
    stop = newColor,
    fraction = newScaledProgressValue)
    }

    View Slide

  105. private fun interpolateColors(
    progress: Float,
    colorsInput: List,
    ): Color {
    val scaledProgress = progress * (colorsInput.size - 1)
    val oldColor = colorsInput[scaledProgress.toInt()]
    val newColor = colorsInput[(scaledProgress + 1f).toInt()]
    val newScaledProgressValue = scaledProgress - floor(progress)
    return lerp(
    start = oldColor,
    stop = newColor,
    fraction = newScaledProgressValue)
    }
    start stop
    fraction

    View Slide

  106. View Slide

  107. val lines = remember {
    path.asAndroidPath().flatten(30f)
    }
    val progress = remember {
    Animatable(0f)
    }
    LaunchedEffect(Unit) {
    progress.animateTo(
    1f,
    animationSpec = infiniteRepeatable(tween(3000))
    )
    }
    Box(modifier = Modifier.fillMaxSize()) {
    Canvas(modifier = Modifier
    .align(Alignment.Center),
    onDraw = {
    lines.forEach { line ->
    drawLine( //..)
    }
    })
    }

    View Slide

  108. val lines = remember {
    path.asAndroidPath().flatten(30f)
    }
    val progress = remember {
    Animatable(0f)
    }
    LaunchedEffect(Unit) {
    progress.animateTo(
    1f,
    animationSpec = infiniteRepeatable(tween(3000))
    )
    }
    Box(modifier = Modifier.fillMaxSize()) {
    Canvas(modifier = Modifier
    .align(Alignment.Center),
    onDraw = {
    val currentLength = totalLength * progress.value
    lines.forEach { line ->
    if (line.startFraction * totalLength < currentLength) {
    drawLine( //..)
    }
    }
    })

    View Slide

  109. goo.gle/compose-gradient-along-path

    View Slide

  110. goo.gle/compose-graphics-docs


    goo.gle/compose-jetlagged


    goo.gle/compose-jellyfish


    goo.gle/compose-gradient-path

    View Slide

  111. Thank you!
    Rebecca Franks (she/her)


    Developer Relations Engineer


    @riggaroo

    View Slide