Slide 1

Slide 1 text

Interact with @Composable Canvas Piotr Prus Mobile Tech Lead @Airly GDG 3City Organizer

Slide 2

Slide 2 text

• Android Canvas recap • Composable Canvas • Key di ff erences • Implementation • Interaction • Animation • State management • Test composable Agenda:

Slide 3

Slide 3 text

Canvas coordinate system

Slide 4

Slide 4 text

Canvas coordinate system X Y

Slide 5

Slide 5 text

Canvas coordinate system X Y distance distance

Slide 6

Slide 6 text

Canvas coordinate system X Y distance distance

Slide 7

Slide 7 text

Draw schema

Slide 8

Slide 8 text

Draw schema Start and end padding

Slide 9

Slide 9 text

Draw schema Top padding Bottom padding

Slide 10

Slide 10 text

Draw schema Draw area

Slide 11

Slide 11 text

Canvas draw functions • drawRect() • drawOval() • drawLine() • drawText() • drawBitmap() • drawPath()

Slide 12

Slide 12 text

Canvas draw functions • drawRect() • drawOval() • drawLine() • drawText() • drawBitmap() • drawPath()

Slide 13

Slide 13 text

Canvas draw functions • drawRect() • drawOval() • drawLine() • drawText() • drawBitmap() • drawPath()

Slide 14

Slide 14 text

Composable Canvas @Composable fun Canvas(modifier: Modifier, onDraw: DrawScope.() -> Unit) = Spacer(modifier.drawBehind(onDraw)) @Composable fun MyCanvas() { Box(modifier = Modifier .size(100.dp) .background(color = Color.White) .drawBehind { drawRect(color = Color.Blue, size = Size(width = 20f, height = 20f)) }) }

Slide 15

Slide 15 text

Composable Canvas @Composable fun MyCanvas() { Box(modifier = Modifier .size(100.dp) .background(color = Color.White) .drawBehind { drawRect(color = Color.Blue, size = Size(width = 20f, height = 20f)) }) } Result:

Slide 16

Slide 16 text

Composable Canvas Result: @Composable fun MyCanvas() { Box(modifier = Modifier .background(color = Color.White) .drawBehind { drawRect(color = Color.Blue, size = Size(width = 20f, height = 20f)) } ) }

Slide 17

Slide 17 text

Composable Canvas @Composable fun MyCanvas() { Box(modifier = Modifier .background(color = Color.White) .drawBehind { drawRect(color = Color.Blue, size = Size(width = 20f, height = 20f)) } ) } You MUST specify size with modi fi er, whether with exact sizes via Modi fi er.size modi fi er, or relative to parent, via Modi fi er. fi llMaxSize, ColumnScope.weight, etc. If parent wraps this child, only exact sizes must be speci fi ed.

Slide 18

Slide 18 text

Composable Canvas @Composable fun MyCanvas() { Box(modifier = Modifier .size(100.dp) .background(color = Color.White) .drawBehind { drawRect(color = Color.Blue, size = Size(width = 20f, height = 20f)) } ) { Box( modifier = Modifier .size(20.dp) .background(color = Color.Red) ) } } Result:

Slide 19

Slide 19 text

Composable Canvas @Composable fun MyCanvas() { Box(modifier = Modifier .size(100.dp) .background(color = Color.White) .drawBehind { drawRect(color = Color.Blue, size = Size(width = 20f, height = 20f)) } ) { Box( modifier = Modifier .size(20.dp) .background(color = Color.Red) .align(Alignment.BottomEnd) ) } } Result:

Slide 20

Slide 20 text

Android view Canvas vs Composable canvas val density = LocalDensity.current val horizontalPadding = with(density) { 20.dp.toPx() } fun dp2px(resource: Resources, dp: Int): Int { return TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), resource.getDisplayMetrics()) .toInt() } private fun dpToPixel(dp: Float): Float { val metrics: DisplayMetrics = this.getResources().getDisplayMetrics() return dp * (metrics.densityDpi / 160f) }

Slide 21

Slide 21 text

Android view Canvas vs Composable canvas class CustomView(context: Context?, attr: AttributeSet?) : View(context, attr) { private var width = 0 private var height = 0 fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { this.width = w this.height = h super.onSizeChanged(w, h, oldw, oldh) } fun onDraw(canvas: Canvas) { canvas.drawText("TEST", width / 2, height / 2, paint) } } @Composable fun MyCanvas() { Canvas(modifier = Modifier, onDraw = { val width = size.width val height = size.height }) }

Slide 22

Slide 22 text

drawText()

Slide 23

Slide 23 text

drawText() Missing in @Composable Canvas

Slide 24

Slide 24 text

@Composable Canvas has no function drawText 😞 android.graphics.Canvas has function drawText 🙂 Canvas() { this.drawContext.canvas.nativeCanvas.drawText( “Text on canvas”, drawPadding + index.times(distance), size.height, textPaint ) } } val textPaint = Paint().apply { color = MaterialTheme.colors.onPrimary.toArgb() textAlign = Paint.Align.CENTER this.textSize = textSize typeface = Typeface.DEFAULT_BOLD }

Slide 25

Slide 25 text

Feature request https://issuetracker.google.com/issues/190787898

Slide 26

Slide 26 text

Implementation

Slide 27

Slide 27 text

@Composable fun BarChartCanvas(list: List, barSelected: (Int) -> Unit) { Row(Modifier .fillMaxWidth() .padding(12.dp) .height(150.dp) .horizontalScroll(rememberScrollState()) ) { val density = LocalDensity.current val horizontalPadding = with(density) { 12.dp.toPx() } val distance = with(density) { 26.dp.toPx() } val calculatedWidth = with(density) { (distance.times(list.size) + horizontalPadding.times(2)).toDp() } } }

Slide 28

Slide 28 text

@Composable fun BarChartCanvas(list: List, barSelected: (Int) -> Unit) { Row(Modifier .fillMaxWidth() .padding(12.dp) .height(150.dp) .horizontalScroll(rememberScrollState()) ) { val density = LocalDensity.current val horizontalPadding = with(density) { 12.dp.toPx() } val distance = with(density) { 26.dp.toPx() } val calculatedWidth = with(density) { (distance.times(list.size) + horizontalPadding.times(2)).toDp() } } }

Slide 29

Slide 29 text

@Composable fun BarChartCanvas(list: List, barSelected: (Int) -> Unit) { Row(Modifier .fillMaxWidth() .padding(12.dp) .height(150.dp) .horizontalScroll(rememberScrollState()) ) { val density = LocalDensity.current val horizontalPadding = with(density) { 12.dp.toPx() } val distance = with(density) { 26.dp.toPx() } val calculatedWidth = with(density) { (distance.times(list.size) + horizontalPadding.times(2)).toDp() } } }

Slide 30

Slide 30 text

@Composable fun BarChartCanvas(list: List, barSelected: (Int) -> Unit) { Row(Modifier .fillMaxWidth() .padding(12.dp) .height(150.dp) .horizontalScroll(rememberScrollState()) ) { val density = LocalDensity.current val horizontalPadding = with(density) { 12.dp.toPx() } val distance = with(density) { 26.dp.toPx() } val calculatedWidth = with(density) { (distance.times(list.size) + horizontalPadding.times(2)).toDp() } } }

Slide 31

Slide 31 text

@Composable fun BarChartCanvas(list: List, barSelected: (Int) -> Unit) { Row(Modifier .fillMaxWidth() .padding(12.dp) .height(150.dp) .horizontalScroll(rememberScrollState()) ) { val density = LocalDensity.current val horizontalPadding = with(density) { 12.dp.toPx() } val distance = with(density) { 26.dp.toPx() } val calculatedWidth = with(density) { (distance.times(list.size) + horizontalPadding.times(2)).toDp() } } }

Slide 32

Slide 32 text

@Composable fun BarChartCanvas(list: List, barSelected: (Int) -> Unit) { Row(Modifier) { . . . Canvas( modifier = Modifier .fillMaxHeight() .width(calculatedWidth)){ // Draw functions go here } }

Slide 33

Slide 33 text

// Draw scope val lineDistance = size.height.minus(smallPadding.times(2)).div(4) repeat(5) { drawLine( color = Color.Gray, start = Offset(0f, smallPadding.plus(it.times(lineDistance))), end = Offset(size.width, smallPadding.plus(it.times(lineDistance))) ) }

Slide 34

Slide 34 text

// Draw scope val lineDistance = size.height.minus(smallPadding.times(2)).div(4) repeat(5) { drawLine( color = Color.Gray, start = Offset(0f, smallPadding.plus(it.times(lineDistance))), end = Offset(size.width, smallPadding.plus(it.times(lineDistance))) ) }

Slide 35

Slide 35 text

// Draw scope val lineDistance = size.height.minus(smallPadding.times(2)).div(4) repeat(5) { drawLine( color = Color.Gray, start = Offset(0f, smallPadding.plus(it.times(lineDistance))), end = Offset(size.width, smallPadding.plus(it.times(lineDistance))) ) }

Slide 36

Slide 36 text

// Draw scope val lineDistance = size.height.minus(smallPadding.times(2)).div(4) repeat(5) { drawLine( color = Color.Gray, start = Offset(0f, smallPadding.plus(it.times(lineDistance))), end = Offset(size.width, smallPadding.plus(it.times(lineDistance))) ) }

Slide 37

Slide 37 text

// Draw scope val lineDistance = size.height.minus(smallPadding.times(2)).div(4) repeat(5) { drawLine( color = Color.Gray, start = Offset(0f, smallPadding.plus(it.times(lineDistance))), end = Offset(size.width, smallPadding.plus(it.times(lineDistance))) ) }

Slide 38

Slide 38 text

barAreas.forEachIndexed { index, item -> val barHeight = item.value.times(scale).toFloat() drawRoundRect( color = skyBlue400, topLeft = Offset( x = horizontalPadding + distance.times(index) - barWidth.div(2), y = size.height - barHeight - smallPadding ), size = Size(barWidth, barHeight), cornerRadius = CornerRadius(cornerRadius) ) }

Slide 39

Slide 39 text

barAreas.forEachIndexed { index, item -> val barHeight = item.value.times(scale).toFloat() drawRoundRect( color = skyBlue400, topLeft = Offset( x = horizontalPadding + distance.times(index) - barWidth.div(2), y = size.height - barHeight - smallPadding ), size = Size(barWidth, barHeight), cornerRadius = CornerRadius(cornerRadius) ) } data class BarArea( val index: Int, val xStart: Float, val xEnd: Float, val value: Int )

Slide 40

Slide 40 text

barAreas.forEachIndexed { index, item -> val barHeight = item.value.times(scale).toFloat() drawRoundRect( color = skyBlue400, topLeft = Offset( x = horizontalPadding + distance.times(index) - barWidth.div(2), y = size.height - barHeight - smallPadding ), size = Size(barWidth, barHeight), cornerRadius = CornerRadius(cornerRadius) ) } data class BarArea( val index: Int, val xStart: Float, val xEnd: Float, val value: Int ) val barAreas = list.mapIndexed { index, i -> BarArea( index = index, value = i, xStart = horizontalPadding + distance.times(index) - distance.div(2), xEnd = horizontalPadding + distance.times(index) + distance.div(2) ) }

Slide 41

Slide 41 text

barAreas.forEachIndexed { index, item -> val barHeight = item.value.times(scale).toFloat() drawRoundRect( color = skyBlue400, topLeft = Offset( x = horizontalPadding + distance.times(index) - barWidth.div(2), y = size.height - barHeight - smallPadding ), size = Size(barWidth, barHeight), cornerRadius = CornerRadius(cornerRadius) ) }

Slide 42

Slide 42 text

barAreas.forEachIndexed { index, item -> val barHeight = item.value.times(scale).toFloat() drawRoundRect( color = skyBlue400, topLeft = Offset( x = horizontalPadding + distance.times(index) - barWidth.div(2), y = size.height - barHeight - smallPadding ), size = Size(barWidth, barHeight), cornerRadius = CornerRadius(cornerRadius) ) }

Slide 43

Slide 43 text

barAreas.forEachIndexed { index, item -> val barHeight = item.value.times(scale).toFloat() drawRoundRect( color = skyBlue400, topLeft = Offset( x = horizontalPadding + distance.times(index) - barWidth.div(2), y = size.height - barHeight - smallPadding ), size = Size(barWidth, barHeight), cornerRadius = CornerRadius(cornerRadius) ) }

Slide 44

Slide 44 text

barAreas.forEachIndexed { index, item -> val barHeight = item.value.times(scale).toFloat() drawRoundRect( color = skyBlue400, topLeft = Offset( x = horizontalPadding + distance.times(index) - barWidth.div(2), y = size.height - barHeight - smallPadding ), size = Size(barWidth, barHeight), cornerRadius = CornerRadius(cornerRadius) ) }

Slide 45

Slide 45 text

barAreas.forEachIndexed { index, item -> val barHeight = item.value.times(scale).toFloat() drawRoundRect( color = skyBlue400, topLeft = Offset( x = horizontalPadding + distance.times(index) - barWidth.div(2), y = size.height - barHeight - smallPadding ), size = Size(barWidth, barHeight), cornerRadius = CornerRadius(cornerRadius) ) }

Slide 46

Slide 46 text

barAreas.forEachIndexed { index, item -> val barHeight = item.value.times(scale).toFloat() drawRoundRect( color = skyBlue400, topLeft = Offset( x = horizontalPadding + distance.times(index) - barWidth.div(2), y = size.height - barHeight - smallPadding ), size = Size(barWidth, barHeight), cornerRadius = CornerRadius(cornerRadius) ) }

Slide 47

Slide 47 text

barAreas.forEachIndexed { index, item -> val barHeight = item.value.times(scale).toFloat() drawRoundRect( color = skyBlue400, topLeft = Offset( x = horizontalPadding + distance.times(index) - barWidth.div(2), y = size.height - barHeight - smallPadding ), size = Size(barWidth, barHeight), cornerRadius = CornerRadius(cornerRadius) ) }

Slide 48

Slide 48 text

barAreas.forEachIndexed { index, item -> . . . this.drawIntoCanvas { canvas -> val textPositionY = chartAreaBottom - barHeight - smallPadding canvas.nativeCanvas.drawText( "${item.value}", horizontalPadding + distance.times(index), textPositionY, paint ) } }

Slide 49

Slide 49 text

barAreas.forEachIndexed { index, item -> . . . this.drawIntoCanvas { canvas -> val textPositionY = chartAreaBottom - barHeight - smallPadding canvas.nativeCanvas.drawText( "${item.value}", horizontalPadding + distance.times(index), textPositionY, paint ) } }

Slide 50

Slide 50 text

barAreas.forEachIndexed { index, item -> . . . this.drawIntoCanvas { canvas -> val textPositionY = chartAreaBottom - barHeight - smallPadding canvas.nativeCanvas.drawText( "${item.value}", horizontalPadding + distance.times(index), textPositionY, paint ) } }

Slide 51

Slide 51 text

barAreas.forEachIndexed { index, item -> . . . this.drawIntoCanvas { canvas -> val textPositionY = chartAreaBottom - barHeight - smallPadding canvas.nativeCanvas.drawText( "${item.value}", horizontalPadding + distance.times(index), textPositionY, paint ) } } val textSize = with(density) { 10.sp.toPx() } val paint = Paint().apply { color = 0xffff47586B.toInt() textAlign = Paint.Align.CENTER this.textSize = textSize }

Slide 52

Slide 52 text

No content

Slide 53

Slide 53 text

Interaction MotionEvent / PointerInput

Slide 54

Slide 54 text

fun Modifier.pointerInput( key1: Any?, block: suspend PointerInputScope.() -> Unit ): Modifier = composed() { val density = LocalDensity.current val viewConfiguration = LocalViewConfiguration.current remember(density) { SuspendingPointerInputFilter(viewConfiguration, density) }.apply { LaunchedEffect(this, key1) { block() } } } Pointer input

Slide 55

Slide 55 text

@Composable fun Modifier.startGesture( onStart: (offsetX: Float) -> Unit ): Modifier { val interactionSource = remember { MutableInteractionSource() } return this.pointerInput(interactionSource) { forEachGesture { coroutineScope { awaitPointerEventScope { val touch = awaitFirstDown().also { it.consumeDownChange() } onStart(touch.position.x) } } } } } Pointer input

Slide 56

Slide 56 text

@Composable fun Modifier.startGesture( onStart: (offsetX: Float) -> Unit ): Modifier { val interactionSource = remember { MutableInteractionSource() } return this.pointerInput(interactionSource) { forEachGesture { coroutineScope { awaitPointerEventScope { val touch = awaitFirstDown().also { it.consumeDownChange() } onStart(touch.position.x) } } } } } Pointer input

Slide 57

Slide 57 text

@Composable fun Modifier.startGesture( onStart: (offsetX: Float) -> Unit ): Modifier { val interactionSource = remember { MutableInteractionSource() } return this.pointerInput(interactionSource) { forEachGesture { coroutineScope { awaitPointerEventScope { val touch = awaitFirstDown().also { it.consumeDownChange() } onStart(touch.position.x) } } } } } Pointer input

Slide 58

Slide 58 text

@Composable fun Modifier.startGesture( onStart: (offsetX: Float) -> Unit ): Modifier { val interactionSource = remember { MutableInteractionSource() } return this.pointerInput(interactionSource) { forEachGesture { coroutineScope { awaitPointerEventScope { val touch = awaitFirstDown().also { it.consumeDownChange() } onStart(touch.position.x) } } } } } Pointer input

Slide 59

Slide 59 text

@Composable fun Modifier.startGesture( onStart: (offsetX: Float) -> Unit ): Modifier { val interactionSource = remember { MutableInteractionSource() } return this.pointerInput(interactionSource) { forEachGesture { coroutineScope { awaitPointerEventScope { val touch = awaitFirstDown().also { it.consumeDownChange() } onStart(touch.position.x) } } } } } Pointer input

Slide 60

Slide 60 text

@Composable fun Modifier.startGesture( onStart: (offsetX: Float) -> Unit ): Modifier { val interactionSource = remember { MutableInteractionSource() } return this.pointerInput(interactionSource) { forEachGesture { coroutineScope { awaitPointerEventScope { val touch = awaitFirstDown().also { it.consumeDownChange() } onStart(touch.position.x) } } } } } Pointer input

Slide 61

Slide 61 text

@Composable fun Modifier.startGesture( onStart: (offsetX: Float) -> Unit ): Modifier { val interactionSource = remember { MutableInteractionSource() } return this.pointerInput(interactionSource) { forEachGesture { coroutineScope { awaitPointerEventScope { val touch = awaitFirstDown().also { it.consumeDownChange() } onStart(touch.position.x) } } } } } Pointer input

Slide 62

Slide 62 text

@Composable fun Modifier.tapOrPress( onStart: (offsetX: Float) -> Unit, onCancel: (offsetX: Float) -> Unit, onCompleted: (offsetX: Float) -> Unit ): Modifier { val interactionSource = remember { MutableInteractionSource() } return this.pointerInput(interactionSource) { forEachGesture { coroutineScope { awaitPointerEventScope { val tap = awaitFirstDown().also { it.consumeDownChange() } onStart(tap.position.x) val up = waitForUpOrCancellation() if (up == null) { onCancel(tap.position.x) } else { up.consumeDownChange() onCompleted(tap.position.x) } } } } } } Pointer input

Slide 63

Slide 63 text

@Composable fun Modifier.tapOrPress( onStart: (offsetX: Float) -> Unit, onCancel: (offsetX: Float) -> Unit, onCompleted: (offsetX: Float) -> Unit ): Modifier { val interactionSource = remember { MutableInteractionSource() } return this.pointerInput(interactionSource) { forEachGesture { coroutineScope { awaitPointerEventScope { val tap = awaitFirstDown().also { it.consumeDownChange() } onStart(tap.position.x) val up = waitForUpOrCancellation() if (up == null) { onCancel(tap.position.x) } else { up.consumeDownChange() onCompleted(tap.position.x) } } } } } } Pointer input

Slide 64

Slide 64 text

@Composable fun Modifier.tapOrPress( onStart: (offsetX: Float) -> Unit, onCancel: (offsetX: Float) -> Unit, onCompleted: (offsetX: Float) -> Unit ): Modifier { val interactionSource = remember { MutableInteractionSource() } return this.pointerInput(interactionSource) { forEachGesture { coroutineScope { awaitPointerEventScope { val tap = awaitFirstDown().also { it.consumeDownChange() } onStart(tap.position.x) val up = waitForUpOrCancellation() if (up == null) { onCancel(tap.position.x) } else { up.consumeDownChange() onCompleted(tap.position.x) } } } } } } Pointer input

Slide 65

Slide 65 text

@Composable fun Modifier.tapOrPress( onStart: (offsetX: Float) -> Unit, onCancel: (offsetX: Float) -> Unit, onCompleted: (offsetX: Float) -> Unit ): Modifier { val interactionSource = remember { MutableInteractionSource() } return this.pointerInput(interactionSource) { forEachGesture { coroutineScope { awaitPointerEventScope { val tap = awaitFirstDown().also { it.consumeDownChange() } onStart(tap.position.x) val up = waitForUpOrCancellation() if (up == null) { onCancel(tap.position.x) } else { up.consumeDownChange() onCompleted(tap.position.x) } } } } } } Pointer input

Slide 66

Slide 66 text

@Composable fun Modifier.tapOrPress( onStart: (offsetX: Float) -> Unit, onCancel: (offsetX: Float) -> Unit, onCompleted: (offsetX: Float) -> Unit ): Modifier { val interactionSource = remember { MutableInteractionSource() } return this.pointerInput(interactionSource) { forEachGesture { coroutineScope { awaitPointerEventScope { val tap = awaitFirstDown().also { it.consumeDownChange() } onStart(tap.position.x) val up = waitForUpOrCancellation() if (up == null) { onCancel(tap.position.x) } else { up.consumeDownChange() onCompleted(tap.position.x) } } } } } } Pointer input

Slide 67

Slide 67 text

Simple selection var selectedPosition by remember { mutableStateOf(0) } val selectedBar by remember(selectedPosition, barAreas) { derivedStateOf { barAreas.find { it.xStart < selectedPosition && selectedPosition < it.xEnd } } } Modifier.tapOrPress( onStart = { }, onCancel = { }, onCompleted = { selectedPosition = it } )

Slide 68

Slide 68 text

Stateful Simple selection var selectedPosition by remember { mutableStateOf(0) } val selectedBar by remember(selectedPosition, barAreas) { derivedStateOf { barAreas.find { it.xStart < selectedPosition && selectedPosition < it.xEnd } } } Modifier.tapOrPress( onStart = { }, onCancel = { }, onCompleted = { selectedPosition = it } )

Slide 69

Slide 69 text

Simple selection // in Draw scope if (selectedBar != null) { drawRoundRect( brush = Brush.verticalGradient( listOf( skyBlue400.copy(alpha = 0.3f), Color.Transparent ) ), topLeft = Offset( x = horizontalPaddingPx + distancePx.times(bar.index) - areaWidthPx.div(2), y = areaHeightPx.plus(topPaddingPx) - areaHeightPx ), size = Size(areaWidthPx, areaHeightPx), cornerRadius = CornerRadius(cornerRadiusPx) ) }

Slide 70

Slide 70 text

Simple selection // in Draw scope if (selectedBar != null) { drawRoundRect( brush = Brush.verticalGradient( listOf( skyBlue400.copy(alpha = 0.3f), Color.Transparent ) ), topLeft = Offset( x = horizontalPaddingPx + distancePx.times(bar.index) - areaWidthPx.div(2), y = areaHeightPx.plus(topPaddingPx) - areaHeightPx ), size = Size(areaWidthPx, areaHeightPx), cornerRadius = CornerRadius(cornerRadiusPx) ) }

Slide 71

Slide 71 text

Simple selection // in Draw scope if (selectedBar != null) { drawRoundRect( brush = Brush.verticalGradient( listOf( skyBlue400.copy(alpha = 0.3f), Color.Transparent ) ), topLeft = Offset( x = horizontalPaddingPx + distancePx.times(bar.index) - areaWidthPx.div(2), y = areaHeightPx.plus(topPaddingPx) - areaHeightPx ), size = Size(areaWidthPx, areaHeightPx), cornerRadius = CornerRadius(cornerRadiusPx) ) }

Slide 72

Slide 72 text

Simple selection // in Draw scope if (selectedBar != null) { drawRoundRect( brush = Brush.verticalGradient( listOf( skyBlue400.copy(alpha = 0.3f), Color.Transparent ) ), topLeft = Offset( x = horizontalPaddingPx + distancePx.times(bar.index) - areaWidthPx.div(2), y = areaHeightPx.plus(topPaddingPx) - areaHeightPx ), size = Size(areaWidthPx, areaHeightPx), cornerRadius = CornerRadius(cornerRadiusPx) ) }

Slide 73

Slide 73 text

Simple selection // in Draw scope if (selectedBar != null) { drawRoundRect( brush = Brush.verticalGradient( listOf( skyBlue400.copy(alpha = 0.3f), Color.Transparent ) ), topLeft = Offset( x = horizontalPaddingPx + distancePx.times(bar.index) - areaWidthPx.div(2), y = areaHeightPx.plus(topPaddingPx) - areaHeightPx ), size = Size(areaWidthPx, areaHeightPx), cornerRadius = CornerRadius(cornerRadiusPx) ) }

Slide 74

Slide 74 text

Simple selection // in Draw scope if (selectedBar != null) { drawRoundRect( brush = Brush.verticalGradient( listOf( skyBlue400.copy(alpha = 0.3f), Color.Transparent ) ), topLeft = Offset( x = horizontalPaddingPx + distancePx.times(bar.index) - areaWidthPx.div(2), y = areaHeightPx.plus(topPaddingPx) - areaHeightPx ), size = Size(areaWidthPx, areaHeightPx), cornerRadius = CornerRadius(cornerRadiusPx) ) }

Slide 75

Slide 75 text

Animation

Slide 76

Slide 76 text

Jetpack Compose animation fl owchart

Slide 77

Slide 77 text

Jetpack Compose animation fl owchart

Slide 78

Slide 78 text

Jetpack Compose animation fl owchart

Slide 79

Slide 79 text

Jetpack Compose animation fl owchart

Slide 80

Slide 80 text

Jetpack Compose animation fl owchart

Slide 81

Slide 81 text

Jetpack Compose animation fl owchart

Slide 82

Slide 82 text

Animatable val animatable = remember { Animatable(0f) } ... //Modifier.tapOrPress onCompleted = { scope.launch { selectedPosition = it animatable.animateTo(1f) } } ... // draw scope drawRoundRect( brush = brush, topLeft = Offset( x = horizontalPadding + distance.times(selectedBar!!.index) - selectionWidth.div(2), y = chartAreaHeight - chartAreaHeight.times(animatable.value)), // use of animatable value size = Size(selectionWidth, chartAreaHeight), cornerRadius = CornerRadius(cornerRadius) )

Slide 83

Slide 83 text

Animatable val animatable = remember { Animatable(0f) } ... //Modifier.tapOrPress onCompleted = { scope.launch { selectedPosition = it animatable.animateTo(1f) } } ... // draw scope drawRoundRect( brush = brush, topLeft = Offset( x = horizontalPadding + distance.times(selectedBar!!.index) - selectionWidth.div(2), y = chartAreaHeight - chartAreaHeight.times(animatable.value)), // use of animatable value size = Size(selectionWidth, chartAreaHeight), cornerRadius = CornerRadius(cornerRadius) )

Slide 84

Slide 84 text

Manage two selection

Slide 85

Slide 85 text

gestureStarted() tempSelection = xPosition tempAnimatable.animateTo(1) var selectedPosition by remember { mutableStateOf(0f) } var tempPosition by remember { mutableStateOf(-Int.MAX_VALUE.toFloat()) } val animatable = remember { Animatable(1f) } val tempAnimatable = remember { Animatable(0f) }

Slide 86

Slide 86 text

gestureStarted() tempSelection = xPosition tempAnimatable.animateTo(1) var selectedPosition by remember { mutableStateOf(0f) } var tempPosition by remember { mutableStateOf(-Int.MAX_VALUE.toFloat()) } val animatable = remember { Animatable(1f) } val tempAnimatable = remember { Animatable(0f) } gestureCanceled() tempSelection = -1 tempAnimatable.animateTo(0)

Slide 87

Slide 87 text

gestureStarted() tempSelection = xPosition tempAnimatable.animateTo(1) var selectedPosition by remember { mutableStateOf(0f) } var tempPosition by remember { mutableStateOf(-Int.MAX_VALUE.toFloat()) } val animatable = remember { Animatable(1f) } val tempAnimatable = remember { Animatable(0f) } gestureCanceled() tempSelection = -1 tempAnimatable.animateTo(0) gestureCompleted() animatable = tempAnimatable + animateTo(1) tempAnimatable = 0 selection = tempSelection tempSelection = -1

Slide 88

Slide 88 text

Final result:

Slide 89

Slide 89 text

Testing

Slide 90

Slide 90 text

Testing @Composable fun BarChartCanvas(list: List, barSelected: (Int) -> Unit) Row(Modifier .fillMaxWidth() .padding(12.dp) .height(150.dp) .testTag("BarChart") .horizontalScroll(rememberScrollState()) )

Slide 91

Slide 91 text

Testing @Composable fun BarChartCanvas(list: List, barSelected: (Int) -> Unit) Row(Modifier .fillMaxWidth() .padding(12.dp) .height(150.dp) .testTag("BarChart") .horizontalScroll(rememberScrollState()) )

Slide 92

Slide 92 text

Testing @get:Rule val composeTestRule = createComposeRule() @Test fun checkCanvasExistence() { val list = mutableStateOf(listOf(1, 2, 3, 4, 5, 6, 7, 8)) composeTestRule.setContent { BarChartCanvas(list = list.value, barSelected = { }) } composeTestRule.onNodeWithTag(testTag = "BarChart").assertExists() }

Slide 93

Slide 93 text

Testing @Test fun clickOnThirdElementOfList() { val list = mutableStateOf(listOf(1, 2, 3, 4, 5, 6, 7, 8)) val selectedItem = mutableStateOf(list.value.first()) val distance = with(composeTestRule.density) { (12.dp + 20.dp.times(3)).toPx() } composeTestRule.setContent { BarChartCanvas(list = list.value, barSelected = { selectedItem.value = it }) } composeTestRule.onNodeWithTag(testTag = "BarChart") .performGesture { click(position = Offset(distance, 1f)) } assertEquals(3, selectedItem.value) }

Slide 94

Slide 94 text

Testing @Test fun clickOnThirdElementOfList() { val list = mutableStateOf(listOf(1, 2, 3, 4, 5, 6, 7, 8)) val selectedItem = mutableStateOf(list.value.first()) val distance = with(composeTestRule.density) { (12.dp + 20.dp.times(3)).toPx() } composeTestRule.setContent { BarChartCanvas(list = list.value, barSelected = { selectedItem.value = it }) } composeTestRule.onNodeWithTag(testTag = "BarChart") .performGesture { click(position = Offset(distance, 1f)) } assertEquals(3, selectedItem.value) }

Slide 95

Slide 95 text

Testing @Test fun clickOnThirdElementOfList() { val list = mutableStateOf(listOf(1, 2, 3, 4, 5, 6, 7, 8)) val selectedItem = mutableStateOf(list.value.first()) val distance = with(composeTestRule.density) { (12.dp + 20.dp.times(3)).toPx() } composeTestRule.setContent { BarChartCanvas(list = list.value, barSelected = { selectedItem.value = it }) } composeTestRule.onNodeWithTag(testTag = "BarChart") .performGesture { click(position = Offset(distance, 1f)) } assertEquals(3, selectedItem.value) }

Slide 96

Slide 96 text

Testing @Test fun clickOnThirdElementOfList() { val list = mutableStateOf(listOf(1, 2, 3, 4, 5, 6, 7, 8)) val selectedItem = mutableStateOf(list.value.first()) val distance = with(composeTestRule.density) { (12.dp + 20.dp.times(3)).toPx() } composeTestRule.setContent { BarChartCanvas(list = list.value, barSelected = { selectedItem.value = it }) } composeTestRule.onNodeWithTag(testTag = "BarChart") .performGesture { click(position = Offset(distance, 1f)) } assertEquals(3, selectedItem.value) }

Slide 97

Slide 97 text

Testing @Test fun clickOnThirdElementOfList() { val list = mutableStateOf(listOf(1, 2, 3, 4, 5, 6, 7, 8)) val selectedItem = mutableStateOf(list.value.first()) val distance = with(composeTestRule.density) { (12.dp + 20.dp.times(3)).toPx() } composeTestRule.setContent { BarChartCanvas(list = list.value, barSelected = { selectedItem.value = it }) } composeTestRule.onNodeWithTag(testTag = "BarChart") .performGesture { click(position = Offset(distance, 1f)) } assertEquals(3, selectedItem.value) }

Slide 98

Slide 98 text

Testing @Test fun clickOnThirdElementOfList() { val list = mutableStateOf(listOf(1, 2, 3, 4, 5, 6, 7, 8)) val selectedItem = mutableStateOf(list.value.first()) val distance = with(composeTestRule.density) { (12.dp + 20.dp.times(3)).toPx() } composeTestRule.setContent { BarChartCanvas(list = list.value, barSelected = { selectedItem.value = it }) } composeTestRule.onNodeWithTag(testTag = "BarChart") .performGesture { click(position = Offset(distance, 1f)) } assertEquals(3, selectedItem.value) }

Slide 99

Slide 99 text

Testing @Test fun cancelSelectionOfThirdElement() { val list = mutableStateOf(listOf(1, 2, 3, 4, 5, 6, 7, 8)) val selectedItem = mutableStateOf(list.value.first()) val distance = with(composeTestRule.density) { (12.dp + 20.dp.times(3)).toPx() } composeTestRule.setContent { BarChartCanvas(list = list.value, barSelected = { selectedItem.value = it }) } composeTestRule.onNodeWithTag(testTag = "BarChart") .performGesture { down(Offset(distance, 1f)) } .performGesture { moveBy(Offset(distance, 0f)) } .performGesture { up() } assertEquals(1, selectedItem.value) }

Slide 100

Slide 100 text

Testing @Test fun cancelSelectionOfThirdElement() { val list = mutableStateOf(listOf(1, 2, 3, 4, 5, 6, 7, 8)) val selectedItem = mutableStateOf(list.value.first()) val distance = with(composeTestRule.density) { (12.dp + 20.dp.times(3)).toPx() } composeTestRule.setContent { BarChartCanvas(list = list.value, barSelected = { selectedItem.value = it }) } composeTestRule.onNodeWithTag(testTag = "BarChart") .performGesture { down(Offset(distance, 1f)) } .performGesture { moveBy(Offset(distance, 0f)) } .performGesture { up() } assertEquals(1, selectedItem.value) }

Slide 101

Slide 101 text

Testing @Test fun cancelSelectionOfThirdElement() { val list = mutableStateOf(listOf(1, 2, 3, 4, 5, 6, 7, 8)) val selectedItem = mutableStateOf(list.value.first()) val distance = with(composeTestRule.density) { (12.dp + 20.dp.times(3)).toPx() } composeTestRule.setContent { BarChartCanvas(list = list.value, barSelected = { selectedItem.value = it }) } composeTestRule.onNodeWithTag(testTag = "BarChart") .performGesture { down(Offset(distance, 1f)) } .performGesture { moveBy(Offset(distance, 0f)) } .performGesture { up() } assertEquals(1, selectedItem.value) }

Slide 102

Slide 102 text

Testing @Test fun cancelSelectionOfThirdElement() { val list = mutableStateOf(listOf(1, 2, 3, 4, 5, 6, 7, 8)) val selectedItem = mutableStateOf(list.value.first()) val distance = with(composeTestRule.density) { (12.dp + 20.dp.times(3)).toPx() } composeTestRule.setContent { BarChartCanvas(list = list.value, barSelected = { selectedItem.value = it }) } composeTestRule.onNodeWithTag(testTag = "BarChart") .performGesture { down(Offset(distance, 1f)) } .performGesture { moveBy(Offset(distance, 0f)) } .performGesture { up() } assertEquals(1, selectedItem.value) }

Slide 103

Slide 103 text

Testing @Test fun cancelSelectionOfThirdElement() { val list = mutableStateOf(listOf(1, 2, 3, 4, 5, 6, 7, 8)) val selectedItem = mutableStateOf(list.value.first()) val distance = with(composeTestRule.density) { (12.dp + 20.dp.times(3)).toPx() } composeTestRule.setContent { BarChartCanvas(list = list.value, barSelected = { selectedItem.value = it }) } composeTestRule.onNodeWithTag(testTag = "BarChart") .performGesture { down(Offset(distance, 1f)) } .performGesture { moveBy(Offset(distance, 0f)) } .performGesture { up() } assertEquals(1, selectedItem.value) }

Slide 104

Slide 104 text

Testing @Test fun cancelSelectionOfThirdElement() { val list = mutableStateOf(listOf(1, 2, 3, 4, 5, 6, 7, 8)) val selectedItem = mutableStateOf(list.value.first()) val distance = with(composeTestRule.density) { (12.dp + 20.dp.times(3)).toPx() } composeTestRule.setContent { BarChartCanvas(list = list.value, barSelected = { selectedItem.value = it }) } composeTestRule.onNodeWithTag(testTag = "BarChart") .performGesture { down(Offset(distance, 1f)) } .performGesture { moveBy(Offset(distance, 0f)) } .performGesture { up() } assertEquals(1, selectedItem.value) }

Slide 105

Slide 105 text

Testing @Test fun cancelSelectionOfThirdElement() { val list = mutableStateOf(listOf(1, 2, 3, 4, 5, 6, 7, 8)) val selectedItem = mutableStateOf(list.value.first()) val distance = with(composeTestRule.density) { (12.dp + 20.dp.times(3)).toPx() } composeTestRule.setContent { BarChartCanvas(list = list.value, barSelected = { selectedItem.value = it }) } composeTestRule.onNodeWithTag(testTag = "BarChart") .performGesture { down(Offset(distance, 1f)) } .performGesture { moveBy(Offset(distance, 0f)) } .performGesture { up() } assertEquals(1, selectedItem.value) }

Slide 106

Slide 106 text

Testing @Test fun cancelSelectionOfThirdElement() { val list = mutableStateOf(listOf(1, 2, 3, 4, 5, 6, 7, 8)) val selectedItem = mutableStateOf(list.value.first()) val distance = with(composeTestRule.density) { (12.dp + 20.dp.times(3)).toPx() } composeTestRule.setContent { BarChartCanvas(list = list.value, barSelected = { selectedItem.value = it }) } composeTestRule.onNodeWithTag(testTag = "BarChart") .performGesture { down(Offset(distance, 1f)) } .performGesture { moveBy(Offset(distance, 0f)) } .performGesture { up() } assertEquals(1, selectedItem.value) }

Slide 107

Slide 107 text

Final thoughts • Drawing using Composable Canvas is very similar to Android View Canvas • drawText is missing in Compose version • Testing is easy and very readable • Animation has good documentation

Slide 108

Slide 108 text

Thanks for listening Piotr Prus Mobile Tech Lead @Airly GDG 3City Organizer

Slide 109

Slide 109 text

Questions? Piotr Prus Mobile Tech Lead @Airly GDG 3City Organizer