Better LocationPinMarker
This commit is contained in:
parent
046d135e4b
commit
ba89201f37
1 changed files with 33 additions and 73 deletions
|
|
@ -18,9 +18,8 @@ import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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.Color
|
||||||
|
import androidx.compose.ui.graphics.Matrix
|
||||||
import androidx.compose.ui.graphics.Path
|
import androidx.compose.ui.graphics.Path
|
||||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||||
import androidx.compose.ui.graphics.drawscope.Fill
|
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.components.avatar.avatarShape
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import kotlin.math.cos
|
|
||||||
import kotlin.math.sin
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Variants of location pin markers.
|
* Variants of location pin markers.
|
||||||
|
|
@ -69,17 +66,13 @@ fun LocationPinMarker(
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier.size(width = PIN_MARKER_WIDTH, height = PIN_MARKER_HEIGHT),
|
modifier = modifier.size(width = PIN_MARKER_WIDTH, height = PIN_MARKER_HEIGHT),
|
||||||
) {
|
) {
|
||||||
// Draw the pin shape
|
Canvas(modifier = Modifier.matchParentSize()) {
|
||||||
Canvas(
|
|
||||||
modifier = Modifier.matchParentSize()
|
|
||||||
) {
|
|
||||||
drawPinShape(
|
drawPinShape(
|
||||||
fillColor = colors.fill,
|
fillColor = colors.fill,
|
||||||
strokeColor = colors.stroke,
|
strokeColor = colors.stroke,
|
||||||
strokeWidth = 1.dp.toPx(),
|
strokeWidth = 1.dp.toPx(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val avatarSize = PIN_MARKER_WIDTH - CONTENT_OFFSET * 2
|
val avatarSize = PIN_MARKER_WIDTH - CONTENT_OFFSET * 2
|
||||||
val contentModifier = Modifier
|
val contentModifier = Modifier
|
||||||
.align(Alignment.TopCenter)
|
.align(Alignment.TopCenter)
|
||||||
|
|
@ -143,8 +136,8 @@ private data class LocationPinColors(
|
||||||
avatarStoke = Color.Transparent,
|
avatarStoke = Color.Transparent,
|
||||||
)
|
)
|
||||||
PinVariant.StaleLocation -> LocationPinColors(
|
PinVariant.StaleLocation -> LocationPinColors(
|
||||||
fill = ElementTheme.colors.bgSubtlePrimary,
|
fill = ElementTheme.colors.bgSubtleSecondary,
|
||||||
stroke = ElementTheme.colors.borderInteractiveSecondary,
|
stroke = ElementTheme.colors.iconDisabled,
|
||||||
dotColor = ElementTheme.colors.iconDisabled,
|
dotColor = ElementTheme.colors.iconDisabled,
|
||||||
avatarStoke = Color.Transparent,
|
avatarStoke = Color.Transparent,
|
||||||
)
|
)
|
||||||
|
|
@ -156,77 +149,44 @@ private data class LocationPinColors(
|
||||||
/**
|
/**
|
||||||
* Draws a teardrop-shaped pin with smooth curves.
|
* Draws a teardrop-shaped pin with smooth curves.
|
||||||
*
|
*
|
||||||
* Based on SVG reference with dimensions 40x48 (ratio 1:1.2).
|
* Based on SVG path with dimensions 40x48 (ratio 1:1.2).
|
||||||
* Uses quadratic Bezier curves for smooth transitions from circle to tip.
|
* Scales automatically to fit the canvas size.
|
||||||
*/
|
*/
|
||||||
private fun DrawScope.drawPinShape(
|
private fun DrawScope.drawPinShape(
|
||||||
fillColor: Color,
|
fillColor: Color,
|
||||||
strokeColor: Color,
|
strokeColor: Color,
|
||||||
strokeWidth: Float,
|
strokeWidth: Float,
|
||||||
) {
|
) {
|
||||||
val width = size.width
|
val svgWidth = 40f
|
||||||
val height = size.height
|
val svgHeight = 48f
|
||||||
|
val inset = strokeWidth / 2
|
||||||
val circleRadius = width / 2 - strokeWidth
|
val scaleX = (size.width - strokeWidth) / svgWidth
|
||||||
val circleCenterX = width / 2
|
val scaleY = (size.height - strokeWidth) / svgHeight
|
||||||
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 path = Path().apply {
|
val path = Path().apply {
|
||||||
moveTo(rightTransitionX, rightTransitionY)
|
moveTo(20f, 48f)
|
||||||
arcTo(
|
cubicTo(19.4167f, 48f, 18.8333f, 47.8965f, 18.25f, 47.6895f)
|
||||||
rect = Rect(
|
cubicTo(17.6667f, 47.4825f, 17.1458f, 47.1721f, 16.6875f, 46.7581f)
|
||||||
center = Offset(circleCenterX, circleCenterY),
|
cubicTo(13.9792f, 44.2743f, 11.5833f, 41.8525f, 9.5f, 39.4929f)
|
||||||
radius = circleRadius,
|
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)
|
||||||
startAngleDegrees = rightTransitionAngle,
|
cubicTo(0.364583f, 24.1863f, 0f, 22.2303f, 0f, 20.3674f)
|
||||||
sweepAngleDegrees = arcSweepAngle,
|
cubicTo(0f, 14.1578f, 2.01042f, 9.21087f, 6.03125f, 5.52652f)
|
||||||
forceMoveTo = false,
|
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)
|
||||||
// Cubic Bezier from left transition point to tip
|
cubicTo(40f, 22.2303f, 39.6354f, 24.1863f, 38.9062f, 26.2354f)
|
||||||
cubicTo(leftC1X, leftC1Y, leftC2X, leftC2Y, tipX, tipY)
|
cubicTo(38.1771f, 28.2846f, 37.1146f, 30.4166f, 35.7188f, 32.6313f)
|
||||||
// Cubic Bezier from tip back to right transition point
|
cubicTo(34.3229f, 34.8461f, 32.5833f, 37.1332f, 30.5f, 39.4929f)
|
||||||
cubicTo(rightC1X, rightC1Y, rightC2X, rightC2Y, rightTransitionX, rightTransitionY)
|
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()
|
close()
|
||||||
|
|
||||||
|
transform(Matrix().apply {
|
||||||
|
scale(scaleX, scaleY)
|
||||||
|
translate(inset / scaleX, inset / scaleY)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
drawPath(path = path, color = fillColor, style = Fill)
|
drawPath(path = path, color = fillColor, style = Fill)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue