InfoBubble
Una InfoBubble es un componente que muestra contenido personalizado en un formato de callout adjunto a marcadores en un mapa. Proporciona un método para mostrar información detallada sobre marcadores.
Descripción General
Section titled “Descripción General”Una InfoBubble se muestra encima de un marcador específico. El contenido mostrado en la InfoBubble puede ser cualquier Composable, permitiendo diseños flexibles y personalizados.
Función Composable
Section titled “Función Composable”@Composablefun InfoBubble( marker: MarkerState, bubbleColor: Color = Color.White, borderColor: Color = Color.Black, contentPadding: Dp = 8.dp, cornerRadius: Dp = 4.dp, tailSize: Dp = 8.dp, content: @Composable () -> Unit)Parámetros
Section titled “Parámetros”marker: ElMarkerStateal que se adjunta la burbujabubbleColor: Color de fondo de la burbuja (predeterminado: White)borderColor: Color del borde de la burbuja (predeterminado: Black)contentPadding: Espaciado interno alrededor del contenido (predeterminado: 8dp)cornerRadius: Radio de las esquinas redondeadas de la burbuja (predeterminado: 4dp)tailSize: Tamaño de la cola que apunta al marcador (predeterminado: 8dp)content: Contenido Composable para mostrar dentro de la burbuja
Uso Básico
Section titled “Uso Básico”Burbuja de Texto Simple
Section titled “Burbuja de Texto Simple”@Composablefun SimpleInfoBubbleExample() { val center = GeoPoint.fromLatLong(52.5163, 13.3777)
val camera = MapCameraPosition(position = center, zoom = 13) val mapViewState = rememberHereMapViewState(cameraPosition = camera)
var selectedMarker by remember { mutableStateOf<MarkerState?>(null) } val onMarkerClick: OnMarkerEventHandler = { markerState -> selectedMarker = markerState }
HereMapView( modifier = modifier, state = mapViewState, onMapClick = { selectedMarker = null } ) { val markerState = MarkerState( position = center, icon = DefaultIcon(fillColor = Color.Green, label = "POI"), extra = "Brandenburg Gate", id = "poi-marker", onClick = onMarkerClick )
Marker(markerState)
selectedMarker?.let { marker -> InfoBubble( marker = marker, contentPadding = 12.dp, cornerRadius = 8.dp, tailSize = 10.dp ) { Text( text = marker.extra as? String ?: "", color = Color.Black, style = MaterialTheme.typography.bodyLarge ) } } }}
Burbuja con Estilo Personalizado
Section titled “Burbuja con Estilo Personalizado”data class LocationInfo( val name: String, val description: String, val rating: Float, val imageUrl: String? = null) : java.io.Serializable
@Composablefun RichContentBubbleExample() { var selectedMarker by remember { mutableStateOf<MarkerState?>(null) }
val center = GeoPoint.fromLatLong(37.7694, -122.4862) val camera = MapCameraPosition(position = center, zoom = 13) val mapViewState = rememberHereMapViewState(cameraPosition = camera) val onMarkerClick: OnMarkerEventHandler = { markerState -> selectedMarker = markerState }
HereMapView( modifier = modifier, state = mapViewState, onMapClick = { selectedMarker = null } ) { val locationInfo = LocationInfo( name = "Golden Gate Park", description = "A large urban park with gardens, museums, and recreational areas.", rating = 4.5f )
val markerState = MarkerState( position = GeoPoint.fromLatLong(37.7694, -122.4862), icon = DefaultIcon(fillColor = Color.Green, label = "🌳"), extra = locationInfo, id = "park-marker", onClick = onMarkerClick )
Marker(markerState)
selectedMarker?.let { marker -> val info = marker.extra as? LocationInfo info?.let { InfoBubble( marker = marker, bubbleColor = MaterialTheme.colorScheme.background, borderColor = Color.Gray, contentPadding = 16.dp, cornerRadius = 12.dp ) { Column(modifier = Modifier.width(200.dp)) { Text( text = info.name, style = MaterialTheme.typography.headlineSmall, color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold ) Spacer(modifier = Modifier.height(8.dp)) Text( text = info.description, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.primary ) Spacer(modifier = Modifier.height(8.dp)) Row(verticalAlignment = Alignment.CenterVertically) { repeat(5) { index -> Icon( Icons.Default.Star, contentDescription = null, tint = if (index < info.rating.toInt()) Color.Yellow else Color.Gray, modifier = Modifier.size(16.dp) ) } Text( text = " ${info.rating}/5", color = MaterialTheme.colorScheme.primary, style = MaterialTheme.typography.bodySmall, modifier = Modifier.padding(start = 4.dp) ) } } } } } }}
Burbuja Interactiva con Acciones
Section titled “Burbuja Interactiva con Acciones”data class StoreInfo( val name: String, val address: String, val phone: String, val type: String) : java.io.Serializable
@Composablefun InteractiveBubbleExample(modifier: Modifier = Modifier) { val center = GeoPoint.fromLatLong(35.70662, 139.76378) val camera = MapCameraPosition(position = center, zoom = 16) val mapViewState = rememberArcGISMapViewState(cameraPosition = camera)
var selectedMarker by remember { mutableStateOf<MarkerState?>(null) } val context = LocalContext.current val onMarkerClick: OnMarkerEventHandler = { markerState -> selectedMarker = markerState }
ArcGISMapView( state = mapViewState, modifier = modifier, onMapClick = { selectedMarker = null } ) { val storeInfo = StoreInfo( name = "Coffee Shop", address = "1-4, Hongo 4-Chome, Bunkyo-Ku", phone = "+81 (555) 123-4567", type = "coffee" )
val markerState = MarkerState( position = GeoPoint.fromLatLong(35.708106, 139.760823), icon = DefaultIcon(fillColor = Color(0xFF8B4513), label = "☕"), extra = storeInfo, id = "coffee-shop-marker", onClick = onMarkerClick )
Marker(markerState)
selectedMarker?.let { marker -> val store = marker.extra as? StoreInfo store?.let { InfoBubble( marker = marker, bubbleColor = Color.White, borderColor = Color.Black, contentPadding = 12.dp, cornerRadius = 8.dp ) { Column(modifier = Modifier.width(250.dp)) { Text( text = store.name, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold ) Text( text = store.address, style = MaterialTheme.typography.bodySmall, color = Color.Gray, modifier = Modifier.padding(vertical = 4.dp) ) Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly ) { Button( onClick = { val intent = Intent(Intent.ACTION_DIAL).apply { data = Uri.parse("tel:${store.phone}") } context.startActivity(intent) }, colors = ButtonDefaults.buttonColors(containerColor = Color.Blue) ) { Icon(Icons.Default.Phone, contentDescription = "Call") Text("Call", modifier = Modifier.padding(start = 4.dp)) } } } } } } }}Gestión de Múltiples Burbujas
Section titled “Gestión de Múltiples Burbujas”@Composablefun MultipleBubblesExample() { val mapViewState = rememberGoogleMapViewState() var selectedMarkers by remember { mutableStateOf(setOf<String>()) } val onMarkerClick: OnMarkerEventHandler = { markerState -> selectedMarkers = if (selectedMarkers.contains(markerState.id)) { selectedMarkers - markerState.id // Deselect } else { selectedMarkers + markerState.id // Select } }
val markerData = remember { listOf( Triple( GeoPoint.fromLatLong(37.7749, -122.4194), "Restaurant A", Color.Red ), Triple( GeoPoint.fromLatLong(37.7849, -122.4094), "Hotel B", Color.Blue ), Triple( GeoPoint.fromLatLong(37.7649, -122.4294), "Shop C", Color.Green ) ) }
val markerStates = remember { markerData.mapIndexed { index, (position, name, color) -> MarkerState( id = "marker_$index", position = position, icon = DefaultIcon(fillColor = color, label = "${index + 1}"), extra = name, onClick = onMarkerClick ) } }
// Reemplace MapView con el proveedor de mapas que elija, como GoogleMapView o MapboxMapView MapView( state = mapViewState, onMapClick = { selectedMarkers = emptySet() // Clear all selections } ) { markerStates.forEach { markerState -> Marker(markerState)
// Show bubble if marker is selected if (selectedMarkers.contains(markerState.id)) { InfoBubble( marker = markerState, bubbleColor = Color.White, borderColor = Color.Black ) { Column { Text( text = markerState.extra as String, style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Bold ) Text( text = "Toque para cerrar", style = MaterialTheme.typography.bodySmall, color = Color.Gray ) } } } } }}Diseños Personalizados
Section titled “Diseños Personalizados”Para implementar un diseño personalizado, puede crear su propio componente basado en InfoBubble. El siguiente código y video muestran un ejemplo de una burbuja con una cola en el lado derecho.
@Composablefun RightInfoBubbleMapExample(modifier: Modifier = Modifier) { val center = GeoPoint.fromLatLong(37.7749, -122.4194) val mapViewState = rememberMapLibreMapViewState( cameraPosition = MapCameraPosition( position = center, zoom = 13, ), ) var selectedMarker by remember { mutableStateOf<MarkerState?>(null) } val onMarkerClick: OnMarkerEventHandler = { markerState -> selectedMarker = markerState } val onMarkerDragStart: OnMarkerEventHandler = { markerState -> println("Comenzó a arrastrar el marcador: ${markerState.id}") } val onMarkerDrag: OnMarkerEventHandler = { markerState -> println("Arrastrando el marcador a: ${markerState.position}") } val onMarkerDragEnd: OnMarkerEventHandler = { markerState -> println("Finalizó el arrastre del marcador: ${markerState.id}") }
val markerState1 by remember { mutableStateOf( MarkerState( id = "marker1", position = GeoPoint.fromLatLong(37.7749, -122.4194), icon = DefaultIcon( fillColor = Color.Blue, infoAnchor = Offset(0.5f, 0.25f), label = "1", ), draggable = true, onClick = onMarkerClick, onDragStart = onMarkerDragStart, onDrag = onMarkerDrag, onDragEnd = onMarkerDragEnd, ), ) } val markerState2 by remember { mutableStateOf( MarkerState( id = "marker2", position = GeoPoint.fromLatLong(37.7849, -122.4094), icon = DefaultIcon( fillColor = Color.Red, infoAnchor = Offset(0.5f, 0.25f), label = "2", ), onClick = onMarkerClick, ), ) }
MapLibreMapView( state = mapViewState, onMapLoaded = { println("El mapa se cargó y está listo") }, onMapClick = { geoPoint -> selectedMarker = null // Borrar la selección al pulsar el mapa } ) { // Contenido de mapa con marcadores interactivos Marker(markerState1) Marker(markerState2)
// Mostrar información del marcador seleccionado selectedMarker?.let { marker -> val text = GeoPoint.from(marker.position).toUrlValue(6) // Burbuja personalizada con cola en el lado derecho InfoBubbleCustom( marker = marker, tailOffset = Offset(0f, 0.5f), // attach at center-left of the bubble ) { RightTailInfoBubble( bubbleColor = Color.White, borderColor = Color.Black, ) { Text( text = text, color = MaterialTheme.colorScheme.primary, ) } } } }}
@Composableprivate fun RightTailInfoBubble( bubbleColor: Color, borderColor: Color, contentPadding: androidx.compose.ui.unit.Dp = 8.dp, cornerRadius: androidx.compose.ui.unit.Dp = 4.dp, tailSize: androidx.compose.ui.unit.Dp = 8.dp, content: @Composable () -> Unit,) { Box(modifier = Modifier.wrapContentSize()) { Canvas(modifier = Modifier.matchParentSize()) { val width = size.width val height = size.height val tail = tailSize.toPx() val corner = cornerRadius.toPx()
val path = Path().apply { // start at top-left (after tail area) moveTo(tail + 2 * corner, 0f) lineTo(width - 2 * corner, 0f) // top-right corner arcTo( rect = Rect( topLeft = Offset(width - 2 * corner, 0f), bottomRight = Offset(width, 2 * corner), ), startAngleDegrees = -90f, sweepAngleDegrees = 90f, forceMoveTo = false, ) // right edge lineTo(width, height - 2 * corner) // bottom-right corner arcTo( rect = Rect( topLeft = Offset(width - 2 * corner, height - 2 * corner), bottomRight = Offset(width, height), ), startAngleDegrees = 0f, sweepAngleDegrees = 90f, forceMoveTo = false, ) // bottom edge towards left (before tail) lineTo(tail + 2 * corner, height) // bottom-left corner (before tail) arcTo( rect = Rect( topLeft = Offset(tail, height - 2 * corner), bottomRight = Offset(tail + 2 * corner, height), ), startAngleDegrees = 90f, sweepAngleDegrees = 90f, forceMoveTo = false, ) // left edge up to tail bottom lineTo(tail, height / 2 + tail / 2) // Tail on left side lineTo(0f, height / 2) lineTo(tail, height / 2 - tail / 2) // left edge up to top-left corner lineTo(tail, 2 * corner) // top-left corner arcTo( rect = Rect( topLeft = Offset(tail, 0f), bottomRight = Offset(tail + 2 * corner, 2 * corner), ), startAngleDegrees = 180f, sweepAngleDegrees = 90f, forceMoveTo = false, ) close() }
drawPath(path, color = bubbleColor, style = Fill) drawPath(path, color = borderColor, style = Stroke(width = 2f)) }
Box( modifier = Modifier .padding( start = contentPadding + tailSize, top = contentPadding, bottom = contentPadding, end = contentPadding, ).wrapContentSize() .clip(RoundedCornerShape(cornerRadius)), ) { content() } }}Posicionamiento y Comportamiento
Section titled “Posicionamiento y Comportamiento”Posicionamiento Automático
Section titled “Posicionamiento Automático”La InfoBubble se posiciona automáticamente encima de su marcador asociado:
- La cola de la burbuja apunta al centro del marcador
- La burbuja ajusta su posición para permanecer visible en el viewport del mapa
- La burbuja sigue al marcador mientras se realiza pan o zoom en el mapa
Posicionamiento Personalizado
Section titled “Posicionamiento Personalizado”Aunque InfoBubble maneja el posicionamiento automáticamente, puede influir en él a través del ancla del icono del marcador:
// Marker with bottom-center anchor - bubble will appear aboveval markerWithBottomAnchor = MarkerState( position = position, icon = ImageDefaultIcon( drawable = customIcon, anchor = Offset(0.5f, 1f) // Bottom center ))
// Marker with center anchor - bubble will appear above centerval markerWithCenterAnchor = MarkerState( position = position, icon = ImageDefaultIcon( drawable = customIcon, anchor = Offset(0.5f, 0.5f) // Center ))Gestión del Ciclo de Vida
Section titled “Gestión del Ciclo de Vida”La InfoBubble gestiona automáticamente su ciclo de vida:
// Bubble appears when component is composedselectedMarker?.let { marker -> InfoBubble(marker = marker) { // Content }}
// Bubble disappears when component is removed from composition// This automatically happens when selectedMarker becomes nullControl Manual del Ciclo de Vida
Section titled “Control Manual del Ciclo de Vida”@Composablefun ManualLifecycleExample() { var showBubble by remember { mutableStateOf(false) } val markerState = remember { /* marker state */ }
LaunchedEffect(showBubble) { if (showBubble) { delay(3000) // Show for 3 seconds showBubble = false } }
// Replace MapView with your chosen map provider, such as GoogleMapView, MapboxMapView MapView(state = mapViewState) { Marker(markerState)
if (showBubble) { InfoBubble(marker = markerState) { Text("Auto-hiding bubble") } } }}Estilo y Temas
Section titled “Estilo y Temas”Soporte de Modo Oscuro
Section titled “Soporte de Modo Oscuro”@Composablefun DarkModeInfoBubble(marker: MarkerState) { val isDarkTheme = isSystemInDarkTheme()
InfoBubble( marker = marker, bubbleColor = if (isDarkTheme) Color(0xFF2D2D2D) else Color.White, borderColor = if (isDarkTheme) Color(0xFF555555) else Color.Black, contentPadding = 12.dp, cornerRadius = 8.dp ) { Text( text = marker.extra as? String ?: "", color = if (isDarkTheme) Color.White else Color.Black, style = MaterialTheme.typography.bodyMedium ) }}Tema Personalizado
Section titled “Tema Personalizado”data class BubbleTheme( val backgroundColor: Color, val borderColor: Color, val textColor: Color, val contentPadding: Dp, val cornerRadius: Dp, val tailSize: Dp) : java.io.Serializable
val DefaultBubbleTheme = BubbleTheme( backgroundColor = Color.White, borderColor = Color.Black, textColor = Color.Black, contentPadding = 8.dp, cornerRadius = 4.dp, tailSize = 8.dp)
val DarkBubbleTheme = BubbleTheme( backgroundColor = Color(0xFF2D2D2D), borderColor = Color(0xFF555555), textColor = Color.White, contentPadding = 12.dp, cornerRadius = 8.dp, tailSize = 10.dp)
@Composablefun ThemedInfoBubble( marker: MarkerState, theme: BubbleTheme = DefaultBubbleTheme, content: @Composable () -> Unit) { InfoBubble( marker = marker, bubbleColor = theme.backgroundColor, borderColor = theme.borderColor, contentPadding = theme.contentPadding, cornerRadius = theme.cornerRadius, tailSize = theme.tailSize, content = content )}Consideraciones de Rendimiento
Section titled “Consideraciones de Rendimiento”Actualizaciones Eficientes
Section titled “Actualizaciones Eficientes”// Good: Use stable keys for markersval markerStates = remember(markersData) { markersData.map { data -> MarkerState( id = "marker_${data.id}", // Stable ID position = data.position, extra = data ) }}
// Avoid: Creating new marker state on every recompositionval markerStates = markersData.map { data -> MarkerState(position = data.position, extra = data) // New instance each time}Gestión de Memoria
Section titled “Gestión de Memoria”// Clear selected marker when navigating awayDisposableEffect(Unit) { onDispose { selectedMarker = null // Clear the InfoBubble }}Mejores Prácticas
Section titled “Mejores Prácticas”Directrices de Diseño
Section titled “Directrices de Diseño”- Contenido Conciso: La InfoBubble debe proporcionar información esencial sin abrumar al usuario
- Tamaño Apropiado: Limitar el ancho de la burbuja para mantener la legibilidad en dispositivos móviles
- Acciones Claras: Si se incluyen botones, aclarar su propósito
- Objetivos táctiles: Asegurar que los elementos interactivos cumplan con los tamaños mínimos de objetivo táctil
Experiencia del Usuario
Section titled “Experiencia del Usuario”- Comportamiento de Cierre: Permitir que los usuarios cierren la burbuja tocando el mapa o marcadores
- Estado de Carga: Mostrar un indicador de carga para contenido que requiere solicitudes de red
- Manejo de Errores: Manejar datos faltantes o inválidos apropiadamente
- Accesibilidad: Proporcionar descripciones de contenido para lectores de pantalla
Consejos de Implementación
Section titled “Consejos de Implementación”// Good: Stable marker referenceval markerState = remember(markerId) { MarkerState(id = markerId, position = position)}
// Good: Efficient content updatesLaunchedEffect(selectedMarkerId) { if (selectedMarkerId != null) { // Load additional data if needed }}
// Avoid: Heavy computation inside bubble contentInfoBubble(marker = marker) { // Avoid expensive operations here val processedData = remember(marker.extra) { processData(marker.extra) // Move to remember } DisplayContent(processedData)}Solución de Problemas
Section titled “Solución de Problemas”Problemas Comunes
Section titled “Problemas Comunes”- La burbuja no aparece: Verificar que el marcador esté configurado correctamente y que la InfoBubble esté dentro de un MapViewScope
- La burbuja no se cierra: Verificar que el renderizado condicional responda adecuadamente a los cambios de estado
- Degradación del rendimiento: Limitar el número de burbujas mostradas simultáneamente y optimizar la composición del contenido
- Problemas de diseño: Usar restricciones de tamaño apropiadas y probar en diversos tamaños de pantalla