https://kotlinlang.org logo
#compose-android
Title
# compose-android
v

vanshg

03/05/2024, 11:02 PM
Does Compose rendering work differently between emulators and real devices? I'm trying to use a dashPathEffect but I can't set the phase properly because it seems like the stroke starts drawing in different locations Here is my dashPathEffect - it aims to draw a border only around the rounded corners of a roundedRect
Copy code
/**
 * Returns a [PathEffect] that draws a dashed line around the corners of a rounded rectangle
 *
 * @param cornerRadius The radius of the rounded corners
 * @param roundedRectSize The size of the rounded rectangle
 * @param extendCornerBy The amount to extend the corner dashes by. This will be distributed evenly
 * on both ends of each corner
 */
fun roundedRectCornerDashPathEffect(
    cornerRadius: Float,
    roundedRectSize: Size,
    extendCornerBy: Float = 0f,
): PathEffect {
    // Each corner's length is a quarter circle
    val cornerLength = (2 * Math.PI * cornerRadius / 4f).toFloat() + extendCornerBy

    // There are 2 corners, so we subtract 2 * radius from the width (same goes for height)
    val cornerHeight = cornerRadius + (extendCornerBy / 2)
    val roundedRectWidthExcludingCorners = roundedRectSize.width - (2 * cornerHeight)
    val roundedRectHeightExcludingCorners = roundedRectSize.height - (2 * cornerHeight)

    return dashPathEffect(
        intervals = floatArrayOf(
            cornerLength,
            roundedRectWidthExcludingCorners,
            cornerLength,
            roundedRectHeightExcludingCorners,
            cornerLength,
            roundedRectWidthExcludingCorners,
            cornerLength,
            roundedRectHeightExcludingCorners,
        ),
        phase = cornerLength - (extendCornerBy / 2),
    )
}
I use this within a Canvas like so:
Copy code
// Draw the rounded rectangle cutout
        drawRoundRect(
            cornerRadius = CornerRadius(radius),
            size = roundedRectSize,
            topLeft = roundedRectTopLeft,
            color = Color.Black.copy(alpha = cutoutOpacity),
            style = Fill,
            blendMode = BlendMode.SrcIn,
        )

        // Draw the corner borders
        drawRoundRect(
            cornerRadius = CornerRadius(radius),
            size = roundedRectSize,
            topLeft = roundedRectTopLeft,
            color = cornerBorderColor,
            style = Stroke(
                width = 4.dp.toPx(),
                cap = StrokeCap.Round,
                pathEffect = roundedRectCornerDashPathEffect(
                    cornerRadius = radius,
                    roundedRectSize = roundedRectSize,
                    extendCornerBy = 16.dp.toPx(),
                ),
            ),
        )
The same code results in the first image on the emulator and Compose preview, but works on a real device. If I modify the order of
intervals
in the dashPathEffect, then it looks okay in Compose Preview and on an emulator, but fails similarly on a real device. Where does this indeterminism come from? I assume it is related to something about Path/Canvas
🧵 1
r

romainguy

03/05/2024, 11:04 PM
Is the device the same exact config (density, aspect ratio, etc.) compared to the emulator?
v

vanshg

03/05/2024, 11:04 PM
Yes -- Pixel 8 emulator and Pixel 8 real device
r

romainguy

03/05/2024, 11:06 PM
Same exact version of Android?
Looks like the path just doesn’t start at the same position, so could be a change in Skia across versions
v

vanshg

03/05/2024, 11:13 PM
Ah no, different versions. API 34 real device and VanillaIceCream emulator
Is there a way to account for the different path starting position somehow? I tried looking but my surface level look at Canvas apis didn't reveal much
r

romainguy

03/05/2024, 11:18 PM
We now have apis to iterate over paths and you could use that to figure out what's the positions of the corners are, but even then it would be brittle because a corner can be made of one or more curves, and that could change (it has changed over time already)
For your use case I would create my own path with 4 curves for the 4 corners
Much more reliable than dashing a rounded rect
v

vanshg

03/05/2024, 11:21 PM
Noted, I'll look into that. Thank you for the help! I thought dashing the rounded rect approach was going to be simpler 😅
r

romainguy

03/05/2024, 11:22 PM
No, especially once you would realize that you’d have to adapt your dash intervals/data to different resolutions/densities :))
v

vanshg

03/05/2024, 11:23 PM
val radius = 16.dp.toPx()
would not be enough?
r

romainguy

03/05/2024, 11:28 PM
the intervals specifically between the dashes would depend on the screen resolution/aspect ratio
v

vanshg

03/05/2024, 11:30 PM
Ah I guess it would be apparent if I posted my full code. I am only passing in Px sizes to that dashPath helper function, including the roundedRect size
fwiw, changing to this now has consistent behavior between API 34 and VanillaIceCream and the Compose Preview
Copy code
val roundedRect = RoundRect(
    rect = Rect(offset = roundedRectTopLeft, size = roundedRectSize),
    cornerRadius = CornerRadius(radius),
)
drawPath(
    path = Path().apply {
        addRoundRect(roundedRect)
    },
    color = cornerBorderColor,
    style = Stroke(
        width = 4.dp.toPx(),
        cap = StrokeCap.Round,
        pathEffect = roundedRectCornerDashPathEffect(
            cornerRadius = radius,
            roundedRectSize = roundedRectSize,
            extendCornerBy = 16.dp.toPx(),
        ),
    ),
)
that is, drawing path directly but still with dashed rounded rect. As opposed to drawing roundedrect with dashed stroke
still going to investigate the approach you originally mentioned though, of the path with 4 curves, in case this also breaks with some future Skia change. Just thought it was worth mentioning
5 Views