From 2a25d1da57afb19fb04ff454c22d2b52b3631be9 Mon Sep 17 00:00:00 2001 From: "github@internetwideworld.com" Date: Wed, 21 Aug 2024 16:15:50 +0100 Subject: [PATCH 01/92] Implemented basic TemplatedView. Added sample pages. --- .../AppShell.xaml.cs | 1 + .../CommunityToolkit.Maui.Sample.csproj | 23 +- .../MauiProgram.cs | 39 +- .../RatingView/RatingViewCsharpPage.xaml | 15 + .../RatingView/RatingViewCsharpPage.xaml.cs | 11 + .../Views/RatingView/RatingViewXamlPage.xaml | 175 +++++++++ .../RatingView/RatingViewXamlPage.xaml.cs | 8 + .../RatingView/RatingViewCsharpViewModel.cs | 4 + .../RatingView/RatingViewXamlViewModel.cs | 3 + .../ViewModels/Views/ViewsGalleryViewModel.cs | 2 + .../Defaults/RatingViewDefaults.shared.cs | 54 +++ .../Primitives/RatingViewOptions.shared.cs | 11 + .../Primitives/RatingViewShape.cs | 35 ++ .../Views/RatingView/RatingViewTests.cs | 86 +++++ .../Views/RatingView.shared.cs | 351 ++++++++++++++++++ 15 files changed, 797 insertions(+), 21 deletions(-) create mode 100644 samples/CommunityToolkit.Maui.Sample/Pages/Views/RatingView/RatingViewCsharpPage.xaml create mode 100644 samples/CommunityToolkit.Maui.Sample/Pages/Views/RatingView/RatingViewCsharpPage.xaml.cs create mode 100644 samples/CommunityToolkit.Maui.Sample/Pages/Views/RatingView/RatingViewXamlPage.xaml create mode 100644 samples/CommunityToolkit.Maui.Sample/Pages/Views/RatingView/RatingViewXamlPage.xaml.cs create mode 100644 samples/CommunityToolkit.Maui.Sample/ViewModels/Views/RatingView/RatingViewCsharpViewModel.cs create mode 100644 samples/CommunityToolkit.Maui.Sample/ViewModels/Views/RatingView/RatingViewXamlViewModel.cs create mode 100644 src/CommunityToolkit.Maui.Core/Primitives/Defaults/RatingViewDefaults.shared.cs create mode 100644 src/CommunityToolkit.Maui.Core/Primitives/RatingViewOptions.shared.cs create mode 100644 src/CommunityToolkit.Maui.Core/Primitives/RatingViewShape.cs create mode 100644 src/CommunityToolkit.Maui.UnitTests/Views/RatingView/RatingViewTests.cs create mode 100644 src/CommunityToolkit.Maui/Views/RatingView.shared.cs diff --git a/samples/CommunityToolkit.Maui.Sample/AppShell.xaml.cs b/samples/CommunityToolkit.Maui.Sample/AppShell.xaml.cs index cc7b1fde87..0c1e7a26aa 100644 --- a/samples/CommunityToolkit.Maui.Sample/AppShell.xaml.cs +++ b/samples/CommunityToolkit.Maui.Sample/AppShell.xaml.cs @@ -135,6 +135,7 @@ public partial class AppShell : Shell CreateViewModelMapping(), CreateViewModelMapping(), CreateViewModelMapping(), + CreateViewModelMapping(), // Add PlatformSpecific View Models CreateViewModelMapping(), diff --git a/samples/CommunityToolkit.Maui.Sample/CommunityToolkit.Maui.Sample.csproj b/samples/CommunityToolkit.Maui.Sample/CommunityToolkit.Maui.Sample.csproj index 7350208f90..ff34af8eeb 100644 --- a/samples/CommunityToolkit.Maui.Sample/CommunityToolkit.Maui.Sample.csproj +++ b/samples/CommunityToolkit.Maui.Sample/CommunityToolkit.Maui.Sample.csproj @@ -51,7 +51,7 @@ - + @@ -77,6 +77,27 @@ + + + RatingViewXamlPage.xaml + + + + + + MSBuild:Compile + + + MSBuild:Compile + + + + + + MSBuild:Compile + + + true android-arm;android-arm64;android-x86;android-x64 diff --git a/samples/CommunityToolkit.Maui.Sample/MauiProgram.cs b/samples/CommunityToolkit.Maui.Sample/MauiProgram.cs index c7536e684b..78c30439dc 100644 --- a/samples/CommunityToolkit.Maui.Sample/MauiProgram.cs +++ b/samples/CommunityToolkit.Maui.Sample/MauiProgram.cs @@ -33,7 +33,6 @@ using Microsoft.Maui.Platform; using Polly; - #if WINDOWS10_0_17763_0_OR_GREATER using Microsoft.UI; using Microsoft.UI.Composition.SystemBackdrops; @@ -74,32 +73,31 @@ public static MauiApp CreateMauiApp() fonts.AddFont("Font Awesome 6 Brands-Regular-400.otf", FontFamilies.FontAwesomeBrands); }); - builder.ConfigureLifecycleEvents(events => { #if WINDOWS10_0_17763_0_OR_GREATER - events.AddWindows(wndLifeCycleBuilder => - { - wndLifeCycleBuilder.OnWindowCreated(window => - { - window.SystemBackdrop = new MicaBackdrop { Kind = MicaKind.Base }; + events.AddWindows(wndLifeCycleBuilder => + { + wndLifeCycleBuilder.OnWindowCreated(window => + { + window.SystemBackdrop = new MicaBackdrop { Kind = MicaKind.Base }; - var titleBar = window.GetAppWindow()!.TitleBar; + var titleBar = window.GetAppWindow()!.TitleBar; - titleBar.PreferredHeightOption = TitleBarHeightOption.Tall; + titleBar.PreferredHeightOption = TitleBarHeightOption.Tall; - window.ExtendsContentIntoTitleBar = false; + window.ExtendsContentIntoTitleBar = false; - IntPtr nativeWindowHandle = WinRT.Interop.WindowNative.GetWindowHandle(window); - WindowId win32WindowsId = Win32Interop.GetWindowIdFromWindow(nativeWindowHandle); - AppWindow winuiAppWindow = AppWindow.GetFromWindowId(win32WindowsId); + IntPtr nativeWindowHandle = WinRT.Interop.WindowNative.GetWindowHandle(window); + WindowId win32WindowsId = Win32Interop.GetWindowIdFromWindow(nativeWindowHandle); + AppWindow winuiAppWindow = AppWindow.GetFromWindowId(win32WindowsId); - if (winuiAppWindow.Presenter is OverlappedPresenter p) - { - p.SetBorderAndTitleBar(true, true); - } - }); - }); + if (winuiAppWindow.Presenter is OverlappedPresenter p) + { + p.SetBorderAndTitleBar(true, true); + } + }); + }); #endif }); @@ -131,7 +129,6 @@ static void RegisterViewsAndViewModels(in IServiceCollection services) services.AddTransient(); services.AddTransient(); - // Add Alerts Pages + ViewModels services.AddTransientWithShellRoute(); services.AddTransientWithShellRoute(); @@ -236,6 +233,8 @@ static void RegisterViewsAndViewModels(in IServiceCollection services) services.AddTransientWithShellRoute(); services.AddTransientWithShellRoute(); + services.AddTransientWithShellRoute(); + services.AddTransientWithShellRoute(); services.AddTransientWithShellRoute(); services.AddTransientWithShellRoute(); diff --git a/samples/CommunityToolkit.Maui.Sample/Pages/Views/RatingView/RatingViewCsharpPage.xaml b/samples/CommunityToolkit.Maui.Sample/Pages/Views/RatingView/RatingViewCsharpPage.xaml new file mode 100644 index 0000000000..de9a16d5f1 --- /dev/null +++ b/samples/CommunityToolkit.Maui.Sample/Pages/Views/RatingView/RatingViewCsharpPage.xaml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/samples/CommunityToolkit.Maui.Sample/Pages/Views/RatingView/RatingViewCsharpPage.xaml.cs b/samples/CommunityToolkit.Maui.Sample/Pages/Views/RatingView/RatingViewCsharpPage.xaml.cs new file mode 100644 index 0000000000..d97dced804 --- /dev/null +++ b/samples/CommunityToolkit.Maui.Sample/Pages/Views/RatingView/RatingViewCsharpPage.xaml.cs @@ -0,0 +1,11 @@ +using CommunityToolkit.Maui.Sample.ViewModels.Views; + +namespace CommunityToolkit.Maui.Sample.Pages.Views; + +public partial class RatingViewCsharpPage : BasePage +{ + public RatingViewCsharpPage(RatingViewCsharpViewModel viewModel) : base(viewModel) + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/samples/CommunityToolkit.Maui.Sample/Pages/Views/RatingView/RatingViewXamlPage.xaml b/samples/CommunityToolkit.Maui.Sample/Pages/Views/RatingView/RatingViewXamlPage.xaml new file mode 100644 index 0000000000..e3d3b2afd7 --- /dev/null +++ b/samples/CommunityToolkit.Maui.Sample/Pages/Views/RatingView/RatingViewXamlPage.xaml @@ -0,0 +1,175 @@ + + + + + + + \ No newline at end of file diff --git a/samples/CommunityToolkit.Maui.Sample/Pages/Views/RatingView/RatingViewXamlPage.xaml.cs b/samples/CommunityToolkit.Maui.Sample/Pages/Views/RatingView/RatingViewXamlPage.xaml.cs new file mode 100644 index 0000000000..3b4000351d --- /dev/null +++ b/samples/CommunityToolkit.Maui.Sample/Pages/Views/RatingView/RatingViewXamlPage.xaml.cs @@ -0,0 +1,8 @@ +using CommunityToolkit.Maui.Sample.ViewModels.Views; + +namespace CommunityToolkit.Maui.Sample.Pages.Views; + +public partial class RatingViewXamlPage : BasePage +{ + public RatingViewXamlPage(RatingViewXamlViewModel viewModel) : base(viewModel) => InitializeComponent(); +} \ No newline at end of file diff --git a/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/RatingView/RatingViewCsharpViewModel.cs b/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/RatingView/RatingViewCsharpViewModel.cs new file mode 100644 index 0000000000..be9c468b76 --- /dev/null +++ b/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/RatingView/RatingViewCsharpViewModel.cs @@ -0,0 +1,4 @@ +// Ignore Spelling: csharp +namespace CommunityToolkit.Maui.Sample.ViewModels.Views; + +public class RatingViewCsharpViewModel : BaseViewModel; \ No newline at end of file diff --git a/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/RatingView/RatingViewXamlViewModel.cs b/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/RatingView/RatingViewXamlViewModel.cs new file mode 100644 index 0000000000..67f5f343db --- /dev/null +++ b/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/RatingView/RatingViewXamlViewModel.cs @@ -0,0 +1,3 @@ +namespace CommunityToolkit.Maui.Sample.ViewModels.Views; + +public class RatingViewXamlViewModel : BaseViewModel; \ No newline at end of file diff --git a/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/ViewsGalleryViewModel.cs b/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/ViewsGalleryViewModel.cs index 99591003b3..e6787e7ad5 100644 --- a/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/ViewsGalleryViewModel.cs +++ b/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/ViewsGalleryViewModel.cs @@ -29,6 +29,8 @@ public sealed class ViewsGalleryViewModel() : BaseGalleryViewModel( SectionModel.Create("Anchor Popup", Colors.Red, "Popups can be anchored to other view's on the screen"), SectionModel.Create("Popup Layout Page", Colors.Red, "Popup.Content demonstrated using different layouts"), SectionModel.Create("Popup Sizing Issues Page", Colors.Red, "A page demonstrating how Popups can be styled in a .NET MAUI application."), + SectionModel.Create("RatingView XAML Page", Colors.Red, "A page demonstrating the RatingView control and possible uses using XAML"), + SectionModel.Create("RatingView C# Page", Colors.Red, "A page demonstrating the RatingView control and possible uses using C#"), SectionModel.Create("Show Popup in OnAppearing", Colors.Red, "Proves that we now support showing a popup before the platform is even ready."), SectionModel.Create("Semantic Order View", Colors.Red, "SemanticOrderView allows developers to indicate the focus order of visible controls when a user is navigating via TalkBack (Android), VoiceOver (iOS) or Narrator (Windows)."), SectionModel.Create("Popup Style Page", Colors.Red, "A page demonstrating how Popups can be styled in a .NET MAUI application.") 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..b8c8b2a9ef --- /dev/null +++ b/src/CommunityToolkit.Maui.Core/Primitives/Defaults/RatingViewDefaults.shared.cs @@ -0,0 +1,54 @@ +// Ignore Spelling: color +namespace CommunityToolkit.Maui.Core; + +using System.ComponentModel; +using CommunityToolkit.Maui.Core.Primitives; + +/// Default Values for RatingView +[EditorBrowsable(EditorBrowsableState.Never)] +public static class RatingViewDefaults +{ + /// Default rating current rating. + [EditorBrowsable(EditorBrowsableState.Never)] + public const double CurrentRating = 0.0; + + /// Default enabled rating view. + [EditorBrowsable(EditorBrowsableState.Never)] + public const bool IsEnabled = true; + + /// Default maximum value for the rating. + [EditorBrowsable(EditorBrowsableState.Never)] + public const byte MaximumRating = 5; + + /// Maximum number of ratings. + [EditorBrowsable(EditorBrowsableState.Never)] + public const byte MaximumRatings = 25; + + /// Default size of a rating. + [EditorBrowsable(EditorBrowsableState.Never)] + public const double Size = 20.0; + + /// Default spacing between ratings. + [EditorBrowsable(EditorBrowsableState.Never)] + public const double Spacing = 10.0; + + /// Default border thickness for a rating. + [EditorBrowsable(EditorBrowsableState.Never)] + public const double BorderThickness = 1.0; + + /// Default background color for an empty rating. + [EditorBrowsable(EditorBrowsableState.Never)] + public static Color EmptyBackgroundColor { get; } = Colors.Transparent; + + /// Default filled color for a rating. + [EditorBrowsable(EditorBrowsableState.Never)] + public static Color FilledBackgroundColor { get; } = Colors.Yellow; + + /// Default border color for a rating. + [EditorBrowsable(EditorBrowsableState.Never)] + public static Color BorderColor { get; } = Colors.Grey; + + /// Default rating shape. + [EditorBrowsable(EditorBrowsableState.Never)] + public static RatingViewShape Shape { get; } = RatingViewShape.Star; +} \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Core/Primitives/RatingViewOptions.shared.cs b/src/CommunityToolkit.Maui.Core/Primitives/RatingViewOptions.shared.cs new file mode 100644 index 0000000000..0ae51665b0 --- /dev/null +++ b/src/CommunityToolkit.Maui.Core/Primitives/RatingViewOptions.shared.cs @@ -0,0 +1,11 @@ +namespace CommunityToolkit.Maui.Core.Primitives; + +/// Visual options for rating view. +public class RatingViewOptions +{ + ///Current rating of a rating view. + public double? CurrentRating { get; set; } + + ///Gets the parameter associated with the command. + public object? RatingViewCommandParameter { get; set; } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Core/Primitives/RatingViewShape.cs b/src/CommunityToolkit.Maui.Core/Primitives/RatingViewShape.cs new file mode 100644 index 0000000000..4fabdc838f --- /dev/null +++ b/src/CommunityToolkit.Maui.Core/Primitives/RatingViewShape.cs @@ -0,0 +1,35 @@ +namespace CommunityToolkit.Maui.Core.Primitives; + +/// Shapes available for the rating view. +/// Path shape data . +public readonly struct RatingViewShape(string pathData) +{ + /// Custom data path for the shape of the rating. + public string PathData { get; } = pathData; + + /// Star shape. + /// Default shape. + public static RatingViewShape Star { get; } = new(PathShapes.Star); + + /// Heart shape. + public static RatingViewShape Heart { get; } = new(PathShapes.Heart); + + /// Circle shape. + public static RatingViewShape Circle { get; } = new(PathShapes.Circle); + + /// Thumb like shape. + public static RatingViewShape Like { get; } = new(PathShapes.Like); + + /// Thumb dislike shape. + public static RatingViewShape Dislike { get; } = new(PathShapes.Dislike); +} + +/// SVG defined path shapes. +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"; +} \ No newline at end of file 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..c82ffdfe97 --- /dev/null +++ b/src/CommunityToolkit.Maui.UnitTests/Views/RatingView/RatingViewTests.cs @@ -0,0 +1,86 @@ +using CommunityToolkit.Maui.Core; +using CommunityToolkit.Maui.Core.Primitives; +using FluentAssertions; +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(); + + // Assert + Assert.Equal(RatingViewDefaults.BorderColor, ratingView.BorderColor); + Assert.Equal(RatingViewDefaults.BorderThickness, ratingView.BorderThickness); + Assert.Equal(RatingViewDefaults.CurrentRating, ratingView.CurrentRating); + Assert.Equal(RatingViewDefaults.EmptyBackgroundColor, ratingView.EmptyBackgroundColor); + Assert.Equal(RatingViewDefaults.FilledBackgroundColor, ratingView.FilledBackgroundColor); + Assert.Equal(RatingViewDefaults.IsEnabled, ratingView.IsEnabled); + Assert.Equal(RatingViewDefaults.MaximumRating, ratingView.MaximumRating); + Assert.Equal(RatingViewDefaults.Size, ratingView.Size); + Assert.Equal(RatingViewDefaults.Spacing, ratingView.Spacing); + } + + [Fact] + public void OnControlInitialized_ShouldCreateCorrectNumberOfShapes() + { + const int maximumRating = 3; + Maui.Views.RatingView ratingView = new(); + ratingView.Control?.ColumnDefinitions.Count.Should().Be(RatingViewDefaults.MaximumRating); + ratingView.Control?.Children.Count.Should().Be(RatingViewDefaults.MaximumRating); + + ratingView.MaximumRating = maximumRating; + ratingView.Control?.ColumnDefinitions.Count.Should().Be(maximumRating); + ratingView.Control?.Children.Count.Should().Be(maximumRating); + } + + [Fact] + public void PropertyChangedEvent_ShouldBeRaised_WhenCurrentRatingChanges() + { + const double currentRating = 3.5; + Maui.Views.RatingView ratingView = new(); + ratingView.CurrentRating.Should().Be(RatingViewDefaults.CurrentRating); + + bool signaled = false; + ratingView.PropertyChanged += (sender, e) => + { + if (e.PropertyName == "CurrentRating") + { + signaled = true; + } + }; + + ratingView.CurrentRating = currentRating; + ratingView.CurrentRating.Should().Be(currentRating); + signaled.Should().BeTrue(); + } + + [Fact] + public void Draw_ShouldCreateCorrectNumberOfShapes() + { + const int maximumRating = 3; + var ratingView = new Maui.Views.RatingView + { + MaximumRating = maximumRating + }; + + ratingView.Control?.ColumnDefinitions.Count.Should().Be(maximumRating); + ratingView.Control?.Children.Count.Should().Be(maximumRating); + } + + [Fact] + public void ShapeProperty_ShouldSetShapeCorrectly() + { + Maui.Views.RatingView ratingView = new() + { + Shape = RatingViewShape.Heart + }; + + ratingView.Shape.Should().Be(RatingViewShape.Heart); + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Maui/Views/RatingView.shared.cs b/src/CommunityToolkit.Maui/Views/RatingView.shared.cs new file mode 100644 index 0000000000..69ad37b96a --- /dev/null +++ b/src/CommunityToolkit.Maui/Views/RatingView.shared.cs @@ -0,0 +1,351 @@ +// Ignore Spelling: color, bindable + +using CommunityToolkit.Maui.Core; +using Microsoft.Maui.Controls.Shapes; + +namespace CommunityToolkit.Maui.Views; + +/// Rating view control. +public class RatingView : TemplatedView +{ + /// The backing store for the bindable property. + public static readonly BindableProperty BorderColorProperty = BindableProperty.Create(nameof(BorderColor), typeof(Color), typeof(RatingView), defaultValue: RatingViewDefaults.BorderColor, propertyChanged: OnBindablePropertyChanged); + + /// The backing store for the bindable property. + public static readonly BindableProperty BorderThicknessProperty = BindableProperty.Create(nameof(BorderThickness), typeof(double), typeof(RatingView), defaultValue: RatingViewDefaults.BorderThickness, propertyChanged: OnBindablePropertyChanged); + + /// The backing store for the bindable property. + public static readonly BindableProperty CurrentRatingProperty = BindableProperty.Create(nameof(CurrentRating), typeof(double), typeof(RatingView), defaultValue: RatingViewDefaults.CurrentRating, propertyChanged: OnBindablePropertyChanged); + + /// The backing store for the bindable property. + public readonly BindableProperty CustomShapeProperty = BindableProperty.Create(nameof(CustomShape), typeof(string), typeof(RatingView), defaultValue: null); + + /// The backing store for the bindable property. + public static readonly BindableProperty EmptyBackgroundColorProperty = BindableProperty.Create(nameof(EmptyBackgroundColor), typeof(Color), typeof(RatingView), defaultValue: RatingViewDefaults.EmptyBackgroundColor, propertyChanged: OnBindablePropertyChanged); + + /// The backing store for the bindable property. + public static readonly BindableProperty FilledBackgroundColorProperty = BindableProperty.Create(nameof(FilledBackgroundColor), typeof(Color), typeof(RatingView), defaultValue: RatingViewDefaults.FilledBackgroundColor, propertyChanged: OnBindablePropertyChanged); + + /// The backing store for the bindable property. + public static new readonly BindableProperty IsEnabledProperty = BindableProperty.Create(nameof(IsEnabled), typeof(bool), typeof(RatingView), defaultValue: RatingViewDefaults.IsEnabled, propertyChanged: OnIsEnabledChanged); + + /// The backing store for the bindable property. + public static readonly BindableProperty MaximumRatingProperty = BindableProperty.Create(nameof(MaximumRating), typeof(byte), typeof(RatingView), defaultValue: RatingViewDefaults.MaximumRating, validateValue: maximumRatingValidator, propertyChanged: OnBindablePropertyChanged); + + /// The backing store for the bindable property. + public static readonly BindableProperty SizeProperty = BindableProperty.Create(nameof(Size), typeof(double), typeof(RatingView), defaultValue: RatingViewDefaults.Size, propertyChanged: OnBindablePropertyChanged); + + /// The backing store for the bindable property. + public static readonly BindableProperty SpacingProperty = BindableProperty.Create(nameof(Spacing), typeof(double), typeof(RatingView), defaultValue: RatingViewDefaults.Spacing, propertyChanged: OnBindablePropertyChanged); + + /// The backing store for the bindable property. + public readonly BindableProperty ShapeProperty = BindableProperty.Create(nameof(Shape), typeof(RatingViewShape), typeof(RatingView), defaultValue: RatingViewShape.Star, propertyChanged: OnShapePropertyChanged); + + static readonly WeakEventManager weakEventManager = new(); + Microsoft.Maui.Controls.Shapes.Path[] shapes; + + ///The default constructor of the control. + public RatingView() + { + ControlTemplate = new ControlTemplate(typeof(Grid)); + shapes = new Microsoft.Maui.Controls.Shapes.Path[MaximumRating]; + HorizontalOptions = new LayoutOptions(LayoutAlignment.Center, true); + AddChildrenToControl(); + } + + /// + /// Occurs when and the rating is changed. + /// + public static event EventHandler RatingChanged + { + add => weakEventManager.AddEventHandler(value); + remove => weakEventManager.RemoveEventHandler(value); + } + + /// Gets or sets a value of the control border color property. + public Color BorderColor + { + get => (Color)GetValue(BorderColorProperty); + set => SetValue(BorderColorProperty, value); + } + + ///Defines the thickness of the shape's stroke(border). + public double BorderThickness + { + get => (double)GetValue(BorderThicknessProperty); + set => SetValue(BorderThicknessProperty, value); + } + + ///The control to be displayed + public Grid? Control { get; private set; } + + /// Gets or sets a value of the control current rating property. + public double CurrentRating + { + get => (double)GetValue(CurrentRatingProperty); + set => SetValue(CurrentRatingProperty, value); + } + + ///Defines the shape to be drawn. + public string CustomShape + { + get => (string)GetValue(CustomShapeProperty); + set => SetValue(CustomShapeProperty, value); + } + + /// Gets or sets a value of the control empty background property. + public Color EmptyBackgroundColor + { + get => (Color)GetValue(EmptyBackgroundColorProperty); + set => SetValue(EmptyBackgroundColorProperty, value); + } + + /// Gets or sets a value of the control filled background property. + public Color FilledBackgroundColor + { + get => (Color)GetValue(FilledBackgroundColorProperty); + set => SetValue(FilledBackgroundColorProperty, value); + } + + ///Defines if the drawn shapes can be clickable to rate. + public new bool IsEnabled + { + get => (bool)GetValue(IsEnabledProperty); + set => SetValue(IsEnabledProperty, value); + } + + /// Gets or sets a value of the control maximum rating property. + public byte MaximumRating + { + get => (byte)GetValue(MaximumRatingProperty); + set => SetValue(MaximumRatingProperty, value); + } + + ///Defines the shape to be drawn. + public RatingViewShape Shape + { + get => (RatingViewShape)GetValue(ShapeProperty); + set => SetValue(ShapeProperty, value); + } + + /// Gets or sets a value of the control size for the drawn shape property. + public double Size + { + get => (double)GetValue(SizeProperty); + set => SetValue(SizeProperty, value); + } + + ///Defines the space between the drawn shapes. + public double Spacing + { + get => (double)GetValue(SpacingProperty); + set => SetValue(SpacingProperty, value); + } + + ///Method called every time the control's Binding Context is changed. + protected override void OnBindingContextChanged() + { + base.OnBindingContextChanged(); + if (Control is not null) + { + Control.BindingContext = BindingContext; + } + } + + ///Called every time 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); + } + + static bool maximumRatingValidator(BindableObject bindable, object value) + { + return (byte)value <= RatingViewDefaults.MaximumRatings; + } + + static void OnBindablePropertyChanged(BindableObject bindable, object oldValue, object newValue) + { + ((RatingView)bindable).ReDraw(); + } + + static void OnIsEnabledChanged(BindableObject bindable, object oldValue, object newValue) + { + RatingView ratingView = (RatingView)bindable; + ratingView.HandleIsEnabledChanged(); + ratingView.ReDraw(); + } + + static void OnShapePropertyChanged(BindableObject bindable, object oldValue, object newValue) + { + RatingView ratingView = (RatingView)bindable; + ratingView.ReDraw(); + } + + /// Add the required children to the control. + void AddChildrenToControl() + { + string shape = Shape switch + { + RatingViewShape.Heart => Core.Primitives.RatingViewShape.Heart.PathData, + RatingViewShape.Circle => Core.Primitives.RatingViewShape.Circle.PathData, + RatingViewShape.Like => Core.Primitives.RatingViewShape.Like.PathData, + RatingViewShape.Dislike => Core.Primitives.RatingViewShape.Dislike.PathData, + RatingViewShape.Custom => CustomShape ?? Core.Primitives.RatingViewShape.Star.PathData, + _ => Core.Primitives.RatingViewShape.Star.PathData, + }; + + 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), + Fill = i <= CurrentRating ? (Brush)FilledBackgroundColor : (Brush)EmptyBackgroundColor, + Stroke = BorderColor, + StrokeLineJoin = PenLineJoin.Round, + StrokeLineCap = PenLineCap.Round, + StrokeThickness = BorderThickness, + Aspect = Stretch.Uniform, + HeightRequest = Size, + WidthRequest = Size + }; + if (IsEnabled) + { + TapGestureRecognizer tapGestureRecognizer = new(); + tapGestureRecognizer.Tapped += OnShapeTapped; + image.GestureRecognizers.Add(tapGestureRecognizer); + } + + Control?.Children.Add(image); + Control?.SetColumn(image, i); + + shapes[i] = image; + } + + UpdateRatingDraw(); + } + + // Ensure VisualElement.IsEnabled always matches RatingView.IsEnabled + void HandleIsEnabledChanged() + { + base.IsEnabled = IsEnabled; + } + + /// Initialise the control. + void OnControlInitialized() + { + shapes = new Microsoft.Maui.Controls.Shapes.Path[MaximumRating]; + HorizontalOptions = new LayoutOptions(LayoutAlignment.Center, true); + if (Control is not null) + { + Control.ColumnSpacing = Spacing; + } + + AddChildrenToControl(); + } + + /// Shape tapped event. + /// Element sender of event. + /// Event arguments. + void OnShapeTapped(object? sender, TappedEventArgs? e) + { + if (sender is not Microsoft.Maui.Controls.Shapes.Path tappedShape) + { + return; + } + + if (Control is null) + { + return; + } + + int columnIndex = Control.GetColumn(tappedShape); + double originalRating = CurrentRating; + if (MaximumRating > 1) + { + CurrentRating = columnIndex + 1; + } + + if (!originalRating.Equals(CurrentRating)) + { + weakEventManager.HandleEvent(this, CurrentRating, nameof(RatingChanged)); + } + } + + /// Re-draw the control. + void ReDraw() + { + Control?.Children.Clear(); + Control?.ColumnDefinitions.Clear(); + shapes = new Microsoft.Maui.Controls.Shapes.Path[MaximumRating]; + AddChildrenToControl(); + UpdateRatingDraw(); + } + + /// Update the drawing of the control. + void UpdateRatingDraw() + { + for (int i = 0; i < MaximumRating; i++) + { + Microsoft.Maui.Controls.Shapes.Path image = shapes[i]; + //image.HeightRequest = Size; + //image.WidthRequest = Size; + //image.StrokeLineJoin = PenLineJoin.Round; + //image.StrokeThickness = BorderThickness; + if (CurrentRating >= i + 1) + { + image.Stroke = FilledBackgroundColor; + continue; + } + + if (CurrentRating % 1 is 0) + { + image.Fill = EmptyBackgroundColor; + image.Stroke = BorderColor; + //image.StrokeLineCap = PenLineCap.Round; + continue; + } + + double fraction = CurrentRating - Math.Floor(CurrentRating); + Microsoft.Maui.Controls.Shapes.Path element = shapes[(int)(CurrentRating - fraction)]; + GradientStopCollection colors = + [ + new(FilledBackgroundColor, (float)fraction), + new(EmptyBackgroundColor, (float)fraction) + ]; + + element.Fill = new LinearGradientBrush(colors, new Point(0, 0), new Point(1, 0)); + //element.StrokeThickness = BorderThickness; + //element.StrokeLineJoin = PenLineJoin.Round; + element.Stroke = BorderColor; + } + } +} + +/// Rating view shape enumerator. +public enum RatingViewShape +{ + /// A star rating shape. + Star = 0, + + /// A heart rating shape. + Heart = 1, + + /// A circle rating shape. + Circle = 2, + + /// A like/thumbs up rating shape. + Like = 3, + + /// A dislike/thumbs down rating shape. + Dislike = 4, + + /// A custom rating shape. + Custom = 99 +} \ No newline at end of file From b42e811b3d1b8e72cfa4ecae76fa1dcd200537bc Mon Sep 17 00:00:00 2001 From: "github@internetwideworld.com" Date: Fri, 23 Aug 2024 18:02:11 +0100 Subject: [PATCH 02/92] Added better properties. Added new property changed events. Handle maximum rating change and events. --- .../AppShell.xaml.cs | 3 +- .../MauiProgram.cs | 1 + .../RatingView/RatingViewCsharpPage.xaml.cs | 1 + .../Views/RatingView/RatingViewXamlPage.xaml | 141 +++++++------ .../RatingView/RatingViewXamlPage.xaml.cs | 29 ++- .../RatingView/RatingViewCsharpViewModel.cs | 2 +- .../RatingView/RatingViewXamlViewModel.cs | 11 +- .../Defaults/RatingViewDefaults.shared.cs | 4 +- .../Views/RatingView/RatingViewTests.cs | 9 +- .../Views/RatingView.shared.cs | 185 +++++++++++++----- 10 files changed, 274 insertions(+), 112 deletions(-) diff --git a/samples/CommunityToolkit.Maui.Sample/AppShell.xaml.cs b/samples/CommunityToolkit.Maui.Sample/AppShell.xaml.cs index 0c1e7a26aa..c58c71458c 100644 --- a/samples/CommunityToolkit.Maui.Sample/AppShell.xaml.cs +++ b/samples/CommunityToolkit.Maui.Sample/AppShell.xaml.cs @@ -135,7 +135,8 @@ public partial class AppShell : Shell CreateViewModelMapping(), CreateViewModelMapping(), CreateViewModelMapping(), - CreateViewModelMapping(), + CreateViewModelMapping(), + CreateViewModelMapping(), // Add PlatformSpecific View Models CreateViewModelMapping(), diff --git a/samples/CommunityToolkit.Maui.Sample/MauiProgram.cs b/samples/CommunityToolkit.Maui.Sample/MauiProgram.cs index 78c30439dc..8e7a90d8de 100644 --- a/samples/CommunityToolkit.Maui.Sample/MauiProgram.cs +++ b/samples/CommunityToolkit.Maui.Sample/MauiProgram.cs @@ -33,6 +33,7 @@ using Microsoft.Maui.Platform; using Polly; + #if WINDOWS10_0_17763_0_OR_GREATER using Microsoft.UI; using Microsoft.UI.Composition.SystemBackdrops; diff --git a/samples/CommunityToolkit.Maui.Sample/Pages/Views/RatingView/RatingViewCsharpPage.xaml.cs b/samples/CommunityToolkit.Maui.Sample/Pages/Views/RatingView/RatingViewCsharpPage.xaml.cs index d97dced804..a05f689e08 100644 --- a/samples/CommunityToolkit.Maui.Sample/Pages/Views/RatingView/RatingViewCsharpPage.xaml.cs +++ b/samples/CommunityToolkit.Maui.Sample/Pages/Views/RatingView/RatingViewCsharpPage.xaml.cs @@ -1,3 +1,4 @@ +// Ignore Spelling: csharp using CommunityToolkit.Maui.Sample.ViewModels.Views; namespace CommunityToolkit.Maui.Sample.Pages.Views; diff --git a/samples/CommunityToolkit.Maui.Sample/Pages/Views/RatingView/RatingViewXamlPage.xaml b/samples/CommunityToolkit.Maui.Sample/Pages/Views/RatingView/RatingViewXamlPage.xaml index e3d3b2afd7..f696e69e37 100644 --- a/samples/CommunityToolkit.Maui.Sample/Pages/Views/RatingView/RatingViewXamlPage.xaml +++ b/samples/CommunityToolkit.Maui.Sample/Pages/Views/RatingView/RatingViewXamlPage.xaml @@ -6,53 +6,83 @@ xmlns:pages="clr-namespace:CommunityToolkit.Maui.Sample.Pages" xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit" xmlns:vm="clr-namespace:CommunityToolkit.Maui.Sample.ViewModels.Views" - Title="Rating View XAML" + Title="RatingView XAML" x:DataType="vm:RatingViewXamlViewModel" x:TypeArguments="vm:RatingViewXamlViewModel"> - - \ No newline at end of file diff --git a/samples/CommunityToolkit.Maui.Sample/Pages/Views/RatingView/RatingViewXamlPage.xaml.cs b/samples/CommunityToolkit.Maui.Sample/Pages/Views/RatingView/RatingViewXamlPage.xaml.cs index 8252fdd7f0..dcc0a3c667 100644 --- a/samples/CommunityToolkit.Maui.Sample/Pages/Views/RatingView/RatingViewXamlPage.xaml.cs +++ b/samples/CommunityToolkit.Maui.Sample/Pages/Views/RatingView/RatingViewXamlPage.xaml.cs @@ -6,30 +6,76 @@ namespace CommunityToolkit.Maui.Sample.Pages.Views; public partial class RatingViewXamlPage : BasePage { + RatingViewXamlViewModel vm; readonly IReadOnlyDictionary colors = typeof(Colors).GetFields(BindingFlags.Static | BindingFlags.Public).ToDictionary(c => c.Name, c => (Color)(c.GetValue(null) ?? throw new InvalidOperationException())); public RatingViewXamlPage(RatingViewXamlViewModel viewModel) : base(viewModel) { InitializeComponent(); + vm = viewModel; } protected override void OnAppearing() { - ColorPickerEmptyBackground.ItemsSource = colors.Keys.ToList(); + List colorKeys = colors.Keys.ToList(); + ColorPickerEmptyBackground.ItemsSource = colorKeys; + ColorPickerFilledBackground.ItemsSource = colorKeys; + ColorPickerRatingShapeBorderColor.ItemsSource = colorKeys; } - void ColorPickerEmptyBackground_SelectedIndexChanged(object sender, EventArgs e) + void ColorPicker_SelectedIndexChanged(object sender, EventArgs e) { Color color = colors.ElementAtOrDefault(ColorPickerEmptyBackground.SelectedIndex).Value ?? Colors.Transparent; - ColorPickerEmptyBackgroundRatingView.EmptyBackgroundColor = color; + switch (((Picker)sender).StyleId) + { + case "ColorPickerEmptyBackground": + ColorPickerEmptyBackgroundTarget.EmptyBackgroundColor = color; + break; + case "ColorPickerFilledBackground": + ColorPickerFilledBackgroundTarget.FilledBackgroundColor = color; + break; + case "ColorPickerRatingShapeBorderColor": + ColorPickerRatingShapeBorderColorTarget.RatingShapeOutlineColor = color; + break; + default: + break; + } } - void StepperValueMaximumRatings_RatingChanged(object sender, EventArgs e) + void StepperMaximumRating_RatingChanged(object sender, EventArgs e) { - // This is the weak event raised when the rating is changed, so that the developer can use to then perform further actions (such as save to DB). + // This is the weak event raised when the rating is changed. The developer can then perform further actions (such as save to DB). if (sender is RatingView ratingView) { - StepperValueMaximumRatingsCurrentRanking.Text = ratingView.CurrentRating.ToString(); + _ = ratingView.Rating; } } + + void RatingViewShapePaddingLeft_ValueChanged(object sender, ValueChangedEventArgs e) + { + var currentThickness = vm.RatingViewShapePadding; + currentThickness.Left = e.NewValue; + vm.RatingViewShapePadding = currentThickness; + } + + void RatingViewShapePaddingTop_ValueChanged(object sender, ValueChangedEventArgs e) + { + var currentThickness = vm.RatingViewShapePadding; + currentThickness.Top = e.NewValue; + vm.RatingViewShapePadding = currentThickness; + } + + void RatingViewShapePaddingRight_ValueChanged(object sender, ValueChangedEventArgs e) + { + var currentThickness = vm.RatingViewShapePadding; + currentThickness.Right = e.NewValue; + vm.RatingViewShapePadding = currentThickness; + } + + void RatingViewShapePaddingBottom_ValueChanged(object sender, ValueChangedEventArgs e) + { + var currentThickness = vm.RatingViewShapePadding; + currentThickness.Bottom = e.NewValue; + vm.RatingViewShapePadding = currentThickness; + } } \ No newline at end of file diff --git a/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/RatingView/RatingViewInspirationsViewModel.cs b/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/RatingView/RatingViewInspirationsViewModel.cs new file mode 100644 index 0000000000..991668e5e4 --- /dev/null +++ b/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/RatingView/RatingViewInspirationsViewModel.cs @@ -0,0 +1,13 @@ +// Ignore Spelling: csharp +namespace CommunityToolkit.Maui.Sample.ViewModels.Views; + +using CommunityToolkit.Mvvm.ComponentModel; + +public partial class RatingViewInspirationsViewModel : BaseViewModel +{ + [ObservableProperty] + double stepperValueMaximumRatings = 1; + + [ObservableProperty] + Thickness ratingViewShapePadding = new(0); +} \ No newline at end of file diff --git a/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/RatingView/RatingViewXamlViewModel.cs b/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/RatingView/RatingViewXamlViewModel.cs index c5d4cdc0a1..e4bccb55ab 100644 --- a/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/RatingView/RatingViewXamlViewModel.cs +++ b/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/RatingView/RatingViewXamlViewModel.cs @@ -7,4 +7,7 @@ public partial class RatingViewXamlViewModel : BaseViewModel { [ObservableProperty] double stepperValueMaximumRatings = 1; + + [ObservableProperty] + Thickness ratingViewShapePadding = new(0); } \ No newline at end of file diff --git a/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/ViewsGalleryViewModel.cs b/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/ViewsGalleryViewModel.cs index e6787e7ad5..ce8e6e0542 100644 --- a/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/ViewsGalleryViewModel.cs +++ b/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/ViewsGalleryViewModel.cs @@ -29,6 +29,7 @@ public sealed class ViewsGalleryViewModel() : BaseGalleryViewModel( SectionModel.Create("Anchor Popup", Colors.Red, "Popups can be anchored to other view's on the screen"), SectionModel.Create("Popup Layout Page", Colors.Red, "Popup.Content demonstrated using different layouts"), SectionModel.Create("Popup Sizing Issues Page", Colors.Red, "A page demonstrating how Popups can be styled in a .NET MAUI application."), + SectionModel.Create("RatingView Inspirations Page", Colors.Red, "A page with inspirations for possible uses of the RatingView control."), SectionModel.Create("RatingView XAML Page", Colors.Red, "A page demonstrating the RatingView control and possible uses using XAML"), SectionModel.Create("RatingView C# Page", Colors.Red, "A page demonstrating the RatingView control and possible uses using C#"), SectionModel.Create("Show Popup in OnAppearing", Colors.Red, "Proves that we now support showing a popup before the platform is even ready."), diff --git a/src/CommunityToolkit.Maui.Core/CommunityToolkit.Maui.Core.csproj b/src/CommunityToolkit.Maui.Core/CommunityToolkit.Maui.Core.csproj index a15763c502..00dd5d40ca 100644 --- a/src/CommunityToolkit.Maui.Core/CommunityToolkit.Maui.Core.csproj +++ b/src/CommunityToolkit.Maui.Core/CommunityToolkit.Maui.Core.csproj @@ -49,8 +49,8 @@ - - + + diff --git a/src/CommunityToolkit.Maui.Core/Interfaces/RatingView/IRatingView.shared.cs b/src/CommunityToolkit.Maui.Core/Interfaces/RatingView/IRatingView.shared.cs new file mode 100644 index 0000000000..ffc8acf757 --- /dev/null +++ b/src/CommunityToolkit.Maui.Core/Interfaces/RatingView/IRatingView.shared.cs @@ -0,0 +1,6 @@ +namespace CommunityToolkit.Maui.Core; + +/// provides functionality to device a rating view. +public interface IRatingView : IContentView, IRatingViewShape +{ +} diff --git a/src/CommunityToolkit.Maui.Core/Interfaces/RatingView/IRatingViewShape.shared.cs b/src/CommunityToolkit.Maui.Core/Interfaces/RatingView/IRatingViewShape.shared.cs new file mode 100644 index 0000000000..e1bdc60a3d --- /dev/null +++ b/src/CommunityToolkit.Maui.Core/Interfaces/RatingView/IRatingViewShape.shared.cs @@ -0,0 +1,62 @@ +using System.ComponentModel; + +namespace CommunityToolkit.Maui.Core; + +/// RatingView interface. +[EditorBrowsable(EditorBrowsableState.Never)] +public interface IRatingViewShape +{ + /// Gets a value indicating the custom rating view shape path. + string? CustomShape { get; } + + /// Gets a value indicating the Rating View shape. + RatingViewShape Shape { get; } + + /// Gets a value indicating the Rating View shape padding. + Thickness ShapePadding { get; } + + /// Action when changes. + /// Old shape path. + /// New shape path. + void OnCustomShapePropertyChanged(string? oldValue, string? newValue); + + /// Action when changes. + /// Old padding thickness. + /// New padding thickness + void OnShapePaddingPropertyChanged(Thickness oldValue, Thickness newValue); + + /// Action when changes. + /// Old shape. + /// New shape. + void OnShapePropertyChanged(RatingViewShape oldValue, RatingViewShape newValue); + + /// Retrieves the default shape value. + /// The shape . + RatingViewShape ShapeDefaultValueCreator(); + + /// Retrieves the default shape padding value. + /// The shape padding . + Thickness ShapePaddingDefaultValueCreator(); +} + +/// Rating view shape enumerator. +public enum RatingViewShape +{ + /// A star rating shape. + Star = 0, + + /// A heart rating shape. + Heart = 1, + + /// A circle rating shape. + Circle = 2, + + /// A like/thumbs up rating shape. + Like = 3, + + /// A dislike/thumbs down rating shape. + Dislike = 4, + + /// A custom rating shape. + Custom = 99 +} \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Core/Primitives/Defaults/RatingViewDefaults.shared.cs b/src/CommunityToolkit.Maui.Core/Primitives/Defaults/RatingViewDefaults.shared.cs index edeb67950b..473d122e16 100644 --- a/src/CommunityToolkit.Maui.Core/Primitives/Defaults/RatingViewDefaults.shared.cs +++ b/src/CommunityToolkit.Maui.Core/Primitives/Defaults/RatingViewDefaults.shared.cs @@ -1,16 +1,17 @@ // Ignore Spelling: color -namespace CommunityToolkit.Maui.Core; using System.ComponentModel; using CommunityToolkit.Maui.Core.Primitives; +namespace CommunityToolkit.Maui.Core; + /// Default Values for RatingView [EditorBrowsable(EditorBrowsableState.Never)] public static class RatingViewDefaults { - /// Default rating current rating. + /// Default rating value. [EditorBrowsable(EditorBrowsableState.Never)] - public const double CurrentRating = 0.0; + public const double DefaultRating = 0.0; /// Default enabled rating view. [EditorBrowsable(EditorBrowsableState.Never)] @@ -24,6 +25,10 @@ public static class RatingViewDefaults [EditorBrowsable(EditorBrowsableState.Never)] public const byte MaximumRatings = 25; + /// Default border thickness for a rating. + [EditorBrowsable(EditorBrowsableState.Never)] + public const double RatingShapeOutlineThickness = 1.0; + /// Default size of a rating. [EditorBrowsable(EditorBrowsableState.Never)] public const double Size = 20.0; @@ -32,9 +37,9 @@ public static class RatingViewDefaults [EditorBrowsable(EditorBrowsableState.Never)] public const double Spacing = 10.0; - /// Default border thickness for a rating. + /// Default shape padding. [EditorBrowsable(EditorBrowsableState.Never)] - public const double RatingShapeOutlineThickness = 1.0; + public static Thickness ShapePadding { get; } = new(0); /// Default background color for an empty rating. [EditorBrowsable(EditorBrowsableState.Never)] diff --git a/src/CommunityToolkit.Maui.UnitTests/Views/RatingView/RatingViewTests.cs b/src/CommunityToolkit.Maui.UnitTests/Views/RatingView/RatingViewTests.cs index 8696609a82..baa5262444 100644 --- a/src/CommunityToolkit.Maui.UnitTests/Views/RatingView/RatingViewTests.cs +++ b/src/CommunityToolkit.Maui.UnitTests/Views/RatingView/RatingViewTests.cs @@ -1,9 +1,8 @@ using CommunityToolkit.Maui.Core; using FluentAssertions; using Xunit; -using Path = Microsoft.Maui.Controls.Shapes.Path; -namespace CommunityToolkit.Maui.UnitTests.Views.RatingView; +namespace CommunityToolkit.Maui.UnitTests.Views; public class RatingViewTests : BaseHandlerTest { @@ -16,7 +15,7 @@ public void DefaultInitialization_ShouldHaveCorrectDefaultValues() // Assert Assert.Equal(RatingViewDefaults.RatingShapeOutlineColor, ratingView.RatingShapeOutlineColor); Assert.Equal(RatingViewDefaults.RatingShapeOutlineThickness, ratingView.RatingShapeOutlineThickness); - Assert.Equal(RatingViewDefaults.CurrentRating, ratingView.CurrentRating); + Assert.Equal(RatingViewDefaults.DefaultRating, ratingView.Rating); Assert.Equal(RatingViewDefaults.EmptyBackgroundColor, ratingView.EmptyBackgroundColor); Assert.Equal(RatingViewDefaults.FilledBackgroundColor, ratingView.FilledBackgroundColor); Assert.Equal(RatingViewDefaults.IsEnabled, ratingView.IsEnabled); @@ -30,11 +29,11 @@ public void OnControlInitialized_ShouldCreateCorrectNumberOfShapes() { const int maximumRating = 3; Maui.Views.RatingView ratingView = new(); - ratingView.Control?.ColumnDefinitions.Count.Should().Be(RatingViewDefaults.MaximumRating); + ratingView.Control?.GetVisualTreeDescendants().Count.Should().Be(RatingViewDefaults.MaximumRating); ratingView.Control?.Children.Count.Should().Be(RatingViewDefaults.MaximumRating); ratingView.MaximumRating = maximumRating; - ratingView.Control?.ColumnDefinitions.Count.Should().Be(maximumRating); + ratingView.Control?.GetVisualTreeDescendants().Count.Should().Be(maximumRating); ratingView.Control?.Children.Count.Should().Be(maximumRating); } @@ -43,19 +42,19 @@ public void PropertyChangedEvent_ShouldBeRaised_WhenCurrentRatingChanges() { const double currentRating = 3.5; Maui.Views.RatingView ratingView = new(); - ratingView.CurrentRating.Should().Be(RatingViewDefaults.CurrentRating); + ratingView.Rating.Should().Be(RatingViewDefaults.DefaultRating); bool signaled = false; ratingView.PropertyChanged += (sender, e) => { - if (e.PropertyName == "CurrentRating") + if (e.PropertyName == nameof(ratingView.Rating)) { signaled = true; } }; - ratingView.CurrentRating = currentRating; - ratingView.CurrentRating.Should().Be(currentRating); + ratingView.Rating = currentRating; + ratingView.Rating.Should().Be(currentRating); signaled.Should().BeTrue(); } @@ -68,7 +67,7 @@ public void Draw_ShouldCreateCorrectNumberOfShapes() MaximumRating = maximumRating }; - ratingView.Control?.ColumnDefinitions.Count.Should().Be(maximumRating); + ratingView.Control?.GetVisualTreeDescendants().Count.Should().Be(maximumRating); ratingView.Control?.Children.Count.Should().Be(maximumRating); } @@ -77,9 +76,9 @@ public void ShapeProperty_ShouldSetShapeCorrectly() { Maui.Views.RatingView ratingView = new() { - Shape = Maui.Views.RatingViewShape.Heart + Shape = RatingViewShape.Heart, }; - ratingView.Shape.Should().Be(Maui.Views.RatingViewShape.Heart); + ratingView.Shape.Should().Be(RatingViewShape.Heart); } } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui/CommunityToolkit.Maui.csproj b/src/CommunityToolkit.Maui/CommunityToolkit.Maui.csproj index 0a1361fe04..0c658cd4c9 100644 --- a/src/CommunityToolkit.Maui/CommunityToolkit.Maui.csproj +++ b/src/CommunityToolkit.Maui/CommunityToolkit.Maui.csproj @@ -47,7 +47,7 @@ - + @@ -63,7 +63,7 @@ - + \ No newline at end of file diff --git a/src/CommunityToolkit.Maui/Views/RatingView.shared.cs b/src/CommunityToolkit.Maui/Views/RatingView/RatingView.shared.cs similarity index 61% rename from src/CommunityToolkit.Maui/Views/RatingView.shared.cs rename to src/CommunityToolkit.Maui/Views/RatingView/RatingView.shared.cs index 2dad1fb81b..b91acf1926 100644 --- a/src/CommunityToolkit.Maui/Views/RatingView.shared.cs +++ b/src/CommunityToolkit.Maui/Views/RatingView/RatingView.shared.cs @@ -6,22 +6,10 @@ namespace CommunityToolkit.Maui.Views; /// Rating view control. -public class RatingView : TemplatedView +public class RatingView : TemplatedView, IContentView, IRatingViewShape { - /// The backing store for the bindable property. - public static readonly BindableProperty RatingBorderProperty = BindableProperty.Create(nameof(RatingBorder), typeof(Border), typeof(RatingView), propertyChanged: OnRatingBorderChanged, defaultValueCreator: (_) => new Border()); - - /// The backing store for the bindable property. - public static readonly BindableProperty RatingShapeOutlineColorProperty = BindableProperty.Create(nameof(RatingShapeOutlineColor), typeof(Color), typeof(RatingView), defaultValue: RatingViewDefaults.RatingShapeOutlineColor, propertyChanged: OnShapeOutlineColorChanged); - - /// The backing store for the bindable property. - public static readonly BindableProperty RatingShapeOutlineThicknessProperty = BindableProperty.Create(nameof(RatingShapeOutlineThickness), typeof(double), typeof(RatingView), defaultValue: RatingViewDefaults.RatingShapeOutlineThickness, propertyChanged: OnShapeOutlineThicknessChanged); - - /// The backing store for the bindable property. - public static readonly BindableProperty CurrentRatingProperty = BindableProperty.Create(nameof(CurrentRating), typeof(double), typeof(RatingView), defaultValue: RatingViewDefaults.CurrentRating, propertyChanged: OnUpdateRatingDraw); - - /// The backing store for the bindable property. - public readonly BindableProperty CustomShapeProperty = BindableProperty.Create(nameof(CustomShape), typeof(string), typeof(RatingView), defaultValue: null); + /// Bindable property for bindable property. + public static readonly BindableProperty CustomShapeProperty = RatingViewItemElement.CustomShapeProperty; /// The backing store for the bindable property. public static readonly BindableProperty EmptyBackgroundColorProperty = BindableProperty.Create(nameof(EmptyBackgroundColor), typeof(Color), typeof(RatingView), defaultValue: RatingViewDefaults.EmptyBackgroundColor, propertyChanged: OnUpdateRatingDraw); @@ -30,11 +18,26 @@ public class RatingView : TemplatedView public static readonly BindableProperty FilledBackgroundColorProperty = BindableProperty.Create(nameof(FilledBackgroundColor), typeof(Color), typeof(RatingView), defaultValue: RatingViewDefaults.FilledBackgroundColor, propertyChanged: OnUpdateRatingDraw); /// The backing store for the bindable property. - public static new readonly BindableProperty IsEnabledProperty = BindableProperty.Create(nameof(IsEnabled), typeof(bool), typeof(RatingView), defaultValue: RatingViewDefaults.IsEnabled, propertyChanged: OnIsEnabledChanged); + public new static readonly BindableProperty IsEnabledProperty = BindableProperty.Create(nameof(IsEnabled), typeof(bool), typeof(RatingView), defaultValue: RatingViewDefaults.IsEnabled, propertyChanged: OnIsEnabledChanged); /// The backing store for the bindable property. public static readonly BindableProperty MaximumRatingProperty = BindableProperty.Create(nameof(MaximumRating), typeof(byte), typeof(RatingView), defaultValue: RatingViewDefaults.MaximumRating, validateValue: maximumRatingValidator, propertyChanged: OnMaximumRatingChange); + /// The backing store for the bindable property. + public static readonly BindableProperty RatingFillProperty = BindableProperty.Create(nameof(RatingFill), typeof(RatingFillElement), typeof(RatingView), defaultValue: RatingFillElement.Shape, propertyChanged: OnUpdateRatingDraw); + + /// The backing store for the bindable property. + public static readonly BindableProperty RatingProperty = BindableProperty.Create(nameof(Rating), typeof(double), typeof(RatingView), defaultValue: RatingViewDefaults.DefaultRating, propertyChanged: OnUpdateRatingDraw); + + /// The backing store for the bindable property. + public static readonly BindableProperty RatingShapeOutlineColorProperty = BindableProperty.Create(nameof(RatingShapeOutlineColor), typeof(Color), typeof(RatingView), defaultValue: RatingViewDefaults.RatingShapeOutlineColor, propertyChanged: OnShapeOutlineColorChanged); + + /// The backing store for the bindable property. + public static readonly BindableProperty RatingShapeOutlineThicknessProperty = BindableProperty.Create(nameof(RatingShapeOutlineThickness), typeof(double), typeof(RatingView), defaultValue: RatingViewDefaults.RatingShapeOutlineThickness, propertyChanged: OnShapeOutlineThicknessChanged); + + /// Bindable property for bindable property. + public static readonly BindableProperty ShapePaddingProperty = RatingViewItemElement.ShapePaddingProperty; + /// The backing store for the bindable property. public static readonly BindableProperty SizeProperty = BindableProperty.Create(nameof(Size), typeof(double), typeof(RatingView), defaultValue: RatingViewDefaults.Size, propertyChanged: OnSizeChanged); @@ -42,7 +45,7 @@ public class RatingView : TemplatedView public static readonly BindableProperty SpacingProperty = BindableProperty.Create(nameof(Spacing), typeof(double), typeof(RatingView), defaultValue: RatingViewDefaults.Spacing, propertyChanged: OnSpacingChanged); /// The backing store for the bindable property. - public readonly BindableProperty ShapeProperty = BindableProperty.Create(nameof(Shape), typeof(RatingViewShape), typeof(RatingView), defaultValue: RatingViewShape.Star, propertyChanged: OnShapePropertyChanged); + public readonly BindableProperty ShapeProperty = RatingViewItemElement.ShapeProperty; static readonly WeakEventManager weakEventManager = new(); Microsoft.Maui.Controls.Shapes.Path[] shapes; @@ -50,7 +53,7 @@ public class RatingView : TemplatedView ///The default constructor of the control. public RatingView() { - ControlTemplate = new ControlTemplate(typeof(Grid)); + ControlTemplate = new ControlTemplate(typeof(HorizontalStackLayout)); shapes = new Microsoft.Maui.Controls.Shapes.Path[MaximumRating]; HorizontalOptions = new LayoutOptions(LayoutAlignment.Center, true); AddChildrenToControl(); @@ -63,36 +66,8 @@ public static event EventHandler RatingChanged remove => weakEventManager.RemoveEventHandler(value); } - /// Gets or sets the rating border property. - public Border RatingBorder - { - get => (Border)GetValue(RatingBorderProperty); - set => SetValue(RatingBorderProperty, value); - } - - /// Gets or sets a value of the rating outline border color property. - public Color RatingShapeOutlineColor - { - get => (Color)GetValue(RatingShapeOutlineColorProperty); - set => SetValue(RatingShapeOutlineColorProperty, value); - } - - ///Gets or sets a value of the rating outline border thickness property. - public double RatingShapeOutlineThickness - { - get => (double)GetValue(RatingShapeOutlineThicknessProperty); - set => SetValue(RatingShapeOutlineThicknessProperty, value); - } - ///The control to be displayed - public Grid? Control { get; private set; } - - /// Gets or sets a value of the control current rating property. - public double CurrentRating - { - get => (double)GetValue(CurrentRatingProperty); - set => SetValue(CurrentRatingProperty, value); - } + public HorizontalStackLayout? Control { get; private set; } ///Defines the shape to be drawn. public string CustomShape @@ -126,7 +101,43 @@ public Color FilledBackgroundColor public byte MaximumRating { get => (byte)GetValue(MaximumRatingProperty); - set => SetValue(MaximumRatingProperty, value); + set + { + if (value is < 1 or > RatingViewDefaults.MaximumRatings) + { + throw new ArgumentOutOfRangeException(nameof(value), value, $"{nameof(MaximumRating)} must be between 1 and {RatingViewDefaults.MaximumRatings}."); + } + + SetValue(MaximumRatingProperty, value); + } + } + + /// Gets or sets a value of the control rating property. + public double Rating + { + get => (double)GetValue(RatingProperty); + set => SetValue(RatingProperty, value); + } + + /// Defines which element of the rating to fill. + public RatingFillElement RatingFill + { + get => (RatingFillElement)GetValue(RatingFillProperty); + set => SetValue(RatingFillProperty, value); + } + + /// Gets or sets a value of the rating outline border color property. + public Color RatingShapeOutlineColor + { + get => (Color)GetValue(RatingShapeOutlineColorProperty); + set => SetValue(RatingShapeOutlineColorProperty, value); + } + + ///Gets or sets a value of the rating outline border thickness property. + public double RatingShapeOutlineThickness + { + get => (double)GetValue(RatingShapeOutlineThicknessProperty); + set => SetValue(RatingShapeOutlineThicknessProperty, value); } ///Defines the shape to be drawn. @@ -136,6 +147,13 @@ public RatingViewShape Shape set => SetValue(ShapeProperty, value); } + /// Defines the shape padding thickness. + public Thickness ShapePadding + { + get => (Thickness)GetValue(ShapePaddingProperty); + set => SetValue(ShapePaddingProperty, value); + } + /// Gets or sets a value of the control size for the drawn shape property. public double Size { @@ -150,6 +168,57 @@ public double Spacing set => SetValue(SpacingProperty, value); } + /// Change the rating shape to a custom shape. + /// Only change the rating shape if . + /// Old rating custom shape path data. + /// Old rating custom shape path data. + void IRatingViewShape.OnCustomShapePropertyChanged(string? oldValue, string? newValue) + { + if (Control is null) + { + return; + } + + if ((this).Shape is RatingViewShape.Custom) + { + ClearAndReDraw(); + } + } + + void IRatingViewShape.OnShapePaddingPropertyChanged(Thickness oldValue, Thickness newValue) + { + if (Control is null) + { + return; + } + + for (int element = 0; element < Control.Count; element++) + { + Microsoft.Maui.Controls.Shapes.Path rating = (Microsoft.Maui.Controls.Shapes.Path)((Border)Control.Children[element]).Content!.GetVisualTreeDescendants()[0]; + rating.Margin = newValue; + } + } + + void IRatingViewShape.OnShapePropertyChanged(RatingViewShape oldValue, RatingViewShape newValue) + { + if (Control is null) + { + return; + } + + ClearAndReDraw(); + } + + RatingViewShape IRatingViewShape.ShapeDefaultValueCreator() + { + return RatingViewDefaults.Shape; + } + + Thickness IRatingViewShape.ShapePaddingDefaultValueCreator() + { + return RatingViewDefaults.ShapePadding; + } + ///Method called every time the control's Binding Context is changed. protected override void OnBindingContextChanged() { @@ -163,9 +232,9 @@ protected override void OnBindingContextChanged() ///Called every time a child is added to the control. protected override void OnChildAdded(Element child) { - if (Control is null && child is Grid grid) + if (Control is null && child is HorizontalStackLayout stack) { - Control = grid; + Control = stack; OnControlInitialized(); } @@ -174,50 +243,59 @@ protected override void OnChildAdded(Element child) static bool maximumRatingValidator(BindableObject bindable, object value) { - return (byte)value <= RatingViewDefaults.MaximumRatings; + return (byte)value is >= 1 and <= RatingViewDefaults.MaximumRatings; } - static void OnRatingBorderChanged(BindableObject bindable, object oldValue, object newValue) + static void OnIsEnabledChanged(BindableObject bindable, object oldValue, object newValue) { - // TODO: Need to add the Border to the child controls of the grid, so as to allow true styling (such as TripAdvisor ratings)! + if (newValue is null) + { + return; + } + + RatingView ratingView = (RatingView)bindable; + ratingView.OnPropertyChanged(nameof(ratingView.IsEnabled)); + ratingView.HandleIsEnabledChanged(); } - static void OnSpacingChanged(BindableObject bindable, object oldValue, object newValue) + /// Maximum rating has changed. + /// + /// Re draw the control. + /// If the current rating is greater than the maximum, then change the current rating to the new value. + /// + /// RatingView object. + /// Old maximum rating value. + /// New maximum rating value. + static void OnMaximumRatingChange(BindableObject bindable, object oldValue, object newValue) { - RatingView ratingView = (RatingView)bindable; - if (ratingView.Control is Grid grid) + if (newValue is null) { - grid.ColumnSpacing = (double)newValue; + return; } - } - static void OnSizeChanged(BindableObject bindable, object oldValue, object newValue) - { RatingView ratingView = (RatingView)bindable; - if (ratingView.Control is Grid grid) + ratingView.ClearAndReDraw(); + if ((byte)newValue < ratingView.Rating) { - double newSize = (double)newValue; - for (int column = 0; column < grid.Count; column++) - { - grid.ColumnDefinitions[column] = new ColumnDefinition { Width = newSize }; - Microsoft.Maui.Controls.Shapes.Path rating = (Microsoft.Maui.Controls.Shapes.Path)grid.Children[column]; - rating.WidthRequest = newSize; - rating.HeightRequest = newSize; - ratingView.shapes[column].WidthRequest = newSize; - ratingView.shapes[column].HeightRequest = newSize; - } + ratingView.Rating = (byte)newValue; + weakEventManager.HandleEvent(ratingView, EventArgs.Empty, nameof(RatingChanged)); } } static void OnShapeOutlineColorChanged(BindableObject bindable, object oldValue, object newValue) { + if (newValue is null) + { + return; + } + RatingView ratingView = (RatingView)bindable; - if (ratingView.Control is Grid grid) + if (ratingView.Control is HorizontalStackLayout stack) { Color newColor = (Color)newValue; - for (int column = 0; column < grid.Count; column++) + for (int column = 0; column < stack.Count; column++) { - Microsoft.Maui.Controls.Shapes.Path rating = (Microsoft.Maui.Controls.Shapes.Path)grid.Children[column]; + Microsoft.Maui.Controls.Shapes.Path rating = (Microsoft.Maui.Controls.Shapes.Path)((Border)stack.Children[column]).Content!.GetVisualTreeDescendants()[0]; rating.Stroke = newColor; ratingView.shapes[column].Stroke = newColor; } @@ -226,58 +304,68 @@ static void OnShapeOutlineColorChanged(BindableObject bindable, object oldValue, static void OnShapeOutlineThicknessChanged(BindableObject bindable, object oldValue, object newValue) { + if (newValue is null) + { + return; + } + RatingView ratingView = (RatingView)bindable; - if (ratingView.Control is Grid grid) + if (ratingView.Control is HorizontalStackLayout stack) { double newThickness = (double)newValue; - for (int column = 0; column < grid.Count; column++) + for (int column = 0; column < stack.Count; column++) { - Microsoft.Maui.Controls.Shapes.Path rating = (Microsoft.Maui.Controls.Shapes.Path)grid.Children[column]; + Microsoft.Maui.Controls.Shapes.Path rating = (Microsoft.Maui.Controls.Shapes.Path)((Border)stack.Children[column]).Content!.GetVisualTreeDescendants()[0]; rating.StrokeThickness = newThickness; ratingView.shapes[column].StrokeThickness = newThickness; } } } - static void OnIsEnabledChanged(BindableObject bindable, object oldValue, object newValue) + static void OnSizeChanged(BindableObject bindable, object oldValue, object newValue) { - RatingView ratingView = (RatingView)bindable; - ratingView.OnPropertyChanged(nameof(ratingView.IsEnabled)); - ratingView.HandleIsEnabledChanged(); - } + if (newValue is null) + { + return; + } - /// Shape property changed. - /// RatingView object. - /// Old shape value. - /// New shape value. - static void OnShapePropertyChanged(BindableObject bindable, object oldValue, object newValue) - { RatingView ratingView = (RatingView)bindable; - ratingView.ReDraw(); + if (ratingView.Control is HorizontalStackLayout stack) + { + double newSize = (double)newValue; + for (int column = 0; column < stack.Count; column++) + { + Microsoft.Maui.Controls.Shapes.Path rating = (Microsoft.Maui.Controls.Shapes.Path)((Border)stack.Children[column]).Content!.GetVisualTreeDescendants()[0]; + rating.WidthRequest = newSize; + rating.HeightRequest = newSize; + rating.Margin = ratingView.ShapePadding; + ratingView.shapes[column].WidthRequest = newSize; + ratingView.shapes[column].HeightRequest = newSize; + } + } } - /// Maximum rating has changed. - /// - /// Re draw the control. - /// If the current rating is greater than the maximum, then change the current rating to the new value. - /// - /// RatingView object. - /// Old maximum rating value. - /// New maximum rating value. - static void OnMaximumRatingChange(BindableObject bindable, object oldValue, object newValue) + static void OnSpacingChanged(BindableObject bindable, object oldValue, object newValue) { - RatingView ratingView = (RatingView)bindable; - if ((byte)newValue < ratingView.CurrentRating) + if (newValue is null) { - ratingView.CurrentRating = (byte)newValue; - weakEventManager.HandleEvent(ratingView, EventArgs.Empty, nameof(RatingChanged)); + return; } - ratingView.ReDraw(); + RatingView ratingView = (RatingView)bindable; + if (ratingView.Control is HorizontalStackLayout stack) + { + stack.Spacing = (double)newValue; + } } static void OnUpdateRatingDraw(BindableObject bindable, object oldValue, object newValue) { + if (newValue is null) + { + return; + } + RatingView ratingView = (RatingView)bindable; ratingView.UpdateRatingDraw(); } @@ -287,29 +375,45 @@ void AddChildrenToControl() { string shape = Shape switch { - RatingViewShape.Heart => Core.Primitives.RatingViewShape.Heart.PathData, RatingViewShape.Circle => Core.Primitives.RatingViewShape.Circle.PathData, - RatingViewShape.Like => Core.Primitives.RatingViewShape.Like.PathData, - RatingViewShape.Dislike => Core.Primitives.RatingViewShape.Dislike.PathData, RatingViewShape.Custom => CustomShape ?? Core.Primitives.RatingViewShape.Star.PathData, + RatingViewShape.Dislike => Core.Primitives.RatingViewShape.Dislike.PathData, + RatingViewShape.Heart => Core.Primitives.RatingViewShape.Heart.PathData, + RatingViewShape.Like => Core.Primitives.RatingViewShape.Like.PathData, _ => Core.Primitives.RatingViewShape.Star.PathData, }; for (int i = 0; i < MaximumRating; i++) { - Control?.ColumnDefinitions.Add(new ColumnDefinition { Width = Size }); + Border shapeBorder = new() + { + Padding = ShapePadding, + Margin = 0, + StrokeThickness = 0 + }; Microsoft.Maui.Controls.Shapes.Path image = new() { + Aspect = Stretch.Uniform, Data = (Geometry?)new PathGeometryConverter().ConvertFromInvariantString(shape), - Fill = i <= CurrentRating ? FilledBackgroundColor : EmptyBackgroundColor, + HeightRequest = Size, + Margin = ShapePadding, Stroke = RatingShapeOutlineColor, - StrokeLineJoin = PenLineJoin.Round, StrokeLineCap = PenLineCap.Round, + StrokeLineJoin = PenLineJoin.Round, StrokeThickness = RatingShapeOutlineThickness, - Aspect = Stretch.Uniform, - HeightRequest = Size, - WidthRequest = Size + WidthRequest = Size, }; + + if (RatingFill is RatingFillElement.Shape) + { + image.Fill = i <= Rating ? (Brush)FilledBackgroundColor : (Brush)EmptyBackgroundColor; + } + else + { + image.Fill = BackgroundColor; + image.BackgroundColor = FilledBackgroundColor; + } + if (IsEnabled) { TapGestureRecognizer tapGestureRecognizer = new(); @@ -317,16 +421,23 @@ void AddChildrenToControl() image.GestureRecognizers.Add(tapGestureRecognizer); } - Control?.Children.Add(image); - Control?.SetColumn(image, i); - + shapeBorder.Content = image; + Control?.Children.Add(shapeBorder); shapes[i] = image; } UpdateRatingDraw(); } - /// Ensure VisualElement.IsEnabled always matches RatingView.IsEnabled, and remove any gesture recognisers added + /// Clear and re-draw the control. + void ClearAndReDraw() + { + Control?.Children.Clear(); + shapes = new Microsoft.Maui.Controls.Shapes.Path[MaximumRating]; + AddChildrenToControl(); + } + + /// Ensure VisualElement.IsEnabled always matches RatingView.IsEnabled, and remove any added gesture recognisers. void HandleIsEnabledChanged() { base.IsEnabled = IsEnabled; @@ -334,7 +445,7 @@ void HandleIsEnabledChanged() { for (int i = 0; i < Control?.Children.Count; i++) { - Microsoft.Maui.Controls.Shapes.Path rankControl = (Microsoft.Maui.Controls.Shapes.Path)Control.Children[i]; + Microsoft.Maui.Controls.Shapes.Path rankControl = (Microsoft.Maui.Controls.Shapes.Path)((Border)Control.Children[i]).Content!.GetVisualTreeDescendants()[0]; rankControl?.GestureRecognizers.Clear(); } } @@ -347,11 +458,12 @@ void OnControlInitialized() HorizontalOptions = new LayoutOptions(LayoutAlignment.Center, true); if (Control is not null) { - Control.ColumnSpacing = Spacing; + Control.Spacing = Spacing; } } - /// Shape tapped event. + /// Shape tapped event, to re-draw the current rating and bubble up the event. + /// When a shape such as 'Like' and there is only a maximum rating of 1, we then toggle the current between 0 and 1. /// Element sender of event. /// Event arguments. void OnShapeTapped(object? sender, TappedEventArgs? e) @@ -366,81 +478,87 @@ void OnShapeTapped(object? sender, TappedEventArgs? e) return; } - int columnIndex = Control.GetColumn(tappedShape); - double originalRating = CurrentRating; - if (MaximumRating > 1) - { - CurrentRating = columnIndex + 1; - } - - if (!originalRating.Equals(CurrentRating)) + int childIndex = Control.Children.IndexOf(tappedShape.Parent); + double originalRating = Rating; + Rating = MaximumRating > 1 ? childIndex + 1 : Rating.Equals(0.0) ? 1 : 0; + if (!originalRating.Equals(Rating)) { UpdateRatingDraw(); weakEventManager.HandleEvent(this, e, nameof(RatingChanged)); } } - /// Re-draw the control. - void ReDraw() - { - Control?.Children.Clear(); - Control?.ColumnDefinitions.Clear(); - shapes = new Microsoft.Maui.Controls.Shapes.Path[MaximumRating]; - AddChildrenToControl(); - } - /// Update the drawing of the controls ratings. void UpdateRatingDraw() { for (int i = 0; i < MaximumRating; i++) { Microsoft.Maui.Controls.Shapes.Path image = shapes[i]; - if (CurrentRating >= i + 1) + if (Rating >= i + 1) { image.Stroke = RatingShapeOutlineColor; - image.Fill = FilledBackgroundColor; + if (RatingFill is RatingFillElement.Shape) + { + image.Fill = FilledBackgroundColor; + } + else + { + image.Fill = BackgroundColor; + image.BackgroundColor = FilledBackgroundColor; + ((Border)image.Parent).BackgroundColor = FilledBackgroundColor; + } + continue; } - if (CurrentRating % 1 is 0) + if (Rating % 1 is 0) { - image.Fill = EmptyBackgroundColor; + if (RatingFill is RatingFillElement.Shape) + { + image.Fill = EmptyBackgroundColor; + } + else + { + image.Fill = BackgroundColor; + image.Background = null; + image.BackgroundColor = EmptyBackgroundColor; + ((Border)image.Parent).BackgroundColor = EmptyBackgroundColor; + } + image.Stroke = RatingShapeOutlineColor; continue; } - double fraction = CurrentRating - Math.Floor(CurrentRating); - Microsoft.Maui.Controls.Shapes.Path element = shapes[(int)(CurrentRating - fraction)]; + double fraction = Rating - Math.Floor(Rating); + Microsoft.Maui.Controls.Shapes.Path element = shapes[(int)(Rating - fraction)]; GradientStopCollection colors = [ new(FilledBackgroundColor, (float)fraction), new(EmptyBackgroundColor, (float)fraction) ]; - element.Fill = new LinearGradientBrush(colors, new Point(0, 0), new Point(1, 0)); + if (RatingFill is RatingFillElement.Shape) + { + element.Fill = new LinearGradientBrush(colors, new Point(0, 0), new Point(1, 0)); + } + else + { + image.Fill = BackgroundColor; + image.Background = new LinearGradientBrush(colors, new Point(0, 0), new Point(1, 0)); + ((Border)image.Parent).Background = new LinearGradientBrush(colors, new Point(0, 0), new Point(1, 0)); + } + element.Stroke = RatingShapeOutlineColor; } } } -/// Rating view shape enumerator. -public enum RatingViewShape +/// Rating view fill element. +public enum RatingFillElement { - /// A star rating shape. - Star = 0, - - /// A heart rating shape. - Heart = 1, - - /// A circle rating shape. - Circle = 2, - - /// A like/thumbs up rating shape. - Like = 3, - - /// A dislike/thumbs down rating shape. - Dislike = 4, + /// Fill the rating shape. + Shape = 0, - /// A custom rating shape. - Custom = 99 + /// Fill the rating cell. + Cell = 1 } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui/Views/RatingView/RatingViewItemElement.shared.cs b/src/CommunityToolkit.Maui/Views/RatingView/RatingViewItemElement.shared.cs new file mode 100644 index 0000000000..bbca2c08b2 --- /dev/null +++ b/src/CommunityToolkit.Maui/Views/RatingView/RatingViewItemElement.shared.cs @@ -0,0 +1,82 @@ +// Ignore Spelling: bindable + +using CommunityToolkit.Maui.Core; + +namespace CommunityToolkit.Maui.Views; + +static class RatingViewItemElement +{ + /// Bindable property for attached property CustomShape. + public static readonly BindableProperty CustomShapeProperty = BindableProperty.Create(nameof(IRatingViewShape.CustomShape), typeof(string), typeof(IRatingViewShape), defaultValue: null, propertyChanged: OnCustomShapePropertyChanged); + + /// Bindable property for attached property PaddingBottom. + public static readonly BindableProperty ShapePaddingBottomProperty = BindableProperty.Create("PaddingBottom", typeof(double), typeof(IRatingViewShape), default(double), propertyChanged: OnShapePaddingBottomChanged); + + /// Bindable property for attached property PaddingLeft. + public static readonly BindableProperty ShapePaddingLeftProperty = BindableProperty.Create("PaddingLeft", typeof(double), typeof(IRatingViewShape), default(double), propertyChanged: OnShapePaddingLeftChanged); + + /// Bindable property for . + public static readonly BindableProperty ShapePaddingProperty = BindableProperty.Create(nameof(IRatingViewShape.ShapePadding), typeof(Thickness), typeof(IRatingViewShape), default(Thickness), propertyChanged: OnShapePaddingPropertyChanged, defaultValueCreator: ShapePaddingDefaultValueCreator); + + /// Bindable property for attached property PaddingRight. + public static readonly BindableProperty ShapePaddingRightProperty = BindableProperty.Create("PaddingRight", typeof(double), typeof(IRatingViewShape), default(double), propertyChanged: OnShapePaddingRightChanged); + + /// Bindable property for attached property PaddingTop. + public static readonly BindableProperty ShapePaddingTopProperty = BindableProperty.Create("PaddingTop", typeof(double), typeof(IRatingViewShape), default(double), propertyChanged: OnShapePaddingTopChanged); + + /// Bindable property for attached property Shape. + public static readonly BindableProperty ShapeProperty = BindableProperty.Create(nameof(IRatingViewShape.Shape), typeof(RatingViewShape), typeof(IRatingViewShape), defaultValue: RatingViewShape.Star, propertyChanged: OnShapePropertyChanged, defaultValueCreator: ShapeDefaultValueCreator); + + static void OnCustomShapePropertyChanged(BindableObject bindable, object? oldValue, object? newValue) + { + ((IRatingViewShape)bindable).OnCustomShapePropertyChanged((string?)oldValue, (string?)newValue); + } + + static void OnShapePaddingBottomChanged(BindableObject bindable, object oldValue, object newValue) + { + Thickness padding = (Thickness)bindable.GetValue(ShapePaddingProperty); + padding.Bottom = (double)newValue; + bindable.SetValue(ShapePaddingProperty, padding); + } + + static void OnShapePaddingLeftChanged(BindableObject bindable, object oldValue, object newValue) + { + Thickness padding = (Thickness)bindable.GetValue(ShapePaddingProperty); + padding.Left = (double)newValue; + bindable.SetValue(ShapePaddingProperty, padding); + } + + static void OnShapePaddingPropertyChanged(BindableObject bindable, object oldValue, object newValue) + { + ((IRatingViewShape)bindable).OnShapePaddingPropertyChanged((Thickness)oldValue, (Thickness)newValue); + } + + static void OnShapePaddingRightChanged(BindableObject bindable, object oldValue, object newValue) + { + Thickness padding = (Thickness)bindable.GetValue(ShapePaddingProperty); + padding.Right = (double)newValue; + bindable.SetValue(ShapePaddingProperty, padding); + } + + static void OnShapePaddingTopChanged(BindableObject bindable, object oldValue, object newValue) + { + Thickness padding = (Thickness)bindable.GetValue(ShapePaddingProperty); + padding.Top = (double)newValue; + bindable.SetValue(ShapePaddingProperty, padding); + } + + static void OnShapePropertyChanged(BindableObject bindable, object oldValue, object newValue) + { + ((IRatingViewShape)bindable).OnShapePropertyChanged((RatingViewShape)oldValue, (RatingViewShape)newValue); + } + + static object ShapeDefaultValueCreator(BindableObject bindable) + { + return ((IRatingViewShape)bindable).ShapeDefaultValueCreator(); + } + + static object ShapePaddingDefaultValueCreator(BindableObject bindable) + { + return ((IRatingViewShape)bindable).ShapePaddingDefaultValueCreator(); + } +} \ No newline at end of file From e7d947fe534bf2a8319f9fe2b3df2c50aea5bfc7 Mon Sep 17 00:00:00 2001 From: "github@internetwideworld.com" Date: Fri, 30 Aug 2024 18:22:11 +0100 Subject: [PATCH 04/92] Moved more to the Item Element Added additional samples --- .../RatingViewInspirationsPage.xaml | 105 +++++++- .../RatingViewInspirationsPage.xaml.cs | 7 +- .../RatingView/RatingViewXamlPage.xaml.cs | 2 +- .../RatingView/IRatingView.shared.cs | 4 +- .../RatingView/IRatingViewShape.shared.cs | 52 +++- .../Defaults/RatingViewDefaults.shared.cs | 25 +- .../Views/RatingView/RatingViewTests.cs | 8 +- .../Views/RatingView/RatingView.shared.cs | 228 +++++++++--------- .../RatingViewItemElement.shared.cs | 92 ++++--- 9 files changed, 351 insertions(+), 172 deletions(-) diff --git a/samples/CommunityToolkit.Maui.Sample/Pages/Views/RatingView/RatingViewInspirationsPage.xaml b/samples/CommunityToolkit.Maui.Sample/Pages/Views/RatingView/RatingViewInspirationsPage.xaml index a6d67fddb1..2a631673c3 100644 --- a/samples/CommunityToolkit.Maui.Sample/Pages/Views/RatingView/RatingViewInspirationsPage.xaml +++ b/samples/CommunityToolkit.Maui.Sample/Pages/Views/RatingView/RatingViewInspirationsPage.xaml @@ -28,7 +28,25 @@ -