Detecting whether a user tapped on an arbitrary shape represented by a
Path is an unfortunately difficult
task on Android. The Path class does not offer any API to do this, and the workaround I have seen
folks use (representing the tap as a small rectangle/circle and computing the intersection with
the test shape) is inefficient and sometimes fails.
Thankfully, Jetpack Compose a simple way to perform this task. The video below shows how the Compose
APIs can be used to detect taps, or generally perform any test requiring to check whether a specific
2D coordinate is contained inside a Path:
The video shows that the tap detection used to change the color of the two images is pixel-perfect.
To achieve this, you need to use the
PathHitTester
API. A hit tester takes a Path as its main parameter and performs internal processing to speed up
subsequent queries:
1val hitTester = PathHitTester(imagePath)
You can then perform the test by calling contains():
1val tap = down.position - imagePosition
2imageColorFilter = if (tap in hitTester) { // calls contains()
3 ColorFilter.lighting(Color.Yellow, Color.Black)
4} else {
5 null
6}
Note that the “down” pointer event is first translated back into the image’s coordinates system by undoing the translation applied to render the image. You must always make sure that both the path used by the hit tester and the input coordinates are in the same coordinates sytem by applying any inverse transform you may have applied to render the path or whatever it represents.
And that’s all there is to it! PathHitTester is provided as an API separate from Path (as
opposed to for instance Path.contains()) for two reasons, both related to performance:
- To avoid allocations.
- To speed up hit tests by using a spatial data structure that allows to test the miniaml number
of segments when testing against a complex
Path.
This means that PathHitTest can be used to perform high-frequency queries, for instance during
a drag event.
The hit tester can also be reused efficiently by calling the update(Path) method to change the
Path used for testing. This is sometimes necessary if you modify the test path. Considering
for instance the following case:
Any hit test performed after modifying the path will return results against the original path, not the modifications.
Last but not least, the demo shown in the video above was built using my own library called pathway. Most of what this library has to offer was added to Compose already, but it provides the ability to generate a path from an image:
1val contour = bitmap.toPath().asComposePath()
The precision of the contour can be controlled by specifying the tolerance parameter of toPath().
This contouring operation works by looking at transparent pixels, so make sure your image have a
transparent background if you plan on using this feature.