Skip to content
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

[Views] - Implement Shadow widget (resolves #31) #32

Open
wants to merge 16 commits into
base: main
Choose a base branch
from

Conversation

suragch
Copy link
Collaborator

@suragch suragch commented Jun 15, 2024

This PR adds a Shadow widget to mimic the behavior of the SwiftUI .shadow modifier method. The reason for implementing Shadow as a composable Widget rather than a property on a shape is that SwiftUI's shadow is a View modifier method. You can add a shadow to anything in SwiftUI, not just a shape.

You can see the results below. SwiftUI is first and Flutter second:

Screenshot 2024-06-15 at 17 16 10 Screenshot 2024-06-15 at 17 16 22

Because non-rectangular shapes can have shadows, I wasn't able to use Flutter's BoxShadow.

Shadow itself is a composite of a few new widgets:

  • Blur
  • Tint
  • Desaturate

I haven't exported those widgets yet, but tint is another SwiftUI property, so we may want to export this widget as well in the future.

I discovered I had made a mistake on the Rectangle stroke. It actually is centered on the edge line, not contained completely within the rectangle. I had missed that before because of SwiftUI's clipping behavior. This means that Ellipse and Rectangle have the same stroke behavior. I snuck this fix into this PR since it related to drawing shadows. You can see the width of the rectangle with the thick stroke is wider than the other rectangles. This is the proper behavior:

Screenshot 2024-06-15 at 09 22 02

@suragch suragch requested a review from matthew-carroll June 15, 2024 09:52
@suragch
Copy link
Collaborator Author

suragch commented Jun 21, 2024

@matthew-carroll Pinging you on this in case you didn't see it earlier.

example/lib/shapes/shadow.dart Outdated Show resolved Hide resolved
example/lib/shapes/shadow.dart Show resolved Hide resolved

/// A widget that adds a shadow effect to its child widget.
class Shadow extends StatelessWidget {
/// Creates a Shadow widget with the given properties.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When there's only a single constructor, I tend to find that docs on the constructor don't help much. For example "Creates a Shadow widget with the given properties" is exactly what the code tell us, without docs.

Given that there's just one constructor, I suggest providing all relevant widget-wide details in the class Dart Doc, and then provide per property clarifications in the property docs.

For example, one thing I would expect to see explained in the class Dart Doc is how a Shadow widget applies a shadow to its child. How does Shadow know the shape of its child? Are there any further requirements to make this widget work? Etc.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I removed the constructor Dart Doc and improved the class one. I didn't try to explain how Shadow knows the shape of its child, though. That seems like an implementation detail that is unnecessary to know at a high level. Shadows in swift_ui just work no matter what the shape is (unlike in vanilla Flutter), so there is nothing special a developer needs to do.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exposing implementation details in docs is fine, so long as those details are directly related to the applicability of the object.

For example, let's say that I display a bitmap image of an oval with transparency. Then I apply a shadow. In Photoshop, I'll get a shadow around the oval. But in Figma I get a shadow around the rectangle of the whole bitmap. Shadows have internal details that leak out and we should make them clear so that people don't have the wrong expectations.

I assume that the underlying shadow mechanism in SwiftUI is based around some kind of vector graphics protocol, is that right? If you modify a bitmapw ith transparency in SwiftUI with a shadow, does the shadow apply to the image bounds, or the visible bitmap bounds?

Assuming that SwiftUI does the same thing that we do, we should at least document what that restriction is.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand now. I updated the doc comments to reflect how the shadow works.

I'm not sure if SwiftUI uses a vector graphics protocol or not, but the shadow applies to the visible parts of the image, even for a bitmap with transparency. I tested this with the dash_hello image. Here it is in SwiftUI:

Screenshot 2024-07-25 at 10 08 36
Image("dash_hello")
    .resizable()
    .aspectRatio(contentMode: .fit)
    .shadow(color: .gray, radius: 10, x: 10, y: 10)
    .frame(width: 200, height: 200)

And this is the Flutter version:

Screenshot 2024-07-25 at 10 08 47
Frame(
  width: 200,
  height: 200,
  Shadow(
    color: Color(0xFF8E8E93),
    radius: 10.0,
    x: 10.0,
    y: 10.0,
    Image.asset(
      'assets/images/dash_hello/dash_hello.png',
      fit: BoxFit.contain,
    ),
  ),
),

The behavior is the same for both.

Widget build(BuildContext context) {
return Stack(
children: [
Transform.translate(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you check what Swift UI does with semi-translucent shapes that have a shadow applied?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When applying a shadow to a semi-translucent shape in SwiftUI, the shadow itself appears to retain its opacity:

Screenshot 2024-07-02 at 16 48 50
Ellipse()
    .fill(Color.yellow)
    .shadow(color: .gray, radius: 10, x: 10, y: 10)
    .frame(width: 200, height: 100)

Ellipse()
    .fill(Color.yellow.opacity(0.3))
    .shadow(color: .gray, radius: 10, x: 10, y: 10)
    .frame(width: 200, height: 100)

This is not the case with the current implementation in Flutter. The shadow is also lightened based on the child's opacity:

Screenshot 2024-07-02 at 16 53 27
Frame(
  width: 200,
  height: 100,
  Shadow(
    color: color,
    radius: radius,
    x: x,
    y: y,
    Ellipse(
      fillColor: Colors.yellow,
    ),
  ),
),

Frame(
  width: 200,
  height: 100,
  Shadow(
    color: color,
    radius: radius,
    x: x,
    y: y,
    Ellipse(
      fillColor: Colors.yellow.withOpacity(0.3),
    ),
  ),
),

Should we match the SwiftUI version? In my opinion, the Flutter version looks better, but if we want a pixel perfect copy, the answer is probably yes. If so, I'll need to think about how to do that.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If possible, let's do what SwiftUI does. The Flutter example that you included looks like what I'd expect if I applied an opacity around the whole widget tree, including the shadow. But if I only applied opacity to the shape's color, I wouldn't expect that to impact the shadow.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated the widget implementation so that the widget opacity and shadow opacity are now independent of each other. The flutter version now works like the SwiftUI version for partial transparency in the widget but full opacity for the inner shadow color:

Screenshot 2024-07-25 at 04 48 13

The implementation had the side effect of removing antialiasing from the edges before the blurring step. So if the blur radius was 0.0, the jagged edges were noticeable. So in the case when radius is 0.0, I now retain the original widget opacity (including its antialiasing) for the shadow. That means there still is a rare edge case where someone uses a semitransparent widget with a 0-radius shadow. In that case the shadow would also be semitransparent. This would be quite rare and a slightly more transparent shadow wouldn't look bad (in my opinion), so I recommend we don't worry about it for now.

lib/src/drawing_and_graphics/shadow.dart Outdated Show resolved Hide resolved
lib/src/drawing_and_graphics/shadow.dart Outdated Show resolved Hide resolved
lib/src/drawing_and_graphics/shadow.dart Outdated Show resolved Hide resolved
@suragch
Copy link
Collaborator Author

suragch commented Jul 25, 2024

@matthew-carroll Ready for another review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants