Skip to content
This repository has been archived by the owner on May 1, 2024. It is now read-only.

[Spec] Shapes & Paths #9178

Closed
jsuarezruiz opened this issue Jan 13, 2020 · 19 comments · Fixed by #9218 or #9264
Closed

[Spec] Shapes & Paths #9178

jsuarezruiz opened this issue Jan 13, 2020 · 19 comments · Fixed by #9218 or #9264

Comments

@jsuarezruiz
Copy link
Contributor

jsuarezruiz commented Jan 13, 2020

Shapes

A Shape is a type of View that enables you to draw a shape to the screen.

But, hey, why Shapes?

Currently there are Views such as BoxView or Frame that allow us to create elements such as rectangles or ellipses but, there are many more complex shapes. Having a set of Shapes (lines, polygons, etc.) allows a greater number of possibilities when creating attractive and rich user interfaces.

shapes

Notice that in the design there are:

  • Dash Line
  • Circle (Ellipse)
  • Bubble (Path)
  • Nested Circles (Path)

API

Next, the Shapes API definition.

NOTE: This API definition is based on WPF Shapes API: https://docs.microsoft.com/en-us/dotnet/framework/wpf/graphics-multimedia/shapes-and-basic-drawing-in-wpf-overview

Shape

To render a Shape, must set the Fill property of the Shape to the Color you want. A Shape can also have a Stroke, which is a line that is drawn around the shape's perimeter. A Stroke also requires a Color that defines its appearance, and should have a non-zero value for StrokeThickness. StrokeThickness is a property that defines the perimeter's thickness around the shape edge.

public class Shape : View
{
    public static readonly BindableProperty FillProperty =
        BindableProperty.Create(nameof(Fill), typeof(Color), typeof(Shape), null);

    public static readonly BindableProperty StrokeProperty =
        BindableProperty.Create(nameof(Stroke), typeof(Color), typeof(Shape), null);

    public static readonly BindableProperty StrokeThicknessProperty =
        BindableProperty.Create(nameof(StrokeThickness), typeof(double), typeof(Shape), 1.0);

    public static readonly BindableProperty StrokeDashArrayProperty =
        BindableProperty.Create(nameof(StrokeDashArray), typeof(DoubleCollection), typeof(Shape), null,
            defaultValueCreator: bindable => new DoubleCollection());

    public static readonly BindableProperty StrokeDashOffsetProperty =
        BindableProperty.Create(nameof(StrokeDashOffset), typeof(double), typeof(Shape), 0.0);

    public static readonly BindableProperty AspectProperty =
        BindableProperty.Create(nameof(Aspect), typeof(Stretch), typeof(Shape), Stretch.None);

    public Color Fill
    {
        set { SetValue(FillProperty, value); }
        get { return (Color)GetValue(FillProperty); }
    }

    public Color Stroke
    {
        set { SetValue(StrokeProperty, value); }
        get { return (Color)GetValue(StrokeProperty); }
    }

    public double StrokeThickness
    {
        set { SetValue(StrokeThicknessProperty, value); }
        get { return (double)GetValue(StrokeThicknessProperty); }
    }

    public DoubleCollection StrokeDashArray
    {
        set { SetValue(StrokeDashArrayProperty, value); }
        get { return (DoubleCollection)GetValue(StrokeDashArrayProperty); }
    }

    public double StrokeDashOffset
    {
        set { SetValue(StrokeDashOffsetProperty, value); }
        get { return (double)GetValue(StrokeDashOffsetProperty); }
    }

    public Stretch Aspect
    {
        set { SetValue(AspectProperty, value); }
        get { return (Stretch)GetValue(AspectProperty); }
    }
}

Ellipse

An Ellipse is a shape with a curved perimeter. To create a basic Ellipse, specify a WidthRequest, HeightRequest, and a Color for the Fill.

public sealed class Ellipse : Shape
{

}

Example:

<Ellipse Fill="SteelBlue" HeightRequest="200" WidthRequest="200" />

Ellipse

Rectangle

A Rectangle is a four-sided shape with its opposite sides being equal. To create a basic Rectangle, specify a WidthRequest, a HeightRequest, and a Fill.
You can round the corners of a Rectangle. To create rounded corners, specify a value for the RadiusX and RadiusY properties. These properties specify the x-axis and y-axis of an ellipse that defines the curve of the corners.

public sealed class Rectangle : Shape
{
    public static readonly BindableProperty RadiusXProperty =
        BindableProperty.Create(nameof(RadiusX), typeof(double), typeof(Rectangle), 0.0);

    public static readonly BindableProperty RadiusYProperty =
        BindableProperty.Create(nameof(RadiusY), typeof(double), typeof(Rectangle), 0.0);

    public double RadiusX
    {
        set { SetValue(RadiusXProperty, value); }
        get { return (double)GetValue(RadiusXProperty); }
    }

    public double RadiusY
    {
        set { SetValue(RadiusYProperty, value); }
        get { return (double)GetValue(RadiusYProperty); }
    }
}

Example:

<Rectangle Fill="Blue" Width="200" Height="100" Stroke="Black" StrokeThickness="3" RadiusX="50" RadiusY="10" />

Rectangle

Polygon

A Polygon is a shape with a boundary defined by an arbitrary number of points. The boundary is created by connecting a line from one point to the next, with the last point connected to the first point. The Points property defines the collection of points that make up the boundary.

public sealed class Polygon : Shape
{
    public static readonly BindableProperty PointsProperty =
        BindableProperty.Create(nameof(Points), typeof(PointCollection), typeof(Polygon), null, defaultValueCreator: bindable => new PointCollection());

    public static readonly BindableProperty FillRuleProperty =
        BindableProperty.Create(nameof(FillRule), typeof(FillRule), typeof(Polygon), FillRule.EvenOdd);

    public PointCollection Points
    {
        set { SetValue(PointsProperty, value); }
        get { return (PointCollection)GetValue(PointsProperty); }
    }

    public FillRule FillRule
    {
        set { SetValue(FillRuleProperty, value); }
        get { return (FillRule)GetValue(FillRuleProperty); }
    }
}

The FillRule property specifies a "rule" which the composite shape uses to determine whether a given point is part of the geometry. There are two possible values for FillRule: EvenOdd and Nonzero.

Example:

<Polygon Fill="LightBlue" Points="10,200,60,140,130,140,180,200" />

Polygon

Line

A Line is simply a line drawn between two points in coordinate space. For a Line, make sure to specify values for the Stroke and StrokeThickness properties, because otherwise the Line won't render.

public sealed class Line : Shape
{
    public static readonly BindableProperty X1Property =
        BindableProperty.Create(nameof(X1), typeof(double), typeof(Line), 0.0);

    public static readonly BindableProperty Y1Property =
        BindableProperty.Create(nameof(Y1), typeof(double), typeof(Line), 0.0);

    public static readonly BindableProperty X2Property =
        BindableProperty.Create(nameof(X2), typeof(double), typeof(Line), 0.0);

    public static readonly BindableProperty Y2Property =
        BindableProperty.Create(nameof(Y2), typeof(double), typeof(Line), 0.0);

    public double X1
    {
        set { SetValue(X1Property, value); }
        get { return (double)GetValue(X1Property); }
    }

    public double Y1
    {
        set { SetValue(Y1Property, value); }
        get { return (double)GetValue(Y1Property); }
    }

    public double X2
    {
        set { SetValue(X2Property, value); }
        get { return (double)GetValue(X2Property); }
    }

    public double Y2
    {
        set { SetValue(Y2Property, value); }
        get { return (double)GetValue(Y2Property); }
    }
}

Example:

<Line Stroke="Red" X2="400"/>

Polyline

A Polyline is similar to a Polygon in that the boundary of the shape is defined by a set of points, except the last point in a Polyline is not connected to the first point.

public sealed class Polyline : Shape
{
    public static readonly BindableProperty PointsProperty =
        BindableProperty.Create(nameof(Points), typeof(PointCollection), typeof(Polyline), null, defaultValueCreator: bindable => new PointCollection());

    public static readonly BindableProperty FillRuleProperty =
        BindableProperty.Create(nameof(FillRule), typeof(FillRule), typeof(Polyline), FillRule.EvenOdd);

    public PointCollection Points
    {
        set { SetValue(PointsProperty, value); }
        get { return (PointCollection)GetValue(PointsProperty); }
    }

    public FillRule FillRule
    {
        set { SetValue(FillRuleProperty, value); }
        get { return (FillRule)GetValue(FillRuleProperty); }
    }
}

Example:

<Polyline Stroke="Black" StrokeThickness="4" Points="10,200,60,140,130,140,180,200" />

polyline

Path

A Path is the most versatile Shape because you can use it to define an arbitrary geometry. But with this versatility comes complexity. Define the geometry of a path with the Data property.

public sealed class Path : Shape
{
    public static readonly BindableProperty DataProperty =
            BindableProperty.Create(nameof(Data), typeof(Geometry), typeof(Path), null);

    public Geometry Data
    {
        set { SetValue(DataProperty, value); }
        get { return (Geometry)GetValue(DataProperty); }
    }
}

Geometry

Where Geometry provides a base class for objects that define geometric shapes.

public class Geometry : BindableObject
{

}

There will be:

  • LineGeometry
  • EllipseGeometry
  • RectangleGeometry
  • PathGeometry

LineGeometry

Represents the geometry of a line.

public class LineGeometry : Geometry
{
    public static readonly BindableProperty StartPointProperty =
        BindableProperty.Create(nameof(StartPoint), typeof(Point), typeof(LineGeometry), new Point());

    public static readonly BindableProperty EndPointProperty =
        BindableProperty.Create(nameof(EndPoint), typeof(Point), typeof(LineGeometry), new Point());

    public Point StartPoint
    {
        set { SetValue(StartPointProperty, value); }
        get { return (Point)GetValue(StartPointProperty); }
    }

    public Point EndPoint
    {
        set { SetValue(StartPointProperty, value); }
        get { return (Point)GetValue(StartPointProperty); }
    }
}

EllipseGeometry

Represents the geometry of a circle or ellipse.

public class EllipseGeometry : Geometry
{
    public static readonly BindableProperty CenterProperty =
        BindableProperty.Create(nameof(Center), typeof(Point), typeof(EllipseGeometry), new Point());

    public static readonly BindableProperty RadiusXProperty =
        BindableProperty.Create(nameof(RadiusX), typeof(double), typeof(EllipseGeometry), 0.0);

    public static readonly BindableProperty RadiusYProperty =
        BindableProperty.Create(nameof(RadiusY), typeof(double), typeof(EllipseGeometry), 0.0);

    public Point Center
    {
        set { SetValue(CenterProperty, value); }
        get { return (Point)GetValue(CenterProperty); }
    }

    public double RadiusX
    {
        set { SetValue(RadiusXProperty, value); }
        get { return (double)GetValue(RadiusXProperty); }
    }

    public double RadiusY
    {
        set { SetValue(RadiusYProperty, value); }
        get { return (double)GetValue(RadiusYProperty); }
    }
}

RectangleGeometry

Describes a two-dimensional rectangle.

public class RectangleGeometry : Geometry
{
    public static readonly BindableProperty RectangleProperty =
        BindableProperty.Create(nameof(Rectangle), typeof(Rectangle), typeof(RectangleGeometry), new Rectangle());

    public Rectangle Rectangle
    {
        set { SetValue(RectangleProperty, value); }
        get { return (Rectangle)GetValue(RectangleProperty); }
    }
}

PathGeometry

Represents a complex shape that may be composed of arcs, curves, ellipses, lines, and rectangles.

public sealed class PathGeometry : Geometry
{
    public PathGeometry()
    {
        Figures = new PathFigureCollection();
    }

    public static readonly BindableProperty FiguresProperty =
        BindableProperty.Create(nameof(Figures), typeof(PathFigureCollection), typeof(PathGeometry), null);

    public static readonly BindableProperty FillRuleProperty =
        BindableProperty.Create(nameof(FillRule), typeof(FillRule), typeof(PathGeometry), FillRule.EvenOdd);

    [TypeConverter(typeof(PathFigureCollectionConverter))]
    public PathFigureCollection Figures
    {
        set { SetValue(FiguresProperty, value); }
        get { return (PathFigureCollection)GetValue(FiguresProperty); }
    }

    public FillRule FillRule
    {
        set { SetValue(FillRuleProperty, value); }
        get { return (FillRule)GetValue(FillRuleProperty); }
    }
}

Example:

<Path Stroke="DarkGoldenRod" StrokeThickness="3" Data="M 100,200 C 100,25 400,350 400,175 H 280" />

path

Clip Views

All views (inherited from VisualElement) have the Clip property of type Geometry.

clip-image

(In the previous sample, the header background image is clipped to achieve the diagonal effect).

The following example shows an Image without a defined clip region.

<Image 
     Source="waterlilies.jpg" 
     WidthRequest="200"
     HeightRequest="150" />

image

In the next example, an identical Image is created, except that it has a defined clip region.

<Image 
     Source="waterlilies.jpg" 
     WidthRequest="200" 
     HeightRequest="150">
     <Image.Clip>
     <EllipseGeometry
          RadiusX="100"
          RadiusY="75"
          Center="100, 75"/>
     </Image.Clip>
</Image>

clip-image

Applying Styles

We can customize the appearance of Shapes using styles. We can create the styles using XAML or CSS.

Using XAML:

<Style x:Key="MyRectangle" Target="Rectangle">
     <Setter Property="Fill" Value="Red"  />     
     <Setter Property="Stroke" Value="Black"  />
     <Setter Property="StrokeThickness" Value="2"  />
     <Setter Property="StrokeDashArray" Value="2"  />
</Style>

Using CSS:

#MyRectangle {
  fill: red;
  stroke: black;
  stroke-width: 2px;
  stroke-dasharray: 2;
}

Difficulty : Medium

More info

This Spec is related to #2452

@charlesroddie
Copy link

charlesroddie commented Jan 13, 2020

This seems to me to reinvent the wheel by creating yet another vector graphics library.

There is already a popular vector graphics markup language (svg) and corresponding .net objects (e.g. vvvv/svg).

So the questions are: How is this superior to svg, and how is it better than putting an svg in an image view? If it's superior to svg, why would it take a Xamarin forms dependency rather than being available to the whole world? :)

@BretJohnson
Copy link

One way to tackle this is basing it on XGraphics.
@jsuarezruiz - Let's do a call to discuss. Maybe we can team up here.

XGraphics has primitives like those above (and more). And it addresses some of the points that @charlesroddie raises:

  • It's not Forms specific. It's intended to work everywhere - all flavors of XAML, intended to be the "XAML Standard" for drawing, plus supports non-XAML UIs.
  • It's not a new standard, instead based on the XAML shape primitives in UWP/WinUI/WPF/Sliverlight (all of which are very similar). XAML graphics is something of a standard today, with icon libraries available for it, graphics tools that support exporting to it, etc.

Why not just use SVG? That's actually something I thought long and hard about with XGraphics. I ended up deciding that using XAML as the native graphics format is best since:

  • XAML graphics elements can be embedded in bigger XAML files and use data binding (for theme resources, etc.), animations, and visual states. Creating these dynamic effects isn't possible (at least not cleanly) with SVG + XAML. SVG pairs well with HTML/CSS/JavaScript, but has something of an impedance mismatch when the rest of your UI is in XAML.
  • XAML graphics are familiar to Microsoft ecosystem developers, from WPF/UWP.
  • We may support some graphics primitives that aren't in SVG. It's nice to not be directly dependent on it.

Instead, XGraphics supports SVG like this (which I've just been finishing up):

  • You can convert an SVG to XAML. By converting you can then use the full power of XAML data binding, animations, etc. - on most any SVG asset.
  • You can also render an SVG directly. This uses the converter & then renders the result - it's the same underlying code as the first option, but more convenient when you just want to render the SVG and don't care about doing anything with the XAML directly.

Anyway, feedback from the community is definitely welcome here, on what path we should go down, what features are most valued, etc. Thanks.

@TopSkillDeveloper
Copy link

Thank you, finally I don't have to use a frame corner radius that is a crutch.
Please, do not do automatic scaling, which is in skia sharp

@ernestoyaquello
Copy link

ernestoyaquello commented Jan 14, 2020

It is kinda annoying to write code using Skia or any other library when all you want is a rectangle with round corners and a border (for example), so I think this is a cool idea. Being able to add and customise basic shapes directly in XAML seems quite useful to me.

@jsuarezruiz
Copy link
Contributor Author

jsuarezruiz commented Jan 14, 2020

@BretJohnson Count on it!.
@charlesroddie Good question. I think Bret has listed the differences well.

This Shapes concept is not a new concept, is very similar to XAML shape primitives available in UWP/WinUI/WPF/Silverlight. This Spec proposes an implementation very close to the WPF implementation for example.

  • XAML graphics elements can be used with resources, data binding, animations, visual states, etc. Create the same with SVG + XAML is not that simple.
  • XAML graphics are familiar to the Microsoft ecosystem developers.
  • In this way we could extend it to have other shapes or options in the future not associated with SVG.

Again, good question. On the other hand, having Shapes help the possibility of support SVGs in Xamarin.Forms too.

@TopSkillDeveloper @ernestoyaquello Thanks for your feedback!

@haavamoa
Copy link

I support this 100%. The first thing I noticed and felt like was missing was a proper way of creating different shapes when I started with Xamarin.Forms . This is because I was so used to the easy way of doing it in WPF. My company is using svg vector graphics for our WPF based product, and we wanted a similar way of doing it in Xamarin.Forms. This will enable it for us and will be greatly appreciated.

@jsuarezruiz jsuarezruiz mentioned this issue Jan 15, 2020
2 tasks
@samhouts samhouts added the in-progress This issue has an associated pull request that may resolve it! label Jan 15, 2020
@samhouts samhouts added this to In Progress in v4.6.0 Jan 15, 2020
@juepiezhongren
Copy link

@jsuarezruiz ,why there is no bush?

@dlandi
Copy link

dlandi commented Jan 22, 2020

Dear Microsoft,

You already own Visio...

Take that code and modularize it. Make it X-Platform and put in in Xamarin.

Thanks for listening.

@jsuarezruiz
Copy link
Contributor Author

@juepiezhongren you can see the brushes Spec here #7293 and the implementation in progress here #9220
The Fill and Stroke properties would use a Brush instead of Color.

@samhouts samhouts added this to In Progress in Enhancements Jan 28, 2020
@juepiezhongren
Copy link

@jsuarezruiz ,all apis for XF 5.0?

@minaairsupport
Copy link

cant wait to see this feature released

@samhouts samhouts added this to In Progress in vCurrent (4.8.0) Apr 16, 2020
@samhouts samhouts removed this from In Progress in v4.6.0 Apr 16, 2020
@samhouts samhouts removed this from In Progress in vCurrent (4.8.0) May 11, 2020
@gxrsprite
Copy link

Is it will have Viewbox Control which can scale child shapes like wpf?

@samhouts samhouts added this to In progress in Sprint 170 May 20, 2020
@samhouts samhouts added this to In progress in Sprint 171 May 21, 2020
@GiampaoloGabba
Copy link
Contributor

this is huge! is a big step forward to create complex UI/UX with the same output on iOS/Android.
Finally we will be able to renderer & animate advanced stuff (i'm watching at you clippath) without take a dependency to Skia!

@samhouts samhouts moved this from In progress to Ready for Review (Issues) in Sprint 171 Jun 4, 2020
@StephaneDelcroix
Copy link
Member

Shape class should probably be abstract
For Rectangle, we probably want CornerRadius (like on BoxView), instead of RadiusX/Y
It's weird that you can specify X/Y coords for line, and not for Rectangle/Ellipse

And now, all of this would make more sense if we had a Canvas Layout...

@jsuarezruiz
Copy link
Contributor Author

Thanks for the feedback Stephane!

I agree to make Shapes an abstract class. About a Canvas layout, I thought about it. But will be similar to an AbsoluteLayout, right?. I mean, allow to position shapes with coordinates, etc.

@YZahringer
Copy link
Contributor

Drawing a Line requires fixed coordinates X/Y? Or is it possible to define a line that stretch the container, for a horizontal line separator for example? Currently we have to use a BoxView with a height of 1px for this.

@samhouts samhouts moved this from Ready for Review (Issues) to Done in Sprint 171 Jun 8, 2020
samhouts pushed a commit that referenced this issue Jun 9, 2020
* Added Path definition in Core (Shapes)
Implemented Path in Android and iOS

* Fixed Android build errors

* Fixed UWP Build

* Fixed WPF Build

* Implemented PathRenderer on UWP

* Added unit tests

* Fixed namespaces conflicts in Platform projects

* Changes to fix the build errors

* Implemented Path Transformations in Android and iOS

* Fixed Build error in WPF and UWP

* Implemented Path Transformations in UWP

* Fixed iOS Build error

* Changes to fix the Build (Path namespace conflict)

* More changes to fix the build error

* Fixed Windows Build errors

* Fixed Path size issue on UWP

* Added Shapes_Experimental flag

* Updated path sample

* Updated Android ShapeRenderer size logic

* Added Shape Aspect sample in Core Gallery

* Added more Shapes samples

* Updated UWP PathRenderer size logic

* Updated droid and iOS pathRenderer size logic (same behavior in all the platforms)

* Updated UWP ShapeRenderer

* Implemented Path in WPF Backend

* Fixed build error

* Initial Clip implementation in WPF and UWP (work in progress)

* Added Path implementation on macOS

* Added Clip implementation in Android, iOS and macOS

* Fixed broken unit tests

* Notify the change of Geometry if any of the child properties changed

* Added new sample clipping different views

* Fixed flipped shape issue on macOS

* Added support to Clip using EllipseGeometry, LineGeometry and RectangleGeometry in UWP

* Changed Shape class to be abstract

* Moved Shapes to Xamarin.Forms.Shapes in Android, iOS and macOS

* Moved Shapes to Xamarin.Forms.Shapes namespace in Windows (UWP and WPF)

* Fixed wrong property in LineGeometry

* Fixed build error

* Added Clip Performance sample in Core Gallery

* Update Matrix.cs

* Update RectangleGeometry.cs

* Update Xamarin.Forms.Platform.macOS.csproj

* Some duplicate classes

* Update PointCollectionTests.cs

* Update ImageButtonRenderer.cs

* Update Xamarin.Forms.Platform.iOS.csproj

* Update Xamarin.Forms.Platform.iOS.csproj

* Fixed tabs error

Co-authored-by: Samantha Houts <samhouts@users.noreply.github.com>

fixes #2452 (partially)
fixes #9178
@samhouts samhouts changed the title [Spec] Shapes [Spec] Shapes & Paths Jun 9, 2020
@Tommigun1980
Copy link

Thank you for this!

@ysmoradi
Copy link
Contributor

NOTE: This API definition is based on WPF Shapes API

Thanks for the fantastic work!
Xamarin.Forms.FillRule's Nonzero should be NonZero to match the WPF name

@Tommigun1980
Copy link

Tommigun1980 commented Jun 12, 2020

Unfortunately the edits to the clipping code probably broke frames: #11031

So just a heads-up for anyone thinking about updating.
Edit: Not caused by this feature!

@samhouts samhouts added this to In Progress in 4.7.0 Jun 20, 2020
@samhouts samhouts moved this from In Progress to Done in 4.7.0 Jun 20, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
No open projects
4.7.0
  
Done
Sprint 170
  
Continued in next sprint
Sprint 171
  
Done
Development

Successfully merging a pull request may close this issue.