Skip to content

Instantly share code, notes, and snippets.

@CanYumusak
Last active March 20, 2025 11:42
Show Gist options
  • Save CanYumusak/34e6620f444d5ba0c8f7419362d5d394 to your computer and use it in GitHub Desktop.
Save CanYumusak/34e6620f444d5ba0c8f7419362d5d394 to your computer and use it in GitHub Desktop.
LabelLayoutModifier which adjusts Android text placement to match Figma text placement
public class LabelLayoutModifier(
val context: Context,
val lineHeight: TextUnit,
val style: MyTextStyle,
) : LayoutModifier {
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
val placeable = measurable.measure(constraints)
val lineCount = lineCount(placeable)
val fullHeight = (lineHeight.toPx() * lineCount).roundToInt()
val fontMetrics = fontMetrics(context, style)
val centerOffset = floor((lineHeight.toPx().toDp() - fontMetrics.descent.toDp() + fontMetrics.ascent.toDp()).value / 2f).dp.toPx().toInt()
val figmaOffset = fontMetrics.ascent - fontMetrics.top
return layout(width = placeable.width, height = fullHeight) {
// Alignment lines are recorded with the parents automatically.
placeable.placeRelative(
x = 0,
y = (centerOffset - figmaOffset).toInt()
)
}
}
override fun IntrinsicMeasureScope.maxIntrinsicHeight(
measurable: IntrinsicMeasurable,
width: Int
): Int {
return ceilToLineHeight(measurable.maxIntrinsicHeight(width))
}
override fun IntrinsicMeasureScope.minIntrinsicHeight(
measurable: IntrinsicMeasurable,
width: Int
): Int {
return ceilToLineHeight(measurable.minIntrinsicHeight(width))
}
override fun IntrinsicMeasureScope.minIntrinsicWidth(
measurable: IntrinsicMeasurable,
height: Int
): Int {
return measurable.minIntrinsicWidth(height)
}
override fun IntrinsicMeasureScope.maxIntrinsicWidth(
measurable: IntrinsicMeasurable,
height: Int
): Int {
return measurable.maxIntrinsicWidth(height)
}
private fun Density.lineCount(placeable: Placeable): Int {
val firstToLast = (placeable[LastBaseline] - placeable[FirstBaseline]).toFloat()
return (firstToLast / lineHeight.toPx()).roundToInt() + 1
}
private fun Density.ceilToLineHeight(value: Int): Int {
val lineHeightPx = lineHeight.toPx()
return (ceil(value.toFloat() / lineHeightPx) * lineHeightPx).roundToInt()
}
}
private fun Density.fontMetrics(context: Context, textStyle: MyTextStyle): Paint.FontMetrics {
val fontResourceId = textStyle.fonts[textStyle.fontWeight]!!
val font = ResourcesCompat.getFont(context, fontResourceId)
val paint = Paint().also {
it.typeface = font
it.textSize = textStyle.fontSize.toPx()
}
return paint.fontMetrics
}
public data class MyTextStyle internal constructor(
val fontSize: TextUnit,
val fontWeight: FontWeight,
val letterSpacing: TextUnit,
val lineHeight: TextUnit,
) {
public val fonts: Map<FontWeight, Int> = mapOf(
WaveFontWeight.Demi.fontWeight to R.font.nationale_demi_bold,
WaveFontWeight.Bold.fontWeight to R.font.nationale_bold,
)
private val fontFamily: FontFamily = FontFamily(
fonts.map { Font(it.value, it.key) }
)
internal fun asTextStyle(): TextStyle {
return TextStyle(
fontSize = fontSize,
fontWeight = fontWeight,
fontFamily = fontFamily,
letterSpacing = letterSpacing,
lineHeight = lineHeight,
)
}
}
@CanYumusak
Copy link
Author

A lot has changed in recent compose versions, thus this is now available for free without custom layouts. I didn't dig as deep on the view system but parts may be applicable, although I assume it should be tedious

@nika-sketch
Copy link

nika-sketch commented Mar 20, 2025

so in compose if i just specify platformStyle = PlatformTextStyle(
includeFontPadding = false,
),
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
), it will match exactly like in figma? without any other calculations?

@CanYumusak
Copy link
Author

That's correct! Just make sure to set the line height, if you don't set it it will not be correct

@nika-sketch
Copy link

i have also searched up and have seen that such small font mismatches can be due to the Renderer of figma and Android and this is normal, i do not know though if i am being wrong :)

@CanYumusak
Copy link
Author

Id recommend reading my article about the topic for details
https://dev.to/canyudev/android-and-figma-typography-and-how-to-achieve-100-fidelity-l40

However you are right, given that these are 2 different renderings it is possible that the result is not sub-pixel similar. I assume that this is good enough for most practical reasons though

@nika-sketch
Copy link

yeah, i have already read that article, really interesting and challenging actually. thank you for your answers!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment