-
Notifications
You must be signed in to change notification settings - Fork 29.7k
Adds Continuous Rectangle and Continuous Stadium shapes #27523
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
e420983 to
d4dccd9
Compare
|
Please check the CI failure |
|
We shouldn't say this is Cupertino-related. It's just a shape, it's not platform-specific. Please check all the documentation for mentions of Superellipse, we want to make sure they're all updated (there's at least one that I know of). |
packages/flutter/lib/src/painting/cupertino_rounded_rectangle_border.dart
Outdated
Show resolved
Hide resolved
packages/flutter/lib/src/painting/cupertino_rounded_rectangle_border.dart
Outdated
Show resolved
Hide resolved
packages/flutter/lib/src/painting/cupertino_rounded_rectangle_border.dart
Outdated
Show resolved
Hide resolved
packages/flutter/lib/src/painting/cupertino_rounded_rectangle_border.dart
Outdated
Show resolved
Hide resolved
packages/flutter/lib/src/painting/cupertino_rounded_rectangle_border.dart
Outdated
Show resolved
Hide resolved
packages/flutter/lib/src/painting/cupertino_rounded_rectangle_border.dart
Outdated
Show resolved
Hide resolved
packages/flutter/lib/src/painting/cupertino_rounded_rectangle_border.dart
Outdated
Show resolved
Hide resolved
packages/flutter/lib/src/painting/cupertino_rounded_rectangle_border.dart
Outdated
Show resolved
Hide resolved
|
@Salby suggested ContinuousCornerBorder, which I think is a better name. |
packages/flutter/lib/src/painting/cupertino_rounded_rectangle_border.dart
Outdated
Show resolved
Hide resolved
packages/flutter/lib/src/painting/cupertino_rounded_rectangle_border.dart
Outdated
Show resolved
Hide resolved
packages/flutter/lib/src/painting/cupertino_rounded_rectangle_border.dart
Outdated
Show resolved
Hide resolved
packages/flutter/lib/src/painting/cupertino_rounded_rectangle_border.dart
Outdated
Show resolved
Hide resolved
packages/flutter/lib/src/painting/cupertino_rounded_rectangle_border.dart
Outdated
Show resolved
Hide resolved
packages/flutter/lib/src/painting/cupertino_rounded_rectangle_border.dart
Outdated
Show resolved
Hide resolved
packages/flutter/lib/src/painting/cupertino_rounded_rectangle_border.dart
Outdated
Show resolved
Hide resolved
packages/flutter/lib/src/painting/cupertino_rounded_rectangle_border.dart
Outdated
Show resolved
Hide resolved
packages/flutter/lib/src/painting/cupertino_rounded_rectangle_border.dart
Outdated
Show resolved
Hide resolved
packages/flutter/lib/src/painting/cupertino_rounded_rectangle_border.dart
Outdated
Show resolved
Hide resolved
packages/flutter/lib/src/painting/cupertino_rounded_rectangle_border.dart
Outdated
Show resolved
Hide resolved
|
I don't think we should be adding a feature and renaming the class in the same PR, that's a bit confusing (makes it very hard to review). I suggest we let #27586 take care of the rename. It's not entirely clear to me what the new feature here does? Are there before/after pictures we can see? |
|
@Hixie Ok that's fair. I think the name 'ContinuousCornerRectangleBorder' makes more sense tho? Given that We have 'RoundedRectangleBorder' and not 'RoundedBorder' ? |
|
The term "rounded rectangle" is the name of the shape that "RoundedRectangleBorder" creates; search Google for [Rounded Rectangle] and you get millions of hits and all the pictures clearly describe what we're doing; see also RRect and the corresponding methods of the canvas. On the other hand the phrase "Continuous Corner Rectangle" doesn't have any hits on Google at all. The phrase "Continuous Corner" does have some hits, especially if you search for [ios continuous corner]; indeed there are a number of terms (e.g. continuous curvature, squircle) that are used. None of them, as far as I can tell, have the word "rectangle" in them, though. |
packages/flutter/lib/src/painting/continuous_rectangle_border.dart
Outdated
Show resolved
Hide resolved
| // The multiplier of the radius in comparison to the smallest edge length | ||
| // used to describe the minimum radius for this shape. | ||
| // | ||
| // This is multiplier used in the case of an extreme aspect ratio and a |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I understand what extreme aspect ratio is concretely and why are we doing something special in that case and what happens if we just followed the 3 instead of 2.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So if we used 3 instead of 2 in this "extreme" case, then the shape would still look noticeably flat on the edges instead of rounded like with the search bar on native iOS.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here too. Don't use subjective words. Quantify and explain why those numbers are important.
| // This is multiplier used in the case of an extreme aspect ratio and a | ||
| // small extent value. It can be less than 'maxMultiplier' because there | ||
| // are not enough pixels to render the clipping of the shape at this size so | ||
| // it appears to still be concave (whereas mathematically it's convex). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm still confused about these various usages of concave vs convex. Isn't the shape always convex? When does it become concave? Or are you describing the path drawing glitch caused by this specific implementation of the bezier curve when it clips?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I now realize that a lozenge isn't actually convex. But yah a lozenge doesn't correctly describe the shape that the curves make when they clip as the shape in reality is more like |X| with a lozenge in middle. Which is a convex shape.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we're mixing a bunch of things.
If we're talking about these shapes assuming we produce protruding rendering artifacts when the shapes clip, then:
- If it doesn't clip, it's a simple non-intersecting polygon that is convex
- If it does clip, it is a non-simple self-intersecting polygon (which by definition is neither convex nor concave).
We shouldn't do this since the fact that we produce rendering artifacts is an implementation detail that we use bezier curves and has nothing to do with superellipses themselves.
If we're talking about these shapes assuming we don't render protruding artifacts (e.g. we end up producing a lozenge), then:
- If it doesn't clip (n>2), it's a simple non-intersecting polygon that is convex.
- If it does clip (n<2), then it is still a simple non-intersecting polygon that is convex
packages/flutter/lib/src/painting/continuous_rectangle_border.dart
Outdated
Show resolved
Hide resolved
| // If the smallest edge length is less than this value, the dynamic radius | ||
| // value can be made smaller than the 'maxMultiplier' while the rendered | ||
| // shape still does not visually clip. | ||
| const double minRadiusEdgeLength = 200.0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How is this number derived?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By visual approximation and comparison with the native Cupertino search bar rounded rect.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Then it should be in Cupertino.
| // As the edge length approaches 200, the limitedRadius approaches ~3 –- the | ||
| // multiplier of the radius value where the resulting shape is concave (ie. | ||
| // does not visually clip) at any dimension. | ||
| final double multiplier = ui.lerpDouble( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this needed? Do we think apple is doing this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is to make a more rounded, stadium-esque shape at a small width or height. And yah, I believe apple does this for their search bar.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here, then this in-between-superellipse-and-stadium-ness should be in cupertino.
packages/flutter/lib/src/painting/continuous_stadium_border.dart
Outdated
Show resolved
Hide resolved
|
@chinmaygarde @liyuqian can you review this with an eye towards performance? |
| return Path() | ||
| ..moveTo(leftX(1.52866483), topY(0.0)) | ||
| ..lineTo(rightX(1.52866471), topY(0.0)) | ||
| ..cubicTo(rightX(1.08849323), topY(0.0), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Skia team had a discussion about this "squircles" about 1.5 years ago. It seems that Skia wanted to add something like bool SkPath::asSuperellipse(SkRect*, int* exp); but it's not there yet.
From my own CPU path rasterization experience, cubics are more expensive than quadratics. Although it's unlikely to be a bottleneck for the whole app, it might be safer to just use quadratics as I heard that you can either use a single cubic or 2 quadratics to approximate the squircle.
CC Skia's GPU guru @bsalomon @csmartdalton here for comments about cubics vs. quadratics performance on GPU.
Of course, it would be better if Skia just added bool SkPath::asSuperellipse(SkRect*, int* exp); API so it can freely change the most performant implementation without Flutter modifying the source code. Not sure if the benefit is worth the maintenance cost (CC @reed-at-google ).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The shapes are not yet linked as this PR needs to land before the shape images can be generated. Tho you should be able to see the shapes at this link: https://github.com/flutter/assets-for-api-docs/pull/55/files
| return Path() | ||
| ..moveTo(leftX(2.00593972), topY(0.0)) | ||
| ..lineTo(originX + width - 1.52866483 * radius, originY) | ||
| ..cubicTo(rightX(1.63527834), topY(0.0), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here. If quadratics can be used, I think they're usually more preferred than cubics from Skia's perspective. My anecdotal experience also told me that cubics are more prone to bugs and rendering artifacts than quadratics since Skia has to handle many more edge cases for cubics than quadratics. Skia's major clients such as Chrome and Anroid probably use much more quadratics than cubics which makes quadratics better tested. @reed-at-google : please correct me if my impression were wrong.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cubic paths are definitely present in our framework. If they're prone to bugs or rendering artifacts, we should make a concerted effort to remove them from our codebase and remove the API until ready.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, cubics definitely passes Skia's quality control which means that it's fast and correct probably 99.99% of the time. It's just that quadratics are even faster and more reliable. So if it's easy to use quadratics instead cubics, we'll prefer quadratics.
|
Just checked perf.skia.org for a comparison between quads and cubics. The results are surprising to me on the GPU side. Maybe need some experts @bsalomon and @csmartdalton to verify the results. First I checked path_hairline_big_AA on Moto G4. On the CPU side (config = 8888), quad is twice as fast as cubic (which is expected). But GPU speed looks the same between cubic and quad, and they're all many times slower than CPU. I thought that Skia would just use CPU path renderer to rasterize the path if the GPU renderer is too slow. Maybe this test is just generating non-sense data for GPU? Then I checked path_hairline_big_AA on Pixel2XL which unfortunately doesn't have CPU (8888) performance data. But the GPU data there is really astonishing. The cubic is about 4x as fast as the quad? I don't understand how that could happen. One possibility is that the GPU falls back to CPU on the cubic, but it uses some very slow GPU renderer on the quad similar to those used on MotoG4? |
|
@liyuqian what is the actionable item here for this PR? |
|
Ok, forget about the perf.skia.org data that I put earlier. It's not very helpful to track GPU performance according to the Skia team. Our actionable item, if there's time (there's no hurry to land this), should be to add a Flutter driver performance test ourselves to check the performance difference between quadratics and cubics implementation of this widget. That driver test will also be helpful in the future to catch any unexpected performance regression. |
|
Thanks. @jslavitz, let's add a microbenchmark test to check the performance of a CupertinoButton when you start using this shape in widgets in a next PR. See something like complex_layout_scroll_perf__timeline_summary for example. |
xster
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're getting closer. Still some comments on the documentation and the dynamic ratio on the continuous rect.
| /// In an attempt to keep the shape of the rectangle the same regardless of its | ||
| /// dimension (and to avoid clipping of the shape), the radius will | ||
| /// automatically be lessened if its width or height is less than ~3x the | ||
| /// declared radius. The new resulting radius will always be maximal in respect |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is already implied in the previous sentence. I would just remove this.
| /// to the dimensions of the given rectangle. | ||
| /// | ||
| /// To achieve the corner smoothness with this shape, ~50% more pixels in each | ||
| /// dimensions are used to render each corner. As such, the ~3 represents twice |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This still reads like too much math in the sentence. Just "Therefore, with 2 corners per side, the height or width needs to be minimally ~1.5 * 2 = ~3 times the length of the radius to accommodate the smooth corner transitions."
| /// width of pixels that are used to construct it. For example, if a | ||
| /// rectangle had and a corner radius of 25, in reality ~38 pixels in each | ||
| /// dimension would be used to construct a corner and so ~76px x ~76px would be | ||
| /// the minimal size needed to render this rectangle. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"... needed to render this rectangle before reducing the radius."
Otherwise, it might sound like we can't render this rectangle at all.
| /// corner and so ~76px x ~38px would be used to render both corners on a given | ||
| /// side. | ||
| /// | ||
| /// This shape will always have 4 linear edges and 4 90º curves. However, for |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm asking what's the motivation for doing something special for smaller sizes and not have one single set of rules. The users will be wondering too after reading this paragraph.
| final BorderRadiusGeometry borderRadius; | ||
| /// The radius will be clamped to 0 if a value less than 0 is entered as the | ||
| /// radius. By default the radius is 0. This value must not be null. | ||
| /// |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Each field's doc must be able to stand on its own. Mention here too the fact that the value you put in isn't always what actually gets drawn.
| // If the smallest edge length is less than this value, the dynamic radius | ||
| // value can be made smaller than the 'maxMultiplier' while the rendered | ||
| // shape still does not visually clip. | ||
| const double minRadiusEdgeLength = 200.0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Then it should be in Cupertino.
| // As the edge length approaches 200, the limitedRadius approaches ~3 –- the | ||
| // multiplier of the radius value where the resulting shape is concave (ie. | ||
| // does not visually clip) at any dimension. | ||
| final double multiplier = ui.lerpDouble( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here, then this in-between-superellipse-and-stadium-ness should be in cupertino.
| /// is approximately a gaussian curve instead of a step function as with a | ||
| /// traditional half circle round. | ||
| /// | ||
| /// The ~3 represents twice the ratio (ie. ~3/2) of a corner's declared radius |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this was copy pasted but doesn't fit in the flow of the paragraphs here. Make sure this doc reads right.
| // shape has a side width as the stroke is drawn centered on the border of | ||
| // the shape instead of inside as with the rounded rect and stadium. | ||
| if (sideWidth > 0.0) | ||
| rect = rect.deflate(actualSideWidth / 2.0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't change the value of incoming function parameters. Make new variables.
|
Same here. We'll pick it up from your branch in a new PR. |
|
@xster and which PR is that? |

Adds Cupertino "super ellipse" rect. Fixes #13914, reverts #26295.