Hi I am working on a project where I am trying to place markers on images, sort of similar to google maps.
For each marker I will save coordinates and details in the database and whenever the user clicks on the marker, it shows the relevant data, right now I’m only using static sample data though. I’m new to android studio but have managed to put something together, but I’m having a couple of problems.
My main problem is getting the correct offset relative to the image and screen (due to different screen sizes and auto resizing of the images). The current offset on click is showing high numbers up to around 1200, which makes the placed markers appear off screen. The current emulator device only goes up to around 200F
width. So I’m not sure how to handle this dynamically for all devices.
ImageScreen.kt –
@Composable
fun ImageScreen(
navController: NavController
) {
//val configuration = LocalConfiguration.current
//val screenHeight = configuration.screenHeightDp.dp
//val screenWidth = configuration.screenWidthDp.dp
val context = LocalContext.current
var xyCoordinates by remember { mutableStateOf(Offset.Zero) }
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val scope = rememberCoroutineScope()
var showBottomSheet by remember { mutableStateOf(false) }
val imgAnnotations = remember {
mutableStateListOf<ImgAnnotation>()
.apply {
add(
ImgAnnotation(
uid = "45224",
coordinateX = 10f,
coordinateY = 10f,
note = "Sample text 1"
)
)
add(
ImgAnnotation(
uid = "6454",
coordinateX = 50f,
coordinateY = 50f,
note = "Sample text 2"
)
)
add(
ImgAnnotation(
uid = "211111",
coordinateX = 200f,
coordinateY = 90f,
note = "Sample text 3"
)
)
add(
ImgAnnotation(
uid = "21555",
coordinateX = 32f,
coordinateY = 93f,
note = "Sample text 4"
)
)
}
}
var currentAnnotationSelected = ImgAnnotation()
var showAnnotation by remember { mutableStateOf(false) }
Column(
modifier = Modifier
.fillMaxSize(),
verticalArrangement = Arrangement.Bottom,
horizontalAlignment = Alignment.CenterHorizontally
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) {
Image(
painter = painterResource(R.drawable.hair_picture),
contentDescription = "Record image",
contentScale = ContentScale.Fit,
modifier = Modifier
.align(Alignment.BottomCenter)
.pointerInput(Unit) {
detectTapGestures(
onPress = { offset ->
xyCoordinates = offset
showBottomSheet = true
}
)
}
)
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) {
imgAnnotations.forEach { item ->
MakeShape(
modifier = Modifier
//.offset(item.coordinateX.dp, item.coordinateY.dp
item.coordinateX!!.toFloat().dp,
item.coordinateY!!.toFloat().dp)
.clickable {
currentAnnotationSelected = item
showAnnotation = true
showBottomSheet = true
},
shape = CircleShape,
size = 20.dp,
bg = Color.Yellow
)
}
}
if (showBottomSheet) {
ModalBottomSheet(
onDismissRequest = {
showBottomSheet = false
showAnnotation = false
currentAnnotationSelected = ImgAnnotation()
},
sheetState = sheetState,
windowInsets = WindowInsets(0, 0, 0, 0)
) {
IconButton(
onClick = {
scope.launch { sheetState.hide() }.invokeOnCompletion {
if (!sheetState.isVisible) {
showBottomSheet = false
showAnnotation = false
currentAnnotationSelected = ImgAnnotation()
}
}
},
modifier = Modifier
.align(Alignment.End)
) {
Icon(
painterResource(R.drawable.close_icon),
contentDescription = "Close icon",
modifier = Modifier.height(18.dp)
)
}
AnnotationNote(
xy = xyCoordinates,
annotationData = currentAnnotationSelected,
show = showAnnotation
)
Spacer(modifier = Modifier.height(16.dp))
}
}
}
}
}
Annotation note composable –
@Composable
fun AnnotationNote(
xy: Offset = Offset.Zero,
show: Boolean = false,
annotationData: ImgAnnotation = ImgAnnotation()
) {
var annotationNote by remember {
mutableStateOf(annotationData.note ?: "")
}
Column(
modifier = Modifier
.fillMaxWidth(),
verticalArrangement = Arrangement.SpaceBetween,
horizontalAlignment = Alignment.CenterHorizontally
) {
if (show) {
Text(annotationNote)
} else {
//Text("$xy")
TextField(
modifier = Modifier.fillMaxWidth(),
value = annotationNote,
onValueChange = {
annotationNote = it
},
label = {
Text(text = "Annotation Note")
}
)
Spacer(modifier = Modifier.height(24.dp))
ActionButton(
onClick = { // to do - need to save coordinates and note to db },
contentColor = Color.Black,
disabledContentColor = Color.Black,
text = stringResource(R.string.save_btn),
)
Spacer(modifier = Modifier.height(24.dp))
}
}
}
Edit
I found another question that may be able to help with my problem, and I’ve tried implementing it but the example shows how to use predefined static values. For my use case I need to get the calculated coordinates’ values depending where a user clicks on the image, so I’m still stuck on how to use it to get (percentage as suggested in question) offest on the click modifier of an image?
2
Answers
If the ContentScale you picked is
ContentScale.FillBounds
this is not that hard to solve. You just need to scale position from you db or the on Bitmap to dimensions of Image composable which you can get from onSizeChanged.You can see this answer that you can detect pixels on Image on every click
How to detect what image part was clicked in Android Compose
Let me first explain the logic how it should be done. Let’s say you have a 1000x1000px bitmap and you want to display it inside an Image that covers 2000x2000px on screen. And let’s say a market is placed on (500,500) on bitmap to correctly draw this on screen it should be at (1000, 1000) which is also center of Composable.
You can calculate x coordinate on screen as
And let’s you touched (400, 400) on Image Composable on screen it’s calculated as
Result is for a bitmap with 2000x1000px image displayed with 3/4 aspect ratio on screen
Demo
You can draw shape with
Modifier.drawWithContent
,Shape
can be converted toOutline
or change this position to display other Composables usingModifier.offset{IntOffset}
However, this works when your Bitmap fits perfectly to
Image
Composable that is always possible whenContentScale.FillBounds
is set. In a case where you see empty spaces or in case of Crop sometimes only some section of the painter is drawn, in Image composable you also need to calculate top, left, end, and bottom spaces or get a Rectangle which is done by Image internally but is not a public parameter. You can useImageWithConstraints
where rectangle is calculated based on current ContentScale with this libraryor copy source code where rectangle is calculated.
After getting rectangle you just need to add start and top of Rectangle into calculations to get correct positions with any ContantScale.
The exact way to map between image and composable coordinates depends on the way
Image
scales its content. But either way it comes down to:Image
composable and offsets if neededImage
composable and image coordinates on marker creation and on marker placementIn this example
imageWidth
andimageHeight
are dimensions of the image loaded inPainter
.windowSize
stores the size of theImage
composable.Marker data class:
An example for
ContentScale.Fit
:A example for
ContentScale.FillBounds
:Marker composable:
Usage:
Markers can be placed by clicking on either of the images. A marker can be clicked to display its info on top.
vertical_background
drawable is a 400 x 1000 bitmap.