Touch interactions in Jetpack Compose
Tags: android, compose, touch, gestures
Published: 2022-07-12 09:20:00

After we learned how to draw different shapes on a Canvas in [the first part](https://dev.to/lex_fury/custom-composable-with-jetpack-compose-5d86) of the series, let’s have a look at how to handle touch interaction in this second part.

Just as a reminder, with this topic we will be able to make our Labeled Range Slider draggable.

![Interactive Labeled Range Slider](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kbsjact2u9yo1fmzrni0.gif)

To keep it simple we will create a Scribble Composable, to explore how we can handle touch interactions.

![Drawing Canvas](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lvxltr02qfkwjpfrsz48.png)

## Touch Interaction

For a regular interactive Composable we would normally use either the Modifier **clickable**:

```
Canvas(
   modifier = Modifier.clickable {
      Log.i("Tag", "Canvas clicked")
   }
) {
   // draw something
}
```

or the experimental **combinedClickable**:

```
Canvas(
   modifier = Modifier.combinedClickable(
      onClick = { Log.i("Tag", "Canvas clicked") },
      onLongClick = { Log.i("Tag", "Canvas long clicked") },
      onDoubleClick = { Log.i("Tag", "Canvas double clicked") }
   )
) {
   // draw something
}
```

Both work great if we just want to handle a click on the whole Composable.

But for our example, we want to have a little bit more control. We need to know the exact location where the click happens and also if the user drags the finger across the screen or not. Luckily Compose offers us the Modifier **pointerInput**, which provides the information we need.

## Gesture detectors

The easiest way to use pointerInput is to implement one of several already provided gesture detectors.

- **detectTapGestures**: This is most similar to the **combinedClickable** Modifier, handling tap, double tap, press and long press. The difference this Modifier provides us with the exact position the user taped within our Composable.

- **detectDragGestures**: With this detector we can detect the start, the end and the path a finger is dragged along in our Composable.

- **detectHorizontalDragGestures**: As the name suggests, this is similar to detectDragGestures, but only provides us with the update for horizontal drag movements.

- **detectVerticalDragGestures**: This is like **detectHorizontalDragGestures**, but only provides vertical drag movements.

- **detectDragGesturesAfterLongPress**: Using this detector we get the same information as **detectDragGestures**, but only after the user performed a long press.

- **detectTransformGestures**: The transformation gesture detector provides us with updated position information for rotating, panning and zooming gestures.

If these predefined detectors are still not enough and you want full control over how a touch is handled, you can use **awaitPointerEventScope**.

For our example of a Scribble Composable, we will focus on using **detectDragGestures**. Since this detector lets us know where the user is starting the touch and also constant updates while the finger is dragged across our Composable.

```
Canvas(
   modifier = modifier
      .pointerInput(key1 = Unit) {
         detectDragGestures(
            onDragStart = { touch ->
               Log.i("Tag", "Start of the interaction is $touch")
            },
            onDrag = { change, dragAmount ->
               Log.i("Tag", "Dragged $dragAmount; Result $change")
            }
         )
      }
) {
   // draw something
}
```

Here we registered a drag gesture detector within **pointerInput**. For the parameter **key1** we simply provide **Unit**. Since we don’t want our detection to be canceled when **key1** changes. In **pointerInput** we then call **detectDragGestures**. This gives us the information where the user starts the interaction in the callback **onDragStart**, as well as continuous updates of the path the finger takes in **onDrag**.

The **onDrag** callback has two parameters, the second one gives us the delta to the last drag we got. This is not the absolute position of where the finger is right now, but only the change to the last position update. The first parameter on the other hand provides us with more details, like historical data and also the absolute position of the finger on our Composable.

To keep it simple we will be using the first parameter **change** to get the absolute position for creating a line drawing.

```
var points by remember { mutableStateOf<List<Offset>>(emptyList()) }

...
         detectDragGestures(
            onDragStart = { touch ->
               points = listOf(touch)
            },
            onDrag = { change, _ ->
               val newPoint = change.position
               points = points + newPoint
            }
         )
...
```

First we create a mutable state **points** that can hold a list of Offsets, that will hold the path the finger was dragged along on our Canvas. When the drag gesture begins we create a new list and add the touch point to it. After that we simply can look at **change.position**, which gives us the absolute position of the touch on our Canvas and we can add this to our path.

This way works and we can get a decent result when drawing our path right now. But if we want to have a smoother result we should consider the historical data as well.

```
...
onDrag = { change, _ ->
   val pointsFromHistory = change.historical
      .map { it.position }
      .toTypedArray()
   val newPoints = listOf(*pointsFromHistory, change.position)
   points = points + newPoints
}
...
```

Here we iterate over the historical data, which represents points between two **onDrag** updates, and get its positions. With that, we simply add the historical positions and the current position to our path list.

All that is now left to do is to draw the path on our Canvas, using the **drawPath** method we saw in the first part of this series.

```
...
if (points.size > 1) {
   val path = Path().apply {
      val firstPoint = points.first()
      val rest = points.subList(1, points.size - 1)

      moveTo(firstPoint.x, firstPoint.y)
      rest.forEach {
         lineTo(it.x, it.y)
      }
   }

   drawPath(path, color, style = Stroke(width = lineWidth.toPx()))
}
...
```

When we have more than one point in our list we create a new **Path** object, move the start of the Path to our first point and then add all lines we have in our list to the Path. Since we want just lines to be draw without filling the area, we draw our path with the style **Stroke**.

In this implementation the tightness of the line to be drawn is parametrized as **lineWidth** of type **Dp**, but Stoke wants pixels as its width parameter. Luckily the **DrawScope** of our Canvas offers us with the extension function **toPx**, with which we can convert Dp to pixels.

## Conclusion
We are now able to handle touch interactions on our custom Composable and translate them to actions. With that, we can draw our Labeled Range Slider and also interact with it.

![Labeled Range Slider](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qfp5ytbh0x9gk0mjae8m.png)

But before we get into implementing it, we take a small excursion to the new Kotlin Context Receivers in [part 3](https://dev.to/lex_fury/context-receivers-39p5).

The whole code of the Scribble Composable can be found on [GitHub](https://github.com/a-frank/touch-interaction-composable).