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.
How can I determine whether a 2D Point is within a Polygon or Complex Path with Jetpack Compose?
Asked Answered
Same ol' Point in polygon problem! I suppose solving with Ray casting algorithm is rather hard because of how paths and polygons are implemented in Android in this case instead of your solution. –
Froghopper
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
)
}
}
}
Hi, I see you've opted for regular
Canvas
drawing instead of using vectorPainter
for the tappable map. How can I make it that the paths in the Canvas
scale to fit the size of the Canvas
? I've created this gist: gist.github.com/NielsMasdorp/d7096bd4c19b3166d05f77ed72b4c07c I want the Canvas
to be 216dp x 252dp
including 16dp
padding, but struggling to do so. using the Matrix
preScale
and postTranslate
doesn't seem the way to do it when scaling for different devices. Any thoughts? –
Sassaby @NielsMasdorp here i added how to scale and offset your path based on canvas size. github.com/SmartToolFactory/Jetpack-Compose-Tutorials/blob/… –
Tragus
You can create a path for scale that you put all of the paths in the list. After getting its size you can get coefficients regarding to canvas size. Let's say total path size is 100x120px while your canvas size 1000x1200 you get scaleX and scaleY using these values where i used 5 for demonstration. I used fillMaxWidth for my tutorial but it should work with any size, i tested with
216dp x 252dp
too. –
Tragus You are welcome. Thank you for your question, i was looking forward to playing with paths to create map or detect paths via touch for some time. This was a good opportunity for me as well. –
Tragus
© 2022 - 2024 — McMap. All rights reserved.