diff --git a/src/Compatibility/Core/src/AppHostBuilderExtensions.cs b/src/Compatibility/Core/src/AppHostBuilderExtensions.cs index 4e67e1754d91..27093ab5f192 100644 --- a/src/Compatibility/Core/src/AppHostBuilderExtensions.cs +++ b/src/Compatibility/Core/src/AppHostBuilderExtensions.cs @@ -95,7 +95,6 @@ static MauiAppBuilder SetupDefaults(this MauiAppBuilder builder) handlers.TryAddCompatibilityRenderer(typeof(ListView), typeof(ListViewRenderer)); handlers.TryAddCompatibilityRenderer(typeof(CollectionView), typeof(CollectionViewRenderer)); handlers.TryAddCompatibilityRenderer(typeof(CarouselView), typeof(CarouselViewRenderer)); - handlers.TryAddCompatibilityRenderer(typeof(IndicatorView), typeof(IndicatorViewRenderer)); handlers.TryAddCompatibilityRenderer(typeof(Path), typeof(PathRenderer)); handlers.TryAddCompatibilityRenderer(typeof(Ellipse), typeof(EllipseRenderer)); handlers.TryAddCompatibilityRenderer(typeof(Line), typeof(LineRenderer)); diff --git a/src/Controls/samples/Controls.Sample/Controls.Sample.csproj b/src/Controls/samples/Controls.Sample/Controls.Sample.csproj index dfd9a22373cb..99f6075733bd 100644 --- a/src/Controls/samples/Controls.Sample/Controls.Sample.csproj +++ b/src/Controls/samples/Controls.Sample/Controls.Sample.csproj @@ -30,6 +30,7 @@ + diff --git a/src/Controls/samples/Controls.Sample/Pages/Compatibility/IndicatorViewPage.xaml b/src/Controls/samples/Controls.Sample/Pages/Compatibility/IndicatorViewPage.xaml deleted file mode 100644 index b6b079cbaa09..000000000000 --- a/src/Controls/samples/Controls.Sample/Pages/Compatibility/IndicatorViewPage.xaml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - Item 1 - Item 2 - Item 3 - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Controls/samples/Controls.Sample/Pages/Compatibility/IndicatorViewPage.xaml.cs b/src/Controls/samples/Controls.Sample/Pages/Compatibility/IndicatorViewPage.xaml.cs deleted file mode 100644 index 5608ece3cb25..000000000000 --- a/src/Controls/samples/Controls.Sample/Pages/Compatibility/IndicatorViewPage.xaml.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Maui.Controls.Sample.Pages -{ - public partial class IndicatorViewPage - { - public IndicatorViewPage() - { - InitializeComponent(); - } - } -} \ No newline at end of file diff --git a/src/Controls/samples/Controls.Sample/Pages/Controls/IndicatorPage.xaml b/src/Controls/samples/Controls.Sample/Pages/Controls/IndicatorPage.xaml new file mode 100644 index 000000000000..d53d7d100ed3 --- /dev/null +++ b/src/Controls/samples/Controls.Sample/Pages/Controls/IndicatorPage.xaml @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Controls/samples/Controls.Sample/Pages/Controls/IndicatorPage.xaml.cs b/src/Controls/samples/Controls.Sample/Pages/Controls/IndicatorPage.xaml.cs new file mode 100644 index 000000000000..5bc4118dd4f6 --- /dev/null +++ b/src/Controls/samples/Controls.Sample/Pages/Controls/IndicatorPage.xaml.cs @@ -0,0 +1,14 @@ +using System; +using Microsoft.Maui.Controls; +using Microsoft.Maui.Graphics; + +namespace Maui.Controls.Sample.Pages +{ + public partial class IndicatorPage + { + public IndicatorPage() + { + InitializeComponent(); + } + } +} \ No newline at end of file diff --git a/src/Controls/samples/Controls.Sample/ViewModels/CompatibilityViewModel.cs b/src/Controls/samples/Controls.Sample/ViewModels/CompatibilityViewModel.cs index b7e1f9832c36..ba927ca50cf3 100644 --- a/src/Controls/samples/Controls.Sample/ViewModels/CompatibilityViewModel.cs +++ b/src/Controls/samples/Controls.Sample/ViewModels/CompatibilityViewModel.cs @@ -21,9 +21,6 @@ protected override IEnumerable CreateItems() => new[] new SectionModel(typeof(FramePage), "Frame", "The Frame class derives from ContentView and displays a border, or frame, around its child."), - new SectionModel(typeof(IndicatorViewPage), "IndicatorView", - "IndicatorView displays indicators that represent the number of items in a CarouselView. Set the CarouselView.IndicatorView property to the IndicatorView object to display indicators for the CarouselView."), - new SectionModel(typeof(ListViewPage), "ListView", "ListView derives from ItemsView and displays a scrollable list of selectable data items."), diff --git a/src/Controls/samples/Controls.Sample/ViewModels/ControlsViewModel.cs b/src/Controls/samples/Controls.Sample/ViewModels/ControlsViewModel.cs index 6b9ce2ea61fa..57e20412d028 100644 --- a/src/Controls/samples/Controls.Sample/ViewModels/ControlsViewModel.cs +++ b/src/Controls/samples/Controls.Sample/ViewModels/ControlsViewModel.cs @@ -63,7 +63,10 @@ protected override IEnumerable CreateItems() => new[] new SectionModel(typeof(TimePickerPage), "TimePicker", "A view that allows the user to select a time."), - new SectionModel(typeof(WebViewPage), "WebView", + new SectionModel(typeof(IndicatorPage), "IndicatorView", + "IndicatorView displays indicators. It can also represent the number of items in a CarouselView. Set the CarouselView.IndicatorView property to the IndicatorView object to display indicators for the CarouselView."), + + new SectionModel(typeof(WebViewPage), "WebView", "WebView is a view for displaying web and HTML content in your app.") }; } diff --git a/src/Controls/src/Core/HandlerImpl/IndicatorView.Impl.cs b/src/Controls/src/Core/HandlerImpl/IndicatorView.Impl.cs new file mode 100644 index 000000000000..d887e89129de --- /dev/null +++ b/src/Controls/src/Core/HandlerImpl/IndicatorView.Impl.cs @@ -0,0 +1,13 @@ +using System.Runtime.CompilerServices; +using Microsoft.Maui.Graphics; + +namespace Microsoft.Maui.Controls +{ + public partial class IndicatorView : ITemplatedIndicatorView + { + Paint IIndicatorView.IndicatorColor => IndicatorColor?.AsPaint(); + Paint IIndicatorView.SelectedIndicatorColor => SelectedIndicatorColor?.AsPaint(); + IShape IIndicatorView.IndicatorsShape => IndicatorsShape == IndicatorShape.Square ? new Shapes.Rectangle() : new Shapes.Ellipse(); + Maui.ILayout ITemplatedIndicatorView.IndicatorsLayoutOverride => (IndicatorTemplate != null) ? IndicatorLayout as Maui.ILayout : null; + } +} diff --git a/src/Controls/src/Core/Hosting/AppHostBuilderExtensions.cs b/src/Controls/src/Core/Hosting/AppHostBuilderExtensions.cs index 6787f95b93f4..6edbfa9ffb7c 100644 --- a/src/Controls/src/Core/Hosting/AppHostBuilderExtensions.cs +++ b/src/Controls/src/Core/Hosting/AppHostBuilderExtensions.cs @@ -44,6 +44,7 @@ public static partial class AppHostBuilderExtensions { typeof(Window), typeof(WindowHandler) }, #if __ANDROID__ || __IOS__ { typeof(RefreshView), typeof(RefreshViewHandler) }, + { typeof(IndicatorView), typeof(IndicatorViewHandler) }, #endif #if __ANDROID__ { typeof(NavigationPage), typeof(Controls.Handlers.NavigationPageHandler) }, diff --git a/src/Controls/src/Core/IndicatorStackLayout.cs b/src/Controls/src/Core/IndicatorStackLayout.cs index 709429f44655..a1878365558f 100644 --- a/src/Controls/src/Core/IndicatorStackLayout.cs +++ b/src/Controls/src/Core/IndicatorStackLayout.cs @@ -6,7 +6,7 @@ namespace Microsoft.Maui.Controls { - internal class IndicatorStackLayout : Compatibility.StackLayout + internal class IndicatorStackLayout : StackLayout { IndicatorView _indicatorView; public IndicatorStackLayout(IndicatorView indicatorView) @@ -98,11 +98,14 @@ void ResetIndicatorStylesNonBatch() var position = _indicatorView.Position; var selectedIndex = position >= maxVisible ? maxVisible - 1 : position; bool isSelected = index == selectedIndex; - Children[index].BackgroundColor = isSelected + var visualElement = Children[index] as VisualElement; + + visualElement.BackgroundColor = isSelected ? GetColorOrDefault(_indicatorView.SelectedIndicatorColor, Colors.Gray) : GetColorOrDefault(_indicatorView.IndicatorColor, Colors.Silver); - VisualStateManager.GoToState(Children[index], isSelected + + VisualStateManager.GoToState(visualElement, isSelected ? VisualStateManager.CommonStates.Selected : VisualStateManager.CommonStates.Normal); diff --git a/src/Controls/src/Core/IndicatorView.cs b/src/Controls/src/Core/IndicatorView.cs index 5e665680ebce..b74d357e15ea 100644 --- a/src/Controls/src/Core/IndicatorView.cs +++ b/src/Controls/src/Core/IndicatorView.cs @@ -2,15 +2,18 @@ using System.Collections; using System.Collections.Specialized; using Microsoft.Maui.Graphics; +using Microsoft.Maui.Layouts; namespace Microsoft.Maui.Controls { [ContentProperty(nameof(IndicatorLayout))] - public class IndicatorView : TemplatedView + public partial class IndicatorView : TemplatedView { const int DefaultPadding = 4; - public static readonly BindableProperty IndicatorsShapeProperty = BindableProperty.Create(nameof(IndicatorsShape), typeof(IndicatorShape), typeof(IndicatorView), IndicatorShape.Circle); + const int DefaultViewSize = 10; + + public static readonly BindableProperty IndicatorsShapeProperty = BindableProperty.Create(nameof(IndicatorsShape), typeof(IndicatorShape), typeof(IndicatorView), Controls.IndicatorShape.Circle); public static readonly BindableProperty PositionProperty = BindableProperty.Create(nameof(Position), typeof(int), typeof(IndicatorView), default(int), BindingMode.TwoWay); @@ -25,9 +28,9 @@ public class IndicatorView : TemplatedView public static readonly BindableProperty HideSingleProperty = BindableProperty.Create(nameof(HideSingle), typeof(bool), typeof(IndicatorView), true); - public static readonly BindableProperty IndicatorColorProperty = BindableProperty.Create(nameof(IndicatorColor), typeof(Color), typeof(IndicatorView), null); + public static readonly BindableProperty IndicatorColorProperty = BindableProperty.Create(nameof(IndicatorColor), typeof(Color), typeof(IndicatorView), Colors.LightGrey); - public static readonly BindableProperty SelectedIndicatorColorProperty = BindableProperty.Create(nameof(SelectedIndicatorColor), typeof(Color), typeof(IndicatorView), null); + public static readonly BindableProperty SelectedIndicatorColorProperty = BindableProperty.Create(nameof(SelectedIndicatorColor), typeof(Color), typeof(IndicatorView), Colors.Black); public static readonly BindableProperty IndicatorSizeProperty = BindableProperty.Create(nameof(IndicatorSize), typeof(double), typeof(IndicatorView), 6.0); @@ -107,25 +110,19 @@ public IEnumerable ItemsSource set => SetValue(ItemsSourceProperty, value); } - protected override SizeRequest OnMeasure(double widthConstraint, double heightConstraint) { - var baseRequest = base.OnMeasure(widthConstraint, heightConstraint); - - if (IndicatorTemplate != null) - return baseRequest; - - var defaultSize = IndicatorSize + DefaultPadding + DefaultPadding + 1; - var items = Count; - var sizeRequest = new SizeRequest(new Size(items * defaultSize, IndicatorSize), new Size(10, 10)); - return sizeRequest; + if (IndicatorTemplate == null) + return Device.PlatformServices.GetNativeSize(this, widthConstraint, heightConstraint); + else + return base.OnMeasure(widthConstraint, heightConstraint); } static void UpdateIndicatorLayout(IndicatorView indicatorView, object newValue) { if (newValue != null) { - indicatorView.IndicatorLayout = new IndicatorStackLayout(indicatorView); + indicatorView.IndicatorLayout = new IndicatorStackLayout(indicatorView) { Spacing = DefaultPadding }; } else if (indicatorView.IndicatorLayout == null) { diff --git a/src/Controls/src/Core/Shapes/Ellipse.cs b/src/Controls/src/Core/Shapes/Ellipse.cs index 45f8412b4a25..8ffbb3fabe6b 100644 --- a/src/Controls/src/Core/Shapes/Ellipse.cs +++ b/src/Controls/src/Core/Shapes/Ellipse.cs @@ -1,3 +1,5 @@ +using Microsoft.Maui.Graphics; + namespace Microsoft.Maui.Controls.Shapes { public sealed partial class Ellipse : Shape diff --git a/src/Controls/src/Core/Shapes/Rectangle.cs b/src/Controls/src/Core/Shapes/Rectangle.cs index ea162a902677..b22b9d85ef93 100644 --- a/src/Controls/src/Core/Shapes/Rectangle.cs +++ b/src/Controls/src/Core/Shapes/Rectangle.cs @@ -1,3 +1,5 @@ +using Microsoft.Maui.Graphics; + namespace Microsoft.Maui.Controls.Shapes { public sealed partial class Rectangle : Shape diff --git a/src/Controls/src/Core/TemplatedView.cs b/src/Controls/src/Core/TemplatedView.cs index eb1c092abfc1..33b73f7111cf 100644 --- a/src/Controls/src/Core/TemplatedView.cs +++ b/src/Controls/src/Core/TemplatedView.cs @@ -5,7 +5,7 @@ namespace Microsoft.Maui.Controls { - public class TemplatedView : Compatibility.Layout, IControlTemplated + public partial class TemplatedView : Compatibility.Layout, IControlTemplated { public static readonly BindableProperty ControlTemplateProperty = BindableProperty.Create(nameof(ControlTemplate), typeof(ControlTemplate), typeof(TemplatedView), null, propertyChanged: TemplateUtilities.OnControlTemplateChanged); diff --git a/src/Core/src/Core/IIndicatorView.cs b/src/Core/src/Core/IIndicatorView.cs new file mode 100644 index 000000000000..d7f4bf14ed15 --- /dev/null +++ b/src/Core/src/Core/IIndicatorView.cs @@ -0,0 +1,63 @@ +using Microsoft.Maui.Graphics; + +namespace Microsoft.Maui +{ + /// + /// A view that displays indicators that represent the number of items, and current position + /// + public interface IIndicatorView : IView + { + /// + /// The number of indicators + /// + int Count { get; } + + /// + /// The currently selected indicator index + /// + int Position { get; set; } + + /// + /// S ize of the indicators + /// + double IndicatorSize { get; } + + /// + /// Maximum number of visible indicators + /// + int MaximumVisible { get; } + + /// + /// Indicates whether the indicator should be hidden when only one exists. + /// + bool HideSingle { get; } + + /// + /// Color of the indicators + /// We only support SolidPaint color for now + /// + Paint? IndicatorColor { get; } + + /// + /// Color of the indicator that represents the currently selected index + /// We only support SolidPaint color for now + /// + Paint? SelectedIndicatorColor { get; } + + /// + /// Shape of native indicators, can be Circle or Square + /// + IShape IndicatorsShape { get; } + } + + /// + /// A layout that displays indicators that represent the number of items using a DataTemplate for indicators + /// + public interface ITemplatedIndicatorView : IIndicatorView + { + /// + /// The layout where to renderer the Template for the indicators + /// + ILayout? IndicatorsLayoutOverride { get; } + } +} diff --git a/src/Core/src/Extensions/IndicatorViewExtensions.cs b/src/Core/src/Extensions/IndicatorViewExtensions.cs new file mode 100644 index 000000000000..86c739deee21 --- /dev/null +++ b/src/Core/src/Extensions/IndicatorViewExtensions.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.Maui.Graphics; + +namespace Microsoft.Maui +{ + internal static class IndicatorViewExtensions + { + public static int GetMaximumVisible(this IIndicatorView indicatorView) + { + var minValue = Math.Min(indicatorView.MaximumVisible, indicatorView.Count); + var maximumVisible = minValue <= 0 ? 0 : minValue; + bool hideSingle = indicatorView.HideSingle; + + if (maximumVisible == 1 && hideSingle) + maximumVisible = 0; + + return maximumVisible; + } + + public static bool IsCircleShape(this IIndicatorView indicatorView) + { + var sH = indicatorView.IndicatorsShape; + var pointsCount = 13; + if (sH != null) + { + var path = sH.PathForBounds(new Rectangle(0, 0, 6, 6)); + pointsCount = path.Count; + } + + return pointsCount == 13; + + } + } +} diff --git a/src/Core/src/Handlers/IndicatorView/IndicatorViewHandler.Android.cs b/src/Core/src/Handlers/IndicatorView/IndicatorViewHandler.Android.cs new file mode 100644 index 000000000000..3ca2d599aeb4 --- /dev/null +++ b/src/Core/src/Handlers/IndicatorView/IndicatorViewHandler.Android.cs @@ -0,0 +1,63 @@ +using Microsoft.Maui.Platform.Android; +using AView = Android.Views.View; + +namespace Microsoft.Maui.Handlers +{ + public partial class IndicatorViewHandler : ViewHandler + { + protected override MauiPageControl CreateNativeView() + { + return new MauiPageControl(Context); + } + + private protected override void OnConnectHandler(AView nativeView) + { + base.OnConnectHandler(nativeView); + NativeView.SetIndicatorView(VirtualView); + } + + private protected override void OnDisconnectHandler(AView nativeView) + { + base.OnDisconnectHandler(nativeView); + NativeView.SetIndicatorView(null); + } + + public static void MapCount(IndicatorViewHandler handler, IIndicatorView indicator) + { + handler.NativeView.UpdateIndicatorCount(); + } + + public static void MapPosition(IndicatorViewHandler handler, IIndicatorView indicator) + { + handler.NativeView.UpdatePosition(); + } + + public static void MapHideSingle(IndicatorViewHandler handler, IIndicatorView indicator) + { + handler.NativeView.UpdateIndicatorCount(); + } + + public static void MapMaximumVisible(IndicatorViewHandler handler, IIndicatorView indicator) + { + handler.NativeView.UpdateIndicatorCount(); + } + + public static void MapIndicatorSize(IndicatorViewHandler handler, IIndicatorView indicator) + { + handler.NativeView.ResetIndicators(); + } + + public static void MapIndicatorColor(IndicatorViewHandler handler, IIndicatorView indicator) + { + handler.NativeView.ResetIndicators(); + } + public static void MapSelectedIndicatorColor(IndicatorViewHandler handler, IIndicatorView indicator) + { + handler.NativeView.ResetIndicators(); + } + public static void MapIndicatorShape(IndicatorViewHandler handler, IIndicatorView indicator) + { + handler.NativeView.ResetIndicators(); + } + } +} diff --git a/src/Core/src/Handlers/IndicatorView/IndicatorViewHandler.Standard.cs b/src/Core/src/Handlers/IndicatorView/IndicatorViewHandler.Standard.cs new file mode 100644 index 000000000000..0d91596a0fac --- /dev/null +++ b/src/Core/src/Handlers/IndicatorView/IndicatorViewHandler.Standard.cs @@ -0,0 +1,18 @@ +using System; + +namespace Microsoft.Maui.Handlers +{ + public partial class IndicatorViewHandler : ViewHandler + { + protected override object CreateNativeView() => throw new NotImplementedException(); + + public static void MapCount(IndicatorViewHandler handler, IIndicatorView indicator) { } + public static void MapPosition(IndicatorViewHandler handler, IIndicatorView indicator) { } + public static void MapHideSingle(IndicatorViewHandler handler, IIndicatorView indicator) { } + public static void MapMaximumVisible(IndicatorViewHandler handler, IIndicatorView indicator) { } + public static void MapIndicatorSize(IndicatorViewHandler handler, IIndicatorView indicator) { } + public static void MapIndicatorColor(IndicatorViewHandler handler, IIndicatorView indicator) { } + public static void MapSelectedIndicatorColor(IndicatorViewHandler handler, IIndicatorView indicator) { } + public static void MapIndicatorShape(IndicatorViewHandler handler, IIndicatorView indicator) { } + } +} diff --git a/src/Core/src/Handlers/IndicatorView/IndicatorViewHandler.Windows.cs b/src/Core/src/Handlers/IndicatorView/IndicatorViewHandler.Windows.cs new file mode 100644 index 000000000000..2bbf8aeb5507 --- /dev/null +++ b/src/Core/src/Handlers/IndicatorView/IndicatorViewHandler.Windows.cs @@ -0,0 +1,19 @@ +using System; +using Microsoft.UI.Xaml.Controls; + +namespace Microsoft.Maui.Handlers +{ + public partial class IndicatorViewHandler : ViewHandler + { + protected override UserControl CreateNativeView() => throw new NotImplementedException(); + + public static void MapCount(IndicatorViewHandler handler, IIndicatorView indicator) { } + public static void MapPosition(IndicatorViewHandler handler, IIndicatorView indicator) { } + public static void MapHideSingle(IndicatorViewHandler handler, IIndicatorView indicator) { } + public static void MapMaximumVisible(IndicatorViewHandler handler, IIndicatorView indicator) { } + public static void MapIndicatorSize(IndicatorViewHandler handler, IIndicatorView indicator) { } + public static void MapIndicatorColor(IndicatorViewHandler handler, IIndicatorView indicator) { } + public static void MapSelectedIndicatorColor(IndicatorViewHandler handler, IIndicatorView indicator) { } + public static void MapIndicatorShape(IndicatorViewHandler handler, IIndicatorView indicator) { } + } +} diff --git a/src/Core/src/Handlers/IndicatorView/IndicatorViewHandler.cs b/src/Core/src/Handlers/IndicatorView/IndicatorViewHandler.cs new file mode 100644 index 000000000000..161fec106011 --- /dev/null +++ b/src/Core/src/Handlers/IndicatorView/IndicatorViewHandler.cs @@ -0,0 +1,26 @@ +#nullable enable +namespace Microsoft.Maui.Handlers +{ + public partial class IndicatorViewHandler + { + public static PropertyMapper IndicatorViewMapper = new(ViewMapper) + { + [nameof(IIndicatorView.Count)] = MapCount, + [nameof(IIndicatorView.Position)] = MapPosition, + [nameof(IIndicatorView.HideSingle)] = MapHideSingle, + [nameof(IIndicatorView.MaximumVisible)] = MapMaximumVisible, + [nameof(IIndicatorView.IndicatorSize)] = MapIndicatorSize, + [nameof(IIndicatorView.IndicatorColor)] = MapIndicatorColor, + [nameof(IIndicatorView.SelectedIndicatorColor)] = MapSelectedIndicatorColor, + [nameof(IIndicatorView.IndicatorsShape)] = MapIndicatorShape + }; + + public IndicatorViewHandler() : base(IndicatorViewMapper) + { + } + + public IndicatorViewHandler(PropertyMapper mapper) : base(mapper ?? IndicatorViewMapper) + { + } + } +} diff --git a/src/Core/src/Handlers/IndicatorView/IndicatorViewHandler.iOS.cs b/src/Core/src/Handlers/IndicatorView/IndicatorViewHandler.iOS.cs new file mode 100644 index 000000000000..4141135240a1 --- /dev/null +++ b/src/Core/src/Handlers/IndicatorView/IndicatorViewHandler.iOS.cs @@ -0,0 +1,86 @@ +using Microsoft.Maui.Platform.iOS; +using UIKit; + +namespace Microsoft.Maui.Handlers +{ + public partial class IndicatorViewHandler : ViewHandler + { + MauiPageControl? UIPager => NativeView as MauiPageControl; + + protected override UIPageControl CreateNativeView() => new MauiPageControl(); + + protected override void ConnectHandler(UIPageControl nativeView) + { + base.ConnectHandler(nativeView); + UIPager?.SetIndicatorView(VirtualView); + UpdateIndicator(); + } + + protected override void DisconnectHandler(UIPageControl nativeView) + { + base.DisconnectHandler(nativeView); + UIPager?.SetIndicatorView(null); + } + + public static void MapCount(IndicatorViewHandler handler, IIndicatorView indicator) + { + handler.UIPager?.UpdateIndicatorCount(); + } + + public static void MapPosition(IndicatorViewHandler handler, IIndicatorView indicator) + { + handler.UIPager?.UpdatePosition(); + } + + public static void MapHideSingle(IndicatorViewHandler handler, IIndicatorView indicator) + { + handler.NativeView?.UpdateHideSingle(indicator); + } + + public static void MapMaximumVisible(IndicatorViewHandler handler, IIndicatorView indicator) + { + handler.UIPager?.UpdateIndicatorCount(); + } + + public static void MapIndicatorSize(IndicatorViewHandler handler, IIndicatorView indicator) + { + handler.UIPager?.UpdateIndicatorSize(indicator); + } + + public static void MapIndicatorColor(IndicatorViewHandler handler, IIndicatorView indicator) + { + handler.NativeView?.UpdatePagesIndicatorTintColor(indicator); + } + + public static void MapSelectedIndicatorColor(IndicatorViewHandler handler, IIndicatorView indicator) + { + handler.NativeView?.UpdateCurrentPagesIndicatorTintColor(indicator); + } + + public static void MapIndicatorShape(IndicatorViewHandler handler, IIndicatorView indicator) + { + handler.UIPager?.UpdateIndicatorShape(indicator); + } + + void UpdateIndicator() + { + if (VirtualView is ITemplatedIndicatorView iTemplatedIndicatorView) + { + var indicatorsLayoutOverride = iTemplatedIndicatorView.IndicatorsLayoutOverride; + UIView? handler; + if (MauiContext != null && indicatorsLayoutOverride != null) + { + ClearIndicators(); + handler = indicatorsLayoutOverride.ToNative(MauiContext); + NativeView.AddSubview(handler); + } + } + + void ClearIndicators() + { + foreach (var child in NativeView.Subviews) + child.RemoveFromSuperview(); + } + } + } +} \ No newline at end of file diff --git a/src/Core/src/Platform/Android/MauiPageControl.cs b/src/Core/src/Platform/Android/MauiPageControl.cs new file mode 100644 index 000000000000..edbd37ffd670 --- /dev/null +++ b/src/Core/src/Platform/Android/MauiPageControl.cs @@ -0,0 +1,202 @@ +using System; +using Android.Content; +using Android.Graphics.Drawables; +using Android.Widget; +using Microsoft.Maui.Graphics; +using AColor = Android.Graphics.Color; +using AShapeDrawable = Android.Graphics.Drawables.ShapeDrawable; +using AShapes = Android.Graphics.Drawables.Shapes; +using AView = Android.Views.View; +using Color = Microsoft.Maui.Graphics.Color; + +namespace Microsoft.Maui.Platform.Android +{ + public class MauiPageControl : LinearLayout + { + const int DefaultPadding = 4; + + Drawable? _currentPageShape; + Drawable? _pageShape; + + IIndicatorView? _indicatorView; + + public MauiPageControl(Context? context) : base(context) + { + } + + public void SetIndicatorView(IIndicatorView? indicatorView) + { + _indicatorView = indicatorView; + if(indicatorView == null) + { + RemoveViews(0); + } + } + + public void ResetIndicators() + { + _pageShape = null; + _currentPageShape = null; + + if ((_indicatorView as ITemplatedIndicatorView)?.IndicatorsLayoutOverride == null) + UpdateShapes(); + else + UpdateIndicatorTemplate((_indicatorView as ITemplatedIndicatorView)?.IndicatorsLayoutOverride); + + UpdatePosition(); + } + + public void UpdatePosition() + { + var index = GetIndexFromPosition(); + var count = ChildCount; + for (int i = 0; i < count; i++) + { + ImageView? view = GetChildAt(i) as ImageView; + if (view == null) + continue; + var drawableToUse = index == i ? _currentPageShape : _pageShape; + if (drawableToUse != view.Drawable) + view.SetImageDrawable(drawableToUse); + } + } + + public void UpdateIndicatorCount() + { + if (_indicatorView == null || Context == null) + return; + + var index = GetIndexFromPosition(); + + var count = _indicatorView.GetMaximumVisible(); + + var childCount = ChildCount; + + for (int i = childCount; i < count; i++) + { + var imageView = new ImageView(Context) + { + Tag = i + }; + + if (Orientation == Orientation.Horizontal) + imageView.SetPadding((int)Context.ToPixels(DefaultPadding), 0, (int)Context.ToPixels(DefaultPadding), 0); + else + imageView.SetPadding(0, (int)Context.ToPixels(DefaultPadding), 0, (int)Context.ToPixels(DefaultPadding)); + + imageView.SetImageDrawable(index == i ? _currentPageShape : _pageShape); + + imageView.SetOnClickListener(new TEditClickListener(view => + { + if (view?.Tag != null) + { + var position = (int)view.Tag; + _indicatorView.Position = position; + } + })); + + AddView(imageView); + } + + RemoveViews(count); + } + + void UpdateIndicatorTemplate(ILayout? layout) + { + if (layout == null || _indicatorView?.Handler?.MauiContext == null) + return; + + AView? handler = layout.ToNative(_indicatorView.Handler.MauiContext); + + RemoveAllViews(); + AddView(handler); + } + + void UpdateShapes() + { + if (_currentPageShape != null || _indicatorView == null) + return; + + var indicatorColor = _indicatorView.IndicatorColor; + + if (indicatorColor is SolidPaint indicatorPaint) + { + if (indicatorPaint.Color is Color c) + _pageShape = GetShape(c.ToNative()); + + } + var indicatorPositionColor = _indicatorView.SelectedIndicatorColor; + if (indicatorPositionColor is SolidPaint indicatorPositionPaint) + { + if (indicatorPositionPaint.Color is Color c) + _currentPageShape = GetShape(c.ToNative()); + } + } + + Drawable? GetShape(AColor color) + { + if (_indicatorView == null || Context == null) + return null; + + AShapeDrawable shape; + + if (_indicatorView.IsCircleShape()) + shape = new AShapeDrawable(new AShapes.OvalShape()); + else + shape = new AShapeDrawable(new AShapes.RectShape()); + + var indicatorSize = _indicatorView.IndicatorSize; + + shape.SetIntrinsicHeight((int)Context.ToPixels(indicatorSize)); + shape.SetIntrinsicWidth((int)Context.ToPixels(indicatorSize)); + + if (shape.Paint != null) + shape.Paint.Color = color; + + return shape; + } + + int GetIndexFromPosition() + { + if (_indicatorView == null) + return 0; + + var maxVisible = _indicatorView.GetMaximumVisible(); + var position = _indicatorView.Position; + return Math.Max(0, position >= maxVisible ? maxVisible - 1 : position); + } + + void RemoveViews(int startAt) + { + for (int i = startAt; i < ChildCount; i++) + { + var imageView = GetChildAt(ChildCount - 1); + imageView?.SetOnClickListener(null); + RemoveView(imageView); + } + } + + class TEditClickListener : Java.Lang.Object, IOnClickListener + { + Action? _command; + + public TEditClickListener(Action command) + { + _command = command; + } + + public void OnClick(AView? v) + { + _command?.Invoke(v); + } + protected override void Dispose(bool disposing) + { + if (disposing) + { + _command = null; + } + base.Dispose(disposing); + } + } + } +} diff --git a/src/Core/src/Platform/iOS/MauiPageControl.cs b/src/Core/src/Platform/iOS/MauiPageControl.cs new file mode 100644 index 000000000000..d2c2d04a77a0 --- /dev/null +++ b/src/Core/src/Platform/iOS/MauiPageControl.cs @@ -0,0 +1,146 @@ +using System; +using CoreGraphics; +using UIKit; + +namespace Microsoft.Maui.Platform.iOS +{ + public class MauiPageControl : UIPageControl + { + const int DefaultIndicatorSize = 6; + + IIndicatorView? _indicatorView; + bool _updatingPosition; + + public MauiPageControl() + { + ValueChanged += MauiPageControlValueChanged; + if (NativeVersion.IsAtLeast(14)) + { + AllowsContinuousInteraction = false; + BackgroundStyle = UIPageControlBackgroundStyle.Minimal; + } + } + + public void SetIndicatorView(IIndicatorView? indicatorView) + { + if (indicatorView == null) + { + ValueChanged -= MauiPageControlValueChanged; + } + _indicatorView = indicatorView; + + } + + public bool IsSquare { get; set; } + + public double IndicatorSize { get; set; } + + protected override void Dispose(bool disposing) + { + if (disposing) + ValueChanged -= MauiPageControlValueChanged; + + base.Dispose(disposing); + } + + + public override void LayoutSubviews() + { + base.LayoutSubviews(); + + if (Subviews.Length == 0) + return; + + UpdateIndicatorSize(); + + if (!IsSquare) + return; + + UpdateSquareShape(); + } + + public void UpdateIndicatorSize() + { + if (IndicatorSize == 0 || IndicatorSize == DefaultIndicatorSize) + return; + + float scale = (float)IndicatorSize / DefaultIndicatorSize; + var newTransform = CGAffineTransform.MakeScale(scale, scale); + + Transform = newTransform; + } + + public void UpdatePosition() + { + _updatingPosition = true; + this.UpdateCurrentPage(GetCurrentPage()); + _updatingPosition = false; + + int GetCurrentPage() + { + if (_indicatorView == null) + return -1; + + var maxVisible = _indicatorView.GetMaximumVisible(); + var position = _indicatorView.Position; + var index = position >= maxVisible ? maxVisible - 1 : position; + return index; + } + } + + public void UpdateIndicatorCount() + { + if (_indicatorView == null) + return; + this.UpdatePages(_indicatorView.GetMaximumVisible()); + UpdatePosition(); + } + + void UpdateSquareShape() + { + if (!NativeVersion.IsAtLeast(14)) + { + UpdateCornerRadius(); + return; + } + + var uiPageControlContentView = Subviews[0]; + if (uiPageControlContentView.Subviews.Length > 0) + { + var uiPageControlIndicatorContentView = uiPageControlContentView.Subviews[0]; + + foreach (var view in uiPageControlIndicatorContentView.Subviews) + { + if (view is UIImageView imageview) + { + imageview.Image = UIImage.GetSystemImage("squareshape.fill"); + var frame = imageview.Frame; + //the square shape is not the same size as the circle so we might need to correct the frame + imageview.Frame = new CGRect(frame.X - 6, frame.Y, frame.Width, frame.Height); + } + } + } + } + + void UpdateCornerRadius() + { + foreach (var view in Subviews) + { + view.Layer.CornerRadius = 0; + } + } + + void MauiPageControlValueChanged(object? sender, System.EventArgs e) + { + if (_updatingPosition || _indicatorView == null) + return; + + _indicatorView.Position = (int)CurrentPage; + //if we are iOS13 or lower and we are using a Square shape + //we need to update the CornerRadius of the new shape. + if (IsSquare && !NativeVersion.IsAtLeast(14)) + LayoutSubviews(); + + } + } +} diff --git a/src/Core/src/Platform/iOS/UIPageControlExtensions.cs b/src/Core/src/Platform/iOS/UIPageControlExtensions.cs new file mode 100644 index 000000000000..e7813e2deba7 --- /dev/null +++ b/src/Core/src/Platform/iOS/UIPageControlExtensions.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.Maui.Graphics; +using UIKit; + +namespace Microsoft.Maui.Platform.iOS +{ + public static class UIPageControlExtensions + { + public static void UpdateIndicatorShape(this MauiPageControl pageControl, IIndicatorView indicatorView) + { + pageControl.IsSquare = !indicatorView.IsCircleShape(); + + pageControl.LayoutSubviews(); + } + + public static void UpdateIndicatorSize(this MauiPageControl pageControl, IIndicatorView indicatorView) + { + pageControl.IndicatorSize = indicatorView.IndicatorSize; + pageControl.LayoutSubviews(); + } + + public static void UpdateHideSingle(this UIPageControl pageControl, IIndicatorView indicatorView) + => pageControl.HidesForSinglePage = indicatorView.HideSingle; + + public static void UpdateCurrentPage(this UIPageControl pageControl, int currentPage) + => pageControl.CurrentPage = currentPage; + + public static void UpdatePages(this UIPageControl pageControl, int pageCount) + => pageControl.Pages = pageCount; + + public static void UpdatePagesIndicatorTintColor(this UIPageControl pageControl, IIndicatorView indicatorView) + => pageControl.PageIndicatorTintColor = indicatorView.IndicatorColor?.ToColor()?.ToNative(); + + public static void UpdateCurrentPagesIndicatorTintColor(this UIPageControl pageControl, IIndicatorView indicatorView) + => pageControl.CurrentPageIndicatorTintColor = indicatorView.SelectedIndicatorColor?.ToColor()?.ToNative(); + } +}