Building a Rating Control using Xamarin.Forms Shapes

In the release of Xamarin.Forms 4.7, Microsoft introduced a Shapes, an API to draw shapes without having to use an external library like SkiaSharp.

In this blog post I will show how you can use Xamarin.Forms and Shapes to create a rating control without using an external library. The complete code for this control can be found here.



Make sure that you are using Xamarin.Forms 4.7 or newer.
Add the experimental flag in your App.xaml.cs file.

 public App()
 {
    InitializeComponent();
    Device.SetFlags(new string[] { "Shapes_Experimental" });
 }

The rating control will be based on StackLayout, so you will create a new class that has StackLayout as its base class.

 public class RatingControl : StackLayout
 {
        public RatingControl()
        {
            Orientation = StackOrientation.Horizontal;
        }
 }

To draw the starts for the control, you need a couple of points, I used Sketch to draw a star and saved it as an svg-file. After that I opened the svg-file in Visual Studio Code to extract the points. In the RatingControl I store the points as a lists:

 private readonly List originalFullStarPoints = new List()
  {
            new Point(96,1.12977573),
            new Point(66.9427701,60.0061542),
            new Point(1.96882894,69.4474205),
            new Point(48.9844145,115.27629),
            new Point(37.8855403,179.987692),
            new Point(96,149.435112),
            new Point(154.11446,179.987692),
            new Point(143.015586,115.27629),
            new Point(190.031171,69.4474205),
            new Point(125.05723,60.0061542),
            new Point(96,1.12977573),
};

 private readonly List originalHalfStarPoints = new List()
 {
            new Point(96,1.12977573),
            new Point(66.9427701,60.0061542),
            new Point(1.96882894,69.4474205),
            new Point(48.9844145,115.27629),
            new Point(37.8855403,179.987692),
            new Point(96,149.435112),
            new Point(96,1.12977573)
};

The control will have a couple of properties that can be used to customize the look of it. Everytime one of these properties changed, it should trigger a redraw of the control:

 private void Draw()
 {
    //We will insert code here later
 }

 private void Set(ref T field, T newValue)
 {
            if (!EqualityComparer.Default.Equals(field, newValue))
            {
                field = newValue;
                Draw();
            }
 }

 public double Rating
 {
            get => (double)GetValue(RatingProperty);
            set => SetValue(RatingProperty, value);
}

 private int max = 5;
 public int Max
 {
            get => max;
            set => Set(ref max, value);
 }

 private Color fillColor = Color.Yellow;
 public Color FillColor
 {
            get => fillColor;
            set => Set(ref fillColor, value);
 }

 private Color strokeColor = Color.Black;
 public Color StrokeColor
 {
            get => strokeColor;
            set => Set(ref strokeColor, value);
 }

 private double strokeThickness = 0;
 public double StrokeThickness
 {
            get => strokeThickness;
            set => Set(ref strokeThickness, value);
 }

 private double size = 50;
 public double Size
 {
            get => size;
            set => Set(ref size, value);
 }

The Rating property should be a BindableProperty so you are able to bind to it:

public static BindableProperty RatingProperty = BindableProperty.Create(nameof(Rating), typeof(double), typeof(RatingControl), 0.0, BindingMode.OneWay, propertyChanged: (bindable, newValue, oldValue) =>
{
             var control = (RatingControl)bindable;

             if (newValue != oldValue)
             {
                 control.Draw();
             }
});

 public double Rating
 {
            get => (double)GetValue(RatingProperty);
            set => SetValue(RatingProperty, value); 
 }

Above you added the original points from the svg file, but because you are able to set size of the stars you need to recalculate them before you draw them and store them in a PointsCollection that you will use in the draw method. Here is how to calculate the new size:

 private readonly PointCollection fullStarPoints = new PointCollection();
 private readonly PointCollection halfStarPoints = new PointCollection();

 private void CalculatePoints(PointCollection calculated, List original)
  {
            calculated.Clear();

            foreach (var point in original)
            {
                var x = point.X * ratio;
                var y = point.Y * ratio;

                var p = new Point(x, y);

                calculated.Add(p);
            }
 }

To draw all different types of stars in this control you need three methods to draw stas, one for a full star, one for a half star, and one for an empty star. Both the full star and the empty star will use the same points and the half start will use points from both. You will use the Polygon object to draw the stars:

private Polygon GetFullStar()
{
            var fullStar = new Polygon()
            {
                Points = fullStarPoints,
                Fill = FillColor,
                StrokeThickness = StrokeThickness,
                Stroke = StrokeColor
            };

            return fullStar;
}

private Polygon GetEmptyStar()
{
            var emptyStar = new Polygon()
            {
                Points = fullStarPoints,
                StrokeThickness = StrokeThickness,
                Stroke = StrokeColor
            };

            return emptyStar;
 }

The half star will be a drawn with two Polygons, therefore you will add them to a Grid so you can have them in layers:

private Grid GetHalfStar()
 {
            var grid = new Grid();

            var halfStar = new Polygon()
            {
                Points = halfStarPoints,
                Fill = fillColor,
                Stroke = Color.Transparent,
                StrokeThickness = 0,
            };

            var emptyStar = new Polygon()
            {
                Points = fullStarPoints,
                StrokeThickness = StrokeThickness,
                Stroke = StrokeColor
            };

            grid.Children.Add(halfStar);
            grid.Children.Add(emptyStar);

            return grid;
 }

The last thing to do is to implement the Draw method, in it you will first calculate the ratio between the new size and the size that I original star was in (in this case, 200). After that you need to use the method you created above to calculate the points that will be used to draw the Polygons. And the last things you do is to call the draw methods to draw the stars:

private double ratio;

private void Draw()
{
            Children.Clear();

            var newRatio = Size / 200;

            if (newRatio != ratio)
            {
                ratio = newRatio;

                CalculatePoints(fullStarPoints, originalFullStarPoints);
                CalculatePoints(halfStarPoints, originalHalfStarPoints);
            }

            for (var i = 1; i <= Max; i++)
            {
                if (Rating >= i)
                {
                    Children.Add(GetFullStar());
                }
                else if(Rating > i - 1)
                {
                    Children.Add(GetHalfStar());
                }
                else
                {
                    Children.Add(GetEmptyStar());
                }
            }
}



The complete code for the control can be found here, https://gist.github.com/dhindrik/4aaa48d5f332222b3ca674cfd1447c6c.

  6/30/2020 - 9:32 AM