This is a share your knowledge, Q&A-style to explain how to detect whether a polygon or a complex shapes such as some section of path is touched as in gif below. Also it contains how to animate path scale, color using linear interpolation and using Matrix with Jetpack Compose Paths thanks to this quesiton.
Easiest way to do to is creating a very small rectangle in touch position with
val touchPath = Path().apply {
addRect(
Rect(
center = it,
radius = .5f
)
)
}
Then checking
val differencePath =
Path.combine(
operation = PathOperation.Difference,
touchPath,
path
)
with path operation if difference path of in position and small rectangle path is empty.
For map implementation first create a class that contains Path
for drawing, Animatable for animating selected or deselected Path
s.
@Stable
internal class AnimatedMapData(
val path: Path,
selected: Boolean = false,
val animatable: Animatable<Float, AnimationVector1D> = Animatable(1f)
) {
var isSelected by mutableStateOf(selected)
}
Inside tap gesture get rectangle and set selected and deselected datas.
@Preview
@Composable
private fun AnimatedMapSectionPathTouchSample() {
val animatedMapDataList = remember {
Netherlands.PathMap.entries.map {
val path = Path()
path.apply {
it.value.forEach {
addPath(it)
}
val matrix = Matrix().apply {
preScale(5f, 5f)
postTranslate(-140f, 0f)
}
this.asAndroidPath().transform(matrix)
}
AnimatedMapData(path = path)
}
}
// This is for animating paths on selection or deselection animations
animatedMapDataList.forEach {
LaunchedEffect(key1 = it.isSelected) {
val targetValue = if (it.isSelected) 1.2f else 1f
it.animatable.animateTo(targetValue, animationSpec = tween(1000))
}
}
Column {
Box(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f)
.background(Blue400)
) {
Canvas(
modifier = Modifier
.pointerInput(Unit) {
detectTapGestures {
val touchPath = Path().apply {
addRect(
Rect(
center = it,
radius = .5f
)
)
}
animatedMapDataList.forEachIndexed { index, data ->
val path = data.path
val differencePath =
Path.combine(
operation = PathOperation.Difference,
touchPath,
path
)
val isInBounds = differencePath.isEmpty
if (isInBounds) {
data.isSelected = data.isSelected.not()
} else {
data.isSelected = false
}
}
}
}
.fillMaxWidth()
.aspectRatio(1f)
.clipToBounds()
) {
animatedMapDataList.forEach { data ->
val path = data.path
if (data.isSelected.not()) {
withTransform(
{
val scale = data.animatable.value
scale(
scaleX = scale,
scaleY = scale,
// Set scale position as center of path
pivot = data.path.getBounds().center
)
}
) {
drawPath(path, Color.Black)
drawPath(path, color = Color.White, style = Stroke(1.dp.toPx()))
}
}
}
// Draw selected path above other paths
animatedMapDataList.firstOrNull { it.isSelected }?.let { data ->
val path = data.path
withTransform(
{
val scale = data.animatable.value
scale(
scaleX = scale,
scaleY = scale,
// Set scale position as center of path
pivot = data.path.getBounds().center
)
}
) {
drawPath(
path = path,
color = lerp(
start = Color.Black,
stop = Orange400,
// animate color via linear interpolation
fraction = (data.animatable.value - 1f) / 0.2f
)
)
drawPath(path, color = Color.White, style = Stroke(1.dp.toPx()))
}
}
}
}
}
}
Map that contains some section of Netherlands and other samples available link below
For touching and dragging non-uniform shapes you need set a drag gesture and holding touched index and setting Matrix of selected path with
modifier = Modifier
.background(Blue400)
.fillMaxWidth()
.aspectRatio(1f)
.pointerInput(Unit) {
detectDragGestures(
onDragStart = { offset: Offset ->
val touchPath = Path().apply {
addRect(
Rect(
center = offset,
radius = .5f
)
)
}
pathDataList.forEachIndexed { index, data ->
val path = data.path
val differencePath =
Path.combine(
operation = PathOperation.Difference,
touchPath,
path
)
val isInBounds = differencePath.isEmpty
if (isInBounds) {
touchIndex = index
}
}
},
onDrag = { change: PointerInputChange, dragAmount: Offset ->
val pathData = pathDataList.getOrNull(touchIndex)
pathData?.let {
val matrix = Matrix().apply {
postTranslate(dragAmount.x, dragAmount.y)
}
pathData.path.asAndroidPath().transform(matrix)
pathDataList[touchIndex] = it.copy(
center = dragAmount
)
}
},
onDragCancel = {
touchIndex = -1
},
onDragEnd = {
touchIndex = -1
}
)
}
Data class is
@Immutable
data class PathData(
val path: Path,
val center: Offset
)
Full sample
@Preview
@Composable
private fun PathTouchSample() {
var touchIndex by remember {
mutableIntStateOf(-1)
}
val pathDataList = remember {
mutableStateListOf<PathData>().apply {
repeat(5) {
val cx = 170f * (it + 1)
val cy = 170f * (it + 1)
val radius = 120f
val sides = 3 + it
val path = createPolygonPath(cx, cy, sides, radius)
add(
PathData(
path = path,
center = Offset(0f, 0f)
)
)
}
}
}
Canvas(
modifier = Modifier
.background(Blue400)
.fillMaxWidth()
.aspectRatio(1f)
.pointerInput(Unit) {
detectDragGestures(
onDragStart = { offset: Offset ->
val touchPath = Path().apply {
addRect(
Rect(
center = offset,
radius = .5f
)
)
}
pathDataList.forEachIndexed { index, data ->
val path = data.path
val differencePath =
Path.combine(
operation = PathOperation.Difference,
touchPath,
path
)
val isInBounds = differencePath.isEmpty
if (isInBounds) {
touchIndex = index
}
}
},
onDrag = { change: PointerInputChange, dragAmount: Offset ->
val pathData = pathDataList.getOrNull(touchIndex)
pathData?.let {
val matrix = Matrix().apply {
postTranslate(dragAmount.x, dragAmount.y)
}
pathData.path.asAndroidPath().transform(matrix)
pathDataList[touchIndex] = it.copy(
center = dragAmount
)
}
},
onDragCancel = {
touchIndex = -1
},
onDragEnd = {
touchIndex = -1
}
)
}
) {
pathDataList.forEachIndexed { index: Int, pathData: PathData ->
val path = pathData.path
if (touchIndex != index) {
drawPath(
path,
color = Color.Black
)
}
}
pathDataList.getOrNull(touchIndex)?.let { pathData ->
val path = pathData.path
drawPath(
path = path,
color = Color.Green
)
}
}
}