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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Item 1
+ Item 2
+ Item 3
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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();
+ }
+}