https://kotlinlang.org logo
Title
o

orangy

03/21/2021, 5:45 PM
Left button is how Figma draws it, right button is screenshot from DC app. There are two issues here. First is sizing, the distance between guide lines is exactly 64 (Figma size units), and you can see that center of the border goes exactly along guides, while DC app has slightly bigger size (the size of the button is 32.dp, so at Retina resolution it should be same 64px). Second problem is quality of the rounding corners rendering. You can clearly see (especially if you zoom it), that they appear thicker than straight line, and anti-aliasing is somewhat of mediocre quality.
Oops, the screenshot doesn’t show the first problem properly at this zoom level, here is more detailed
Here is some blog post on some details from Figma, didn’t read it thoroughly though, but might be helpful https://www.figma.com/blog/desperately-seeking-squircles/
r

romainguy

03/21/2021, 8:01 PM
They don’t look rendered at the same resolution/density
o

orangy

03/21/2021, 8:16 PM
This is screenshot from the app, corners look bolder
This is a button with 9-slices from image made with Figma.
An example side by side.
(that’s not exactly just rounding corners on the left, it’s a bit with that squircles effect, but anyway, it’s same width all the way)
j

jim

03/22/2021, 7:24 AM
cc @Nader Jawad
s

Sergey Y.

03/25/2021, 5:49 PM
Are there any updates on this? I remember running into a similar problem with rounded rectangles.
Probably community could make a library similar to the one Flutter has https://github.com/Salby/superellipse_shape Flutter issue: https://github.com/flutter/flutter/issues/13914
n

Nader Jawad

03/25/2021, 6:11 PM
The difference here is a squircle vs a round rect. And we do not currently have a primitive call for a squircle. The team is also investigating improvements to the border API as well
👍 1
❤️ 1
The interesting thing about border is most folks think that drawing a stroke with the same corner radius as a round rect would achieve the same rounded corner result. However, this does not take into account the corner radius of the stroke at the outer edge. Rather when a corner radius is specified on a stroke, that ends up being the radius of the corner at the mid point of the stroke itself. Additionally the stroked shape ends up being larger since the stroke is centered on the point it is drawn on, so half of the stroke width is handled on the internal to the shape with the other half being external. The issue becomes exaggerated with larger and larger stroke width and attempts to draw a rounded rect stroke on stop of a filled in rounded rect will show the filled rounded rect below.
s

Sergey Y.

03/25/2021, 6:21 PM
Thank you for the response @Nader Jawad. Do you think it is worth implementing a library like the one above? What if I were to draw a squircle border using a path and what about performance in such case?
n

Nader Jawad

03/25/2021, 6:25 PM
You can use paths, however, there will be a performance hit as path rendering is CPU bound. In general favor usage of canvas drawing primitives (drawRect/drawRoundRect/drawCircle etc.) over creating paths unless you absolutely need to. Another thing to note is the example shows a filled in squircle as opposed to the stroked one. As described above, the stroke parameters would need to be adjusted if the intention is to get the same size/shape as a filled in equivalent with the interior removed.
s

Sergey Y.

03/25/2021, 6:26 PM
I see. Thanks 👍
o

orangy

03/27/2021, 2:13 PM
@Nader Jawad I’m not sure I understand fully the explanation and the “squircle vs a round rect”. All I want is that round rect shape combined with border modifier would result in a nice uniform border, which doesn’t look “bolder” at the corner. It’s as simple as
Modifier.clip(RoundedCornerShape(8.dp)).border(BorderStroke(2.dp, Color(0xFF623D15)), RoundedCornerShape(8.dp))
Am I doing something wrong here?
In SVG if I do it like this
<svg width="400" height="180">
    <rect x="50" y="20" rx="20" ry="20" width="150" height="150"
          style="fill:red;stroke:black;stroke-width:5;opacity:0.5" />
</svg>
I get this, without any issue for the corners.
Okay, I looked under the hood. There is some hackery-hack happening inside
Modifier.border
function. First it adjusts stroke width like
val strokeWidth = 1.2f * borderSize
with some long explanation text above which is not entirely clear (to me), and then it draws another rounded rect of hairline width (1px always) on top of it. So I made a copy of this function, removed both hacks, and I see that border has now proper width. But then, there is background visible through antialiased pixels of the border at the corners. I believe, that hackery was introduced to fix this, but I think it’s not really proper way, because of the resulting effect.
The more I think about drawing proper borders with filling, the more I believe we should have
PaintingStyle.StrokeAndFill
or even get rid of the selector altogether and have
PaintingStyle
a data class with nullable stroke and fill brushes. Skia has this
PaintMode.STROKE_AND_FILL
but it is unused, so I believe it is impossible to make it via compose. I don’t think it is possible to properly emulate this with separate fill and stroke, due to all the antialiasing.
r

romainguy

03/27/2021, 8:35 PM
If I remember the Skia implementation stroke and fill is literally just that: it draws twice; a fill then a stroke
(unless they've improved it over the years)
Anti-aliasing shouldn't be an issue with two passes since the same color/shader is used for both stroke and fill
Ah according to the native headed file it seems like Skia does have an optimized path now that won't fill pixels twice. But it shouldn't impact correctness
o

orangy

03/27/2021, 9:31 PM
@romainguy well, then I’m kinda lost on my way to fix this border drawing problem… any ideas?
r

romainguy

03/27/2021, 9:39 PM
I think what you highlighted above in the source is the problem
I don't quite understand the comment either but it seems this causes more harm
If the problem is that the background is visible "between" the fill and the stroke we should consider a different solution
Is there a stroke alignment option? A centered stroke should fix this and you can see this is what figma is doing
o

orangy

03/27/2021, 9:41 PM
The problem in compose is that corners are wrong due to hacks. The problem in solution I have (removed hacks and removed that 1.2 multiplier) is that background is leaking through corners. Let me make a better screenshot showing the problem
r

romainguy

03/27/2021, 9:42 PM
right I understand the problem you're facing when you remove the hack
What I'm saying is that the hack is definitely wrong and should definitely be unnecessary with a centered stroke. Instead of messing with the stroke size it should be possible to either ensure Skia does a center stroke or instead adjust the coordinates of the rounded rect to align it properly
In any case it's definitely a compose but we should fix
o

orangy

03/27/2021, 9:44 PM
The stroke is not centered, it is explicitly pushed inside with some insets
If I remove the insets, it gets clipped, and it still has color leaking from background on the outer edge, probably because of antialiased clipping
Now, if I remove clipPath from drawing the border… It gets better, but obviously the “perceived” size is wrong
so basically what needs to be done, probably, is push both background and border inwards by half of the border size, so the outer size is kept intact, and background is not leaking at antialiasing… for that, we should make it
.shape(borderStroke, fillBrush)
instead of separate background and border and clip modifiers… I can do that for myself, of course 🙂
Okay, implemented, thanks for guidance! Here is the source if anyone needs, including compose team 🙂 https://gist.github.com/e2d2a27201657e3eb921da9d45776b5a (note, I didn’t yet test on paths)
n

Nader Jawad

03/27/2021, 11:22 PM
There's a few things to note here. Because strokes are always centered on the shape itself, the corner radius specified is at the center of the stroke. So the arc outside of the shape, on the exterior of the stroke will have a larger radius than that specified. So as a result drawing a filled rounded rect with a stroked rounded rect on top will end up showing the pixels of the background itself. This is not as noticeable for smaller stroke widths + corner radii but is more noticeable for larger radii/stroke values. The attempted solution (which needs to be revised) attempts to clip the to the shape and draw slightly beyond the bounds with the expectation that the additional size of the stroke would be clipped out. Additionally the 1 pixel stroke is drawn on top to cover up the aliasing that is done as a result of clipping on older Android versions. More recent releases of Android have anti-aliased clipping by default.
Because the intention is to get the rounded corner radius on the exterior of the stroke, not only would the size of the shape need to be inset by half of the stroke width but the corner radius would also need to be inset by half of the stroke width as well to ensure that the exterior of the rounded corner is of the desired size.
I'm not sure what FILL_AND_STROKE buys you in this regard, since the exterior shape will still be larger due to the stroke. Modifier.border is intended to just draw a border and not handle the background since that can be handled elsewhere (i.e. a specific background modifier). There is no option for exterior/interior/centered stroke. I remember discussing this feature with the skia team and it wasn't feasible for all paths (ex. how does a figure 8 shape draw with an interior or exterior stroke?) and all strokes are drawn centered.
We also try to avoid Path operations whenever we can as path rendering is software rendered by default. While we do have a primitive to draw rounded rectangles that all have the same corner radius, it might be more beneficial to draw each of the corners separately with drawArc and the lines to connect them via drawLine
o

orangy

03/27/2021, 11:33 PM
@Nader Jawad thanks for details! I’ve solved my problem here for a while (clearly
Mine
version is better than
Compose
above), but I’d really appreciate if compose would do the right thing out of the box. I will also think about adjusting radius for half of stroke size, to adapt for outer/inner, but not sure how to do this properly for radius less then half of stroke width – should it become rectangular at this point? Anyway, my solution works for me.
n

Nader Jawad

03/27/2021, 11:38 PM
Yes this is a known issue for compose and we're working on it
o

orangy

03/27/2021, 11:39 PM
Would you mind sharing a bug report I can track?
r

romainguy

03/28/2021, 12:08 AM
@Nader Jawad Good to hear that strokes are centered, it's what I would expect (some APIs and design tools let you use inset or outset strokes instead)
Do you know if design tools try to make the outside of the stroke have the specified radius? Or do they not bother (which is what I would expect)
At least in Photoshop you'd define the rounded rect as a path and stroke that so I would not expect the radius to be "correct" on the outside of the stroke
o

orangy

03/28/2021, 12:14 AM
@romainguy I checked with Figma, and they don’t bother. That’s figma’s outline on top of what I did with my modifier (and it doesn’t bother), and they match exactly
r

romainguy

03/28/2021, 12:16 AM
Thanks, that’s what I suspected. We should match what design tools do when possible
(Path rendering in 2D and transparency in 3D are the two most annoying problems to graphics engineers :))
n

Nader Jawad

03/28/2021, 1:43 AM
I don't have the ticket number offhand @orangy but I'll be sure to find it and share or just file a new ticket to track if I can't find the original
o

orangy

03/28/2021, 10:55 AM
Thanks! Now I need to deal with shadows…
m

mzgreen

04/15/2021, 8:04 AM
I've created a ticket for that issue: https://issuetracker.google.com/u/1/issues/185055659
:tnx: 1
n

Nader Jawad

06/07/2021, 6:32 PM
We merged a fixed for this issue that will be part of the next compose release
👍 4