From ba89201f37dee2e7bf2c29c252b0fc61a88c98fd Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 4 Mar 2026 16:32:05 +0100 Subject: [PATCH] Better LocationPinMarker --- .../components/LocationPinMarker.kt | 106 ++++++------------ 1 file changed, 33 insertions(+), 73 deletions(-) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/LocationPinMarker.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/LocationPinMarker.kt index a7b25554b5..e0550911f8 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/LocationPinMarker.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/LocationPinMarker.kt @@ -18,9 +18,8 @@ import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Matrix import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.drawscope.Fill @@ -34,8 +33,6 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarType import io.element.android.libraries.designsystem.components.avatar.avatarShape import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight -import kotlin.math.cos -import kotlin.math.sin /** * Variants of location pin markers. @@ -69,17 +66,13 @@ fun LocationPinMarker( Box( modifier = modifier.size(width = PIN_MARKER_WIDTH, height = PIN_MARKER_HEIGHT), ) { - // Draw the pin shape - Canvas( - modifier = Modifier.matchParentSize() - ) { + Canvas(modifier = Modifier.matchParentSize()) { drawPinShape( fillColor = colors.fill, strokeColor = colors.stroke, strokeWidth = 1.dp.toPx(), ) } - val avatarSize = PIN_MARKER_WIDTH - CONTENT_OFFSET * 2 val contentModifier = Modifier .align(Alignment.TopCenter) @@ -143,8 +136,8 @@ private data class LocationPinColors( avatarStoke = Color.Transparent, ) PinVariant.StaleLocation -> LocationPinColors( - fill = ElementTheme.colors.bgSubtlePrimary, - stroke = ElementTheme.colors.borderInteractiveSecondary, + fill = ElementTheme.colors.bgSubtleSecondary, + stroke = ElementTheme.colors.iconDisabled, dotColor = ElementTheme.colors.iconDisabled, avatarStoke = Color.Transparent, ) @@ -156,77 +149,44 @@ private data class LocationPinColors( /** * Draws a teardrop-shaped pin with smooth curves. * - * Based on SVG reference with dimensions 40x48 (ratio 1:1.2). - * Uses quadratic Bezier curves for smooth transitions from circle to tip. + * Based on SVG path with dimensions 40x48 (ratio 1:1.2). + * Scales automatically to fit the canvas size. */ private fun DrawScope.drawPinShape( fillColor: Color, strokeColor: Color, strokeWidth: Float, ) { - val width = size.width - val height = size.height - - val circleRadius = width / 2 - strokeWidth - val circleCenterX = width / 2 - val circleCenterY = width / 2 - - // The tip at the bottom - val tipX = width / 2 - val tipY = height - strokeWidth - - // Angle from the bottom of circle where it transitions to curves (in degrees) - val transitionAngleDeg = 65f - - val rightTransitionAngle = 90f - transitionAngleDeg - val leftTransitionAngle = 90f + transitionAngleDeg - - // Calculate transition points on the circle - val rightTransitionX = circleCenterX + circleRadius * cos(Math.toRadians(rightTransitionAngle.toDouble())).toFloat() - val rightTransitionY = circleCenterY + circleRadius * sin(Math.toRadians(rightTransitionAngle.toDouble())).toFloat() - val leftTransitionX = circleCenterX + circleRadius * cos(Math.toRadians(leftTransitionAngle.toDouble())).toFloat() - val leftTransitionY = circleCenterY + circleRadius * sin(Math.toRadians(leftTransitionAngle.toDouble())).toFloat() - - // Arc sweep: counter-clockwise over the top - val arcSweepAngle = -(360f - 2 * transitionAngleDeg) - - // For cubic Bezier: tangent direction at transition points - // Shorter tangent for smoother transition from circle - val tangentLength = (tipY - leftTransitionY) * 0.45f - - // Left side control points (from left transition to tip) - val leftTangentAngle = leftTransitionAngle - 90.0 - val leftC1X = leftTransitionX + tangentLength * cos(Math.toRadians(leftTangentAngle)).toFloat() - val leftC1Y = leftTransitionY + tangentLength * sin(Math.toRadians(leftTangentAngle)).toFloat() - // C2 control points - horizontal approach creates rounded tip - val tipOffset = 20f - val leftC2X = tipX - tipOffset - val leftC2Y = tipY - strokeWidth - // Right side control points (from tip to right transition) - val rightTangentAngle = rightTransitionAngle + 90.0 - val rightC1X = tipX + tipOffset - val rightC1Y = tipY - strokeWidth - val rightC2X = rightTransitionX + tangentLength * cos(Math.toRadians(rightTangentAngle)).toFloat() - val rightC2Y = rightTransitionY + tangentLength * sin(Math.toRadians(rightTangentAngle)).toFloat() + val svgWidth = 40f + val svgHeight = 48f + val inset = strokeWidth / 2 + val scaleX = (size.width - strokeWidth) / svgWidth + val scaleY = (size.height - strokeWidth) / svgHeight val path = Path().apply { - moveTo(rightTransitionX, rightTransitionY) - arcTo( - rect = Rect( - center = Offset(circleCenterX, circleCenterY), - radius = circleRadius, - ), - startAngleDegrees = rightTransitionAngle, - sweepAngleDegrees = arcSweepAngle, - forceMoveTo = false, - ) - - // Cubic Bezier from left transition point to tip - cubicTo(leftC1X, leftC1Y, leftC2X, leftC2Y, tipX, tipY) - // Cubic Bezier from tip back to right transition point - cubicTo(rightC1X, rightC1Y, rightC2X, rightC2Y, rightTransitionX, rightTransitionY) - + moveTo(20f, 48f) + cubicTo(19.4167f, 48f, 18.8333f, 47.8965f, 18.25f, 47.6895f) + cubicTo(17.6667f, 47.4825f, 17.1458f, 47.1721f, 16.6875f, 46.7581f) + cubicTo(13.9792f, 44.2743f, 11.5833f, 41.8525f, 9.5f, 39.4929f) + cubicTo(7.41667f, 37.1332f, 5.67708f, 34.8461f, 4.28125f, 32.6313f) + cubicTo(2.88542f, 30.4166f, 1.82292f, 28.2846f, 1.09375f, 26.2354f) + cubicTo(0.364583f, 24.1863f, 0f, 22.2303f, 0f, 20.3674f) + cubicTo(0f, 14.1578f, 2.01042f, 9.21087f, 6.03125f, 5.52652f) + cubicTo(10.0521f, 1.84217f, 14.7083f, 0f, 20f, 0f) + cubicTo(25.2917f, 0f, 29.9479f, 1.84217f, 33.9688f, 5.52652f) + cubicTo(37.9896f, 9.21087f, 40f, 14.1578f, 40f, 20.3674f) + cubicTo(40f, 22.2303f, 39.6354f, 24.1863f, 38.9062f, 26.2354f) + cubicTo(38.1771f, 28.2846f, 37.1146f, 30.4166f, 35.7188f, 32.6313f) + cubicTo(34.3229f, 34.8461f, 32.5833f, 37.1332f, 30.5f, 39.4929f) + cubicTo(28.4167f, 41.8525f, 26.0208f, 44.2743f, 23.3125f, 46.7581f) + cubicTo(22.8542f, 47.1721f, 22.3333f, 47.4825f, 21.75f, 47.6895f) + cubicTo(21.1667f, 47.8965f, 20.5833f, 48f, 20f, 48f) close() + + transform(Matrix().apply { + scale(scaleX, scaleY) + translate(inset / scaleX, inset / scaleY) + }) } drawPath(path = path, color = fillColor, style = Fill)