diff --git a/src/CommunityToolkit.Maui.Core/Primitives/Defaults/RatingViewDefaults.shared.cs b/src/CommunityToolkit.Maui.Core/Primitives/Defaults/RatingViewDefaults.shared.cs
new file mode 100644
index 0000000000..c4b9eca58d
--- /dev/null
+++ b/src/CommunityToolkit.Maui.Core/Primitives/Defaults/RatingViewDefaults.shared.cs
@@ -0,0 +1,52 @@
+namespace CommunityToolkit.Maui.Core;
+
+///
+/// Default Values for RatingView
+///
+public static class RatingViewDefaults
+{
+ ///
+ /// Default value for
+ ///
+ public const int CurrentRating = 0;
+
+ ///
+ /// Default value for
+ ///
+ public const int MaximumRating = 5;
+
+ ///
+ /// Default value for
+ ///
+ public const double Size = 20.0;
+
+ ///
+ /// Default value for
+ ///
+ public const double Spacing = 10.0;
+
+ ///
+ /// Default value for
+ ///
+ public const double StrokeThickness = 7.0;
+
+ ///
+ /// Default value for
+ ///
+ public const bool IsEnabled = true;
+
+ ///
+ /// Default value for
+ ///
+ public static Color FilledBackgroundColor { get; } = Colors.Yellow;
+
+ ///
+ /// Default value for
+ ///
+ public static Color EmptyBackgroundColor { get; } = Colors.Transparent;
+
+ ///
+ /// Default value for
+ ///
+ public static Color StrokeColor { get; } = Colors.Grey;
+}
\ No newline at end of file
diff --git a/src/CommunityToolkit.Maui.Core/Primitives/Rating.cs b/src/CommunityToolkit.Maui.Core/Primitives/Rating.cs
new file mode 100644
index 0000000000..eabf61e612
--- /dev/null
+++ b/src/CommunityToolkit.Maui.Core/Primitives/Rating.cs
@@ -0,0 +1,11 @@
+namespace CommunityToolkit.Maui.Core.Primitives;
+
+///
+public class Rating
+{
+ ///
+ public double? CurrentRating { get; set; }
+
+ ///
+ public object? CommandParameter { get; set; }
+}
diff --git a/src/CommunityToolkit.Maui.Core/Primitives/RatingShape.cs b/src/CommunityToolkit.Maui.Core/Primitives/RatingShape.cs
new file mode 100644
index 0000000000..029ea893d4
--- /dev/null
+++ b/src/CommunityToolkit.Maui.Core/Primitives/RatingShape.cs
@@ -0,0 +1,49 @@
+namespace CommunityToolkit.Maui.Core.Primitives;
+
+///
+/// The type of the shape to display on the rating view
+///
+public readonly struct RatingShape(string pathData)
+{
+ ///
+ /// the data path data from the svg
+ ///
+ public string PathData { get; } = pathData;
+
+ ///
+ /// the default star shape
+ ///
+ public static RatingShape Star { get; } = new(PathShapes.Star);
+
+ ///
+ /// The default heart shape
+ ///
+ public static RatingShape Heart { get; } = new(PathShapes.Heart);
+
+ ///
+ /// The circle shape
+ ///
+ public static RatingShape Circle { get; } = new(PathShapes.Circle);
+
+ ///
+ /// The Like shapes
+ ///
+ public static RatingShape Like { get; } = new(PathShapes.Like);
+
+ ///
+ /// The Dislike
+ ///
+ public static RatingShape Dislike { get; } = new(PathShapes.Dislike);
+}
+
+///
+///
+///
+static class PathShapes
+{
+ public const string Star = "M9 11.3l3.71 2.7-1.42-4.36L15 7h-4.55L9 2.5 7.55 7H3l3.71 2.64L5.29 14z";
+ public const string Heart = "M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z";
+ public const string Circle = "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2z";
+ public const string Like = "M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-1.91l-.01-.01L23 10z";
+ public const string Dislike = "M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v1.91l.01.01L1 14c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z";
+}
diff --git a/src/CommunityToolkit.Maui.UnitTests/Views/RatingView/RatingViewTests.cs b/src/CommunityToolkit.Maui.UnitTests/Views/RatingView/RatingViewTests.cs
new file mode 100644
index 0000000000..c3d8726b63
--- /dev/null
+++ b/src/CommunityToolkit.Maui.UnitTests/Views/RatingView/RatingViewTests.cs
@@ -0,0 +1,162 @@
+using CommunityToolkit.Maui.Core;
+using CommunityToolkit.Maui.Core.Primitives;
+using CommunityToolkit.Maui.Core.Primitives.Defaults;
+using FluentAssertions;
+using Microsoft.Maui.Controls.Shapes;
+using Xunit;
+using Path = Microsoft.Maui.Controls.Shapes.Path;
+
+namespace CommunityToolkit.Maui.UnitTests.Views.RatingView;
+
+public class RatingViewTests : BaseHandlerTest
+{
+ [Fact]
+ public void DefaultInitialization_ShouldHaveCorrectDefaultValues()
+ {
+ // Arrange
+ var ratingView = new Maui.Views.RatingView.RatingView();
+
+ // Assert
+ Assert.Equal(RatingViewDefaults.CurrentRating, ratingView.CurrentRating);
+ Assert.Equal(RatingViewDefaults.MaximumRating, ratingView.MaximumRating);
+ Assert.Equal(RatingViewDefaults.Size, ratingView.Size);
+ Assert.Equal(RatingViewDefaults.FilledBackgroundColor, ratingView.FilledBackgroundColor);
+ Assert.Equal(RatingViewDefaults.EmptyBackgroundColor, ratingView.EmptyBackgroundColor);
+ Assert.Equal(RatingViewDefaults.StrokeThickness, ratingView.StrokeThickness);
+ Assert.Equal(RatingViewDefaults.Spacing, ratingView.Spacing);
+ Assert.Equal(RatingViewDefaults.IsEnabled, ratingView.IsEnabled);
+ }
+
+ [Fact]
+ public void OnControlInitialized_ShouldCreateCorrectNumberOfShapes()
+ {
+ // Arrange
+ var ratingView = new Maui.Views.RatingView.RatingView();
+ ratingView.MaximumRating = 3;
+
+ // Act
+ ratingView.InitializeShape();
+
+ // Assert
+ ratingView.Control?.ColumnDefinitions.Count.Should().Be(3);
+ ratingView.Control?.Children.Count.Should().Be(3);
+ }
+
+ [Fact]
+ public void PropertyChangedEvent_ShouldBeRaised_WhenCurrentRatingChanges()
+ {
+ // Arrange
+ var ratingView = new Maui.Views.RatingView.RatingView();
+ bool eventRaised = false;
+ ratingView.PropertyChanged += (_, e) => { eventRaised = true; };
+
+ // Act
+ ratingView.CurrentRating = 3.5;
+
+ // Assert
+ eventRaised.Should().BeTrue();
+ }
+
+ [Fact]
+ public void Draw_ShouldCreateCorrectNumberOfShapes()
+ {
+ // Arrange
+ var ratingView = new Maui.Views.RatingView.RatingView
+ {
+ MaximumRating = 3
+ };
+
+ // Assert
+ ratingView.Control?.ColumnDefinitions.Count.Should().Be(3);
+ ratingView.Control?.Children.Count.Should().Be(3);
+ }
+
+ [Fact]
+ public void ReDraw_ShouldRecreateShapes()
+ {
+ // Arrange
+ var ratingView = new Maui.Views.RatingView.RatingView
+ {
+ MaximumRating = 3
+ };
+
+ // Act
+ ratingView.ReDraw();
+
+ // Assert
+ ratingView.Control?.ColumnDefinitions.Count.Should().Be(3);
+ ratingView.Control?.Children.Count.Should().Be(3);
+ }
+
+ [Fact]
+ public void UpdateDraw_ShouldUpdateShapesCorrectly()
+ {
+ // Arrange
+ var ratingView = new Maui.Views.RatingView.RatingView
+ {
+ MaximumRating = 3,
+ CurrentRating = 2.5
+ };
+
+ // Act
+ ratingView.UpdateDraw();
+
+ // Assert
+ ratingView.Control?.Children.Count.Should().Be(3);
+#pragma warning disable CS8602 // Dereference of a possibly null reference.
+ (ratingView.Control?.Children[0] as Path).Fill.Should()
+ .BeEquivalentTo(Brush.Yellow);
+ (ratingView.Control?.Children[0] as Path).Stroke.Should()
+ .BeEquivalentTo(Brush.Yellow);
+#pragma warning restore CS8602 // Dereference of a possibly null reference.
+ }
+
+ [Fact]
+ public void InitializeShape_ShouldSetShapeAndControlColumnSpacing()
+ {
+ // Arrange
+ var ratingView = new Maui.Views.RatingView.RatingView();
+
+ // Act
+ ratingView.InitializeShape();
+
+ // Assert
+ ratingView.Shape.Should().NotBeNull();
+ ratingView.Control?.ColumnSpacing.Should().Be(ratingView.Spacing);
+ }
+
+ [Fact]
+ public void OnShapeTapped_ShouldUpdateCurrentRatingCorrectly()
+ {
+ // Arrange
+ var ratingView = new Maui.Views.RatingView.RatingView
+ {
+ MaximumRating = 3,
+ IsEnabled = true
+ };
+
+ Path tappedShape = new()
+ {
+ Data = (Geometry?)new PathGeometryConverter().ConvertFromInvariantString(RatingShape.Star.PathData)
+ };
+
+ // Act
+ ratingView.OnShapeTapped(tappedShape, null);
+
+ // Assert
+ ratingView.CurrentRating.Should().Be(1);
+ }
+
+ [Fact]
+ public void ShapeProperty_ShouldSetShapeCorrectly()
+ {
+ // Arrange
+ var ratingView = new Maui.Views.RatingView.RatingView();
+
+ // Act
+ ratingView.Shape = RatingShape.Heart;
+
+ // Assert
+ ratingView.Shape.Should().Be(RatingShape.Heart);
+ }
+}
\ No newline at end of file
diff --git a/src/CommunityToolkit.Maui/CommunityToolkit.Maui.csproj b/src/CommunityToolkit.Maui/CommunityToolkit.Maui.csproj
index 858cb1cc9e..fd6dcc2070 100644
--- a/src/CommunityToolkit.Maui/CommunityToolkit.Maui.csproj
+++ b/src/CommunityToolkit.Maui/CommunityToolkit.Maui.csproj
@@ -63,8 +63,8 @@
-
-
+
+
diff --git a/src/CommunityToolkit.Maui/Views/RatingView/RatingView.Shared.cs b/src/CommunityToolkit.Maui/Views/RatingView/RatingView.Shared.cs
new file mode 100644
index 0000000000..b493fe914a
--- /dev/null
+++ b/src/CommunityToolkit.Maui/Views/RatingView/RatingView.Shared.cs
@@ -0,0 +1,346 @@
+using CommunityToolkit.Maui.Core;
+using CommunityToolkit.Maui.Core.Primitives;
+using Microsoft.Maui.Controls.Shapes;
+
+namespace CommunityToolkit.Maui.Views.RatingView;
+
+/// RatingView control.
+public class RatingView : TemplatedView
+{
+ /// Rating value bindable property
+ public static readonly BindableProperty CurrentRatingProperty = BindableProperty.Create(nameof(CurrentRating),
+ typeof(double), typeof(RatingView), defaultValue: RatingViewDefaults.CurrentRating, propertyChanged: OnBindablePropertyChanged);
+
+
+ /// MaximumRating rating value Bindable property
+ public static readonly BindableProperty MaximumRatingProperty = BindableProperty.Create(nameof(MaximumRating),
+ typeof(int), typeof(RatingView), defaultValue: RatingViewDefaults.MaximumRating, propertyChanged: OnBindablePropertyChanged);
+
+
+ /// Shape size Bindable property
+ public static readonly BindableProperty SizeProperty = BindableProperty.Create(nameof(Size), typeof(double),
+ typeof(RatingView), defaultValue: RatingViewDefaults.Size, propertyChanged: OnBindablePropertyChanged);
+
+
+ /// Shape filled Color Bindable property.
+ public static readonly BindableProperty FilledBackgroundColorProperty = BindableProperty.Create(nameof(FilledBackgroundColor),
+ typeof(Color), typeof(RatingView), defaultValue: RatingViewDefaults.FilledBackgroundColor, propertyChanged: OnBindablePropertyChanged);
+
+ ///Shape empty Color Bindable property.
+ public static readonly BindableProperty EmptyBackgroundColorProperty = BindableProperty.Create(nameof(EmptyBackgroundColor),
+ typeof(Color), typeof(RatingView), defaultValue: RatingViewDefaults.EmptyBackgroundColor, propertyChanged: OnBindablePropertyChanged);
+
+ ///Shape stroke color bindable property.
+ public static readonly BindableProperty StrokeColorProperty = BindableProperty.Create(nameof(StrokeColor),
+ typeof(Color), typeof(RatingView), defaultValue: RatingViewDefaults.StrokeColor, propertyChanged: OnBindablePropertyChanged);
+
+ ///Shapes space between bindable property.
+ public static readonly BindableProperty SpacingProperty = BindableProperty.Create(nameof(Spacing), typeof(double),
+ typeof(RatingView), defaultValue: RatingViewDefaults.Spacing, propertyChanged: OnBindablePropertyChanged);
+
+ ///Shape clickable property.
+ public static new readonly BindableProperty IsEnabledProperty = BindableProperty.Create(nameof(IsEnabled),
+ typeof(bool), typeof(RatingView), defaultValue: false, propertyChanged: OnIsEnabledChanged);
+
+ ///The shape to be drawn bindable property.
+ public readonly BindableProperty ShapeProperty = BindableProperty.Create(nameof(Shape), typeof(RatingShape),
+ typeof(RatingView), propertyChanged: OnShapePropertyChanged);
+
+ ///Shape stroke thickness bindable property.
+ public static readonly BindableProperty StrokeThicknessProperty = BindableProperty.Create(nameof(StrokeThickness),
+ typeof(double), typeof(RatingView), defaultValue: RatingViewDefaults.StrokeThickness, propertyChanged: OnBindablePropertyChanged);
+
+ Microsoft.Maui.Controls.Shapes.Path[] shapes;
+
+ string shape = string.Empty;
+
+ ///The default constructor of the control.
+ public RatingView()
+ {
+ ControlTemplate = new ControlTemplate(typeof(Grid));
+
+ shapes = new Microsoft.Maui.Controls.Shapes.Path[MaximumRating];
+
+ HorizontalOptions = LayoutOptions.CenterAndExpand;
+
+ InitializeShape();
+
+ Draw();
+ }
+
+ ///Method called everytime the control's Binding Context is changed.
+ protected override void OnBindingContextChanged()
+ {
+ base.OnBindingContextChanged();
+
+ if (Control is not null)
+ {
+ Control.BindingContext = BindingContext;
+ }
+ }
+
+ ///Called everytime a child is added to the control.
+ protected override void OnChildAdded(Element child)
+ {
+ if (Control is null && child is Grid grid)
+ {
+ Control = grid;
+ OnControlInitialized();
+ }
+
+ base.OnChildAdded(child);
+ }
+
+ ///Defines the current rating value.
+ public double CurrentRating
+ {
+ get => (double)GetValue(CurrentRatingProperty);
+ set => SetValue(CurrentRatingProperty, value);
+ }
+
+ ///Defines the maximum value allowed for the rating.
+ public int MaximumRating
+ {
+ get => (int)GetValue(MaximumRatingProperty);
+ set => SetValue(MaximumRatingProperty, value);
+ }
+
+ ///Defines the size of each drawn shapes.
+ public double Size
+ {
+ get => (double)GetValue(SizeProperty);
+ set => SetValue(SizeProperty, value);
+ }
+
+ ///Defines the color to fill with the drawn shape.
+ public Color FilledBackgroundColor
+ {
+ get => (Color)GetValue(FilledBackgroundColorProperty);
+ set => SetValue(FilledBackgroundColorProperty, value);
+ }
+
+ ///Defines the color of the drawn shape is not filled.
+ public Color EmptyBackgroundColor
+ {
+ get => (Color)GetValue(EmptyBackgroundColorProperty);
+ set => SetValue(EmptyBackgroundColorProperty, value);
+ }
+
+ ///Defines the color of the shape's stroke(border).
+ public Color StrokeColor
+ {
+ get => (Color)GetValue(StrokeColorProperty);
+ set => SetValue(StrokeColorProperty, value);
+ }
+
+ ///Defines the thickness of the shape's stroke(border).
+ public double StrokeThickness
+ {
+ get => (double)GetValue(StrokeThicknessProperty);
+ set => SetValue(StrokeThicknessProperty, value);
+ }
+
+ ///Defines the space between the drawn shapes.
+ public double Spacing
+ {
+ get => (double)GetValue(SpacingProperty);
+ set => SetValue(SpacingProperty, value);
+ }
+
+ ///Defines if the drawn shapes can be clickable to rate.
+ public new bool IsEnabled
+ {
+ get => (bool)GetValue(IsEnabledProperty);
+ set => SetValue(IsEnabledProperty, value);
+ }
+
+ ///Defines the shape to be drawn.
+ public RatingShape Shape
+ {
+ get => (RatingShape)GetValue(ShapeProperty);
+ set => SetValue(ShapeProperty, value);
+ }
+
+ ///The control to be displayed
+ public Grid? Control { get; private set; }
+
+ static void OnIsEnabledChanged(BindableObject bindable, object oldValue, object newValue)
+ {
+ var ratingView = (RatingView)bindable;
+ ratingView.HandleIsEnabledChanged();
+ ratingView.ReDraw();
+ }
+
+ static void OnBindablePropertyChanged(BindableObject bindable, object oldValue, object newValue)
+ {
+ ((RatingView)bindable).ReDraw();
+ }
+
+ static void OnShapePropertyChanged(BindableObject bindable, object oldValue, object newValue)
+ {
+ ((RatingView)bindable).InitializeShape();
+ ((RatingView)bindable).ReDraw();
+ }
+
+ void OnShapeTapped(object? sender, TappedEventArgs? e)
+ {
+ if (sender is not Microsoft.Maui.Controls.Shapes.Path tappedShape)
+ {
+ return;
+ }
+
+ if (Control is not null)
+ {
+ int columnIndex = Control.GetColumn(tappedShape);
+
+ if (MaximumRating > 1)
+ {
+ CurrentRating = columnIndex + 1;
+ }
+ }
+ }
+
+ void Draw()
+ {
+ for (int i = 0; i < MaximumRating; i++)
+ {
+ Control?.ColumnDefinitions.Add(new ColumnDefinition { Width = Size });
+
+ Microsoft.Maui.Controls.Shapes.Path image = new()
+ {
+ Data = (Geometry?)new PathGeometryConverter().ConvertFromInvariantString(shape)
+ };
+
+ if (i <= CurrentRating)
+ {
+ image.Fill = FilledBackgroundColor;
+ image.Stroke = FilledBackgroundColor;
+ image.Aspect = Stretch.Uniform;
+ image.HeightRequest = Size;
+ image.WidthRequest = Size;
+ }
+ else
+ {
+ image.Fill = EmptyBackgroundColor;
+ image.Stroke = StrokeColor;
+
+ image.StrokeLineJoin = PenLineJoin.Round;
+ image.StrokeLineCap = PenLineCap.Round;
+ image.StrokeThickness = StrokeThickness;
+
+ image.Aspect = Stretch.Uniform;
+ image.HeightRequest = Size;
+ image.WidthRequest = Size;
+ }
+
+
+ if (IsEnabled)
+ {
+ var tapGestureRecognizer = new TapGestureRecognizer();
+ tapGestureRecognizer.Tapped += OnShapeTapped;
+ image.GestureRecognizers.Add(tapGestureRecognizer);
+ }
+
+ Control?.Children.Add(image);
+ Control?.SetColumn(image, i);
+
+ shapes[i] = image;
+ }
+
+
+ UpdateDraw();
+ }
+
+ void ReDraw()
+ {
+ Control?.Children.Clear();
+
+ Control?.ColumnDefinitions.Clear();
+
+ shapes = new Microsoft.Maui.Controls.Shapes.Path[MaximumRating];
+
+ Draw();
+
+ UpdateDraw();
+ }
+
+ void UpdateDraw()
+ {
+ for (int i = 0; i < MaximumRating; i++)
+ {
+ var image = shapes[i];
+
+ if (CurrentRating >= i + 1)
+ {
+ image.HeightRequest = Size;
+ image.WidthRequest = Size;
+ image.StrokeLineJoin = PenLineJoin.Round;
+ image.StrokeThickness = StrokeThickness;
+ image.Stroke = FilledBackgroundColor;
+ }
+ else
+ {
+ image.HeightRequest = Size;
+ image.WidthRequest = Size;
+ if (CurrentRating % 1 is 0)
+ {
+ image.Fill = EmptyBackgroundColor;
+ image.Stroke = StrokeColor;
+ image.StrokeThickness = StrokeThickness;
+ image.StrokeLineJoin = PenLineJoin.Round;
+ image.StrokeLineCap = PenLineCap.Round;
+ }
+ else
+ {
+ var fraction = CurrentRating - Math.Floor(CurrentRating);
+ var element = shapes[(int)(CurrentRating - fraction)];
+ {
+ var colors = new GradientStopCollection
+ {
+ new(FilledBackgroundColor, (float)fraction),
+ new(EmptyBackgroundColor, (float)fraction)
+ };
+
+ element.Fill =
+ new LinearGradientBrush(colors, new Point(0, 0), new Point(1, 0));
+ element.StrokeThickness = StrokeThickness;
+ element.StrokeLineJoin = PenLineJoin.Round;
+ element.Stroke = StrokeColor;
+ }
+ }
+ }
+ }
+ }
+
+ void InitializeShape()
+ {
+ if (Control is not null)
+ {
+ Control.ColumnSpacing = Spacing;
+ }
+
+ StrokeColor = Colors.Transparent;
+
+ shape = Shape.PathData;
+ }
+
+ // Ensure VisualElement.IsEnabled always matches RatingView.IsEnabled
+ void HandleIsEnabledChanged()
+ {
+ base.IsEnabled = IsEnabled;
+ }
+
+ void OnControlInitialized()
+ {
+ shapes = new Microsoft.Maui.Controls.Shapes.Path[MaximumRating];
+
+ HorizontalOptions = LayoutOptions.CenterAndExpand;
+
+ if (Control is not null)
+ {
+ Control.ColumnSpacing = Spacing;
+ }
+
+ Draw();
+ }
+}
\ No newline at end of file