diff --git a/src/Controls/samples/Controls.Sample/Pages/Controls/IndicatorPage.xaml b/src/Controls/samples/Controls.Sample/Pages/Controls/IndicatorPage.xaml index 92dce624ad2d..5b9ae2c63a9c 100644 --- a/src/Controls/samples/Controls.Sample/Pages/Controls/IndicatorPage.xaml +++ b/src/Controls/samples/Controls.Sample/Pages/Controls/IndicatorPage.xaml @@ -121,7 +121,14 @@ x:Name="indicatorView" IndicatorColor="LightGray" SelectedIndicatorColor="DarkGray" - HorizontalOptions="Center" /> + HorizontalOptions="Center"> + + + + + diff --git a/src/Controls/src/Core/IndicatorView/IndicatorStackLayout.cs b/src/Controls/src/Core/IndicatorView/IndicatorStackLayout.cs index 296e2816dceb..1268984f95ae 100644 --- a/src/Controls/src/Core/IndicatorView/IndicatorStackLayout.cs +++ b/src/Controls/src/Core/IndicatorView/IndicatorStackLayout.cs @@ -1,23 +1,35 @@ -#nullable disable -using System; -using System.Collections; -using System.Collections.Specialized; using System.ComponentModel; +using Microsoft.Maui.Controls.Shapes; using Microsoft.Maui.Graphics; namespace Microsoft.Maui.Controls { internal class IndicatorStackLayout : StackLayout { - IndicatorView _indicatorView; + readonly IndicatorView _indicatorView; public IndicatorStackLayout(IndicatorView indicatorView) { _indicatorView = indicatorView; Orientation = StackOrientation.Horizontal; - _indicatorView.PropertyChanged += _indicatorViewPropertyChanged; + _indicatorView.PropertyChanged += IndicatorViewPropertyChanged; } - void _indicatorViewPropertyChanged(object sender, PropertyChangedEventArgs e) + protected override void OnChildAdded(Element child) + { + base.OnChildAdded(child); + + if (child is View view) + { + var tapGestureRecognizer = new TapGestureRecognizer + { + Command = new Command(sender => _indicatorView.Position = Children.IndexOf(sender)), + CommandParameter = view + }; + view.GestureRecognizers.Add(tapGestureRecognizer); + } + } + + void IndicatorViewPropertyChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName == IndicatorView.IndicatorsShapeProperty.PropertyName || e.PropertyName == IndicatorView.IndicatorTemplateProperty.PropertyName) @@ -53,8 +65,7 @@ internal void ResetIndicators() try { BatchBegin(); - Children.Clear(); - AddExtraIndicatorItems(); + BindIndicatorItems(); } finally { @@ -75,11 +86,10 @@ internal void ResetIndicatorCount(int oldCount) if (oldCount > _indicatorView.Count) { - RemoveRedundantIndicatorItems(); return; } - AddExtraIndicatorItems(); + BindIndicatorItems(); } finally { @@ -100,6 +110,10 @@ void ResetIndicatorStylesNonBatch() var selectedIndex = position >= maxVisible ? maxVisible - 1 : position; bool isSelected = index == selectedIndex; var visualElement = Children[index] as VisualElement; + if (visualElement is null) + { + return; + } visualElement.BackgroundColor = isSelected ? GetColorOrDefault(_indicatorView.SelectedIndicatorColor, Colors.Gray) @@ -115,49 +129,34 @@ void ResetIndicatorStylesNonBatch() IsVisible = indicatorCount > 1 || !_indicatorView.HideSingle; } - Color GetColorOrDefault(Color color, Color defaultColor) => color ?? defaultColor; + Color GetColorOrDefault(Color? color, Color defaultColor) => color ?? defaultColor; - void AddExtraIndicatorItems() + void BindIndicatorItems() { - var indicatorCount = _indicatorView.Count; - var indicatorMaximumVisible = _indicatorView.MaximumVisible; - var indicatorSize = _indicatorView.IndicatorSize; - var indicatorTemplate = _indicatorView.IndicatorTemplate; - - var oldCount = Children.Count; - for (var i = 0; i < indicatorCount - oldCount && i < indicatorMaximumVisible - oldCount; i++) + var indicatorSize = _indicatorView.IndicatorSize > 0 ? _indicatorView.IndicatorSize : 10; + var indicatorTemplate = _indicatorView.IndicatorTemplate ??= new DataTemplate(() => new Border { - var size = indicatorSize > 0 ? indicatorSize : 10; - var indicator = indicatorTemplate?.CreateContent() as View ?? new Frame + Padding = 0, + VerticalOptions = LayoutOptions.Center, + HorizontalOptions = LayoutOptions.Center, + WidthRequest = indicatorSize, + HeightRequest = indicatorSize, + StrokeShape = new RoundRectangle() { - Padding = 0, - HasShadow = false, - BorderColor = Colors.Transparent, - VerticalOptions = LayoutOptions.Center, - HorizontalOptions = LayoutOptions.Center, - WidthRequest = size, - HeightRequest = size, - CornerRadius = _indicatorView.IndicatorsShape == IndicatorShape.Circle ? (float)size / 2 : 0 - }; - var tapGestureRecognizer = new TapGestureRecognizer(); - tapGestureRecognizer.Tapped += (sender, args) => _indicatorView.Position = Children.IndexOf(sender as View); - indicator.GestureRecognizers.Add(tapGestureRecognizer); - Children.Add(indicator); - } - } - - void RemoveRedundantIndicatorItems() - { - var indicatorCount = _indicatorView.Count; - while (Children.Count > indicatorCount) - { - Children.RemoveAt(0); - } + CornerRadius = _indicatorView.IndicatorsShape == IndicatorShape.Circle + ? (float)indicatorSize / 2 + : 0, + Stroke = Colors.Transparent + } + }); + + BindableLayout.SetItemsSource(this, _indicatorView.ItemsSource); + BindableLayout.SetItemTemplate(this, indicatorTemplate); } public void Remove() { - _indicatorView.PropertyChanged -= _indicatorViewPropertyChanged; + _indicatorView.PropertyChanged -= IndicatorViewPropertyChanged; } } -} \ No newline at end of file +} diff --git a/src/Controls/tests/Core.UnitTests/IndicatorViewLayoutTests.cs b/src/Controls/tests/Core.UnitTests/IndicatorViewLayoutTests.cs new file mode 100644 index 000000000000..e75d2b114be4 --- /dev/null +++ b/src/Controls/tests/Core.UnitTests/IndicatorViewLayoutTests.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using Xunit; + +namespace Microsoft.Maui.Controls.Core.UnitTests +{ + public class IndicatorViewTests : BaseTestFixture + { + [Fact] + public void IndicatorStackLayoutNoItems_ResetIndicators_ShouldHaveNoChildren() + { + // Arrange + var indicatorView = new IndicatorView(); + var indicatorStackLayout = new IndicatorStackLayout(indicatorView); + + // Act + indicatorStackLayout.ResetIndicators(); + + // Assert + Assert.Empty(indicatorStackLayout.Children); + } + + [Fact] + public void IndicatorStackLayoutWithItems_ResetIndicators_ShouldBindChildren() + { + // Arrange + var indicatorView = new IndicatorView() { ItemsSource = new List{"item1", "item2"} }; + var indicatorStackLayout = new IndicatorStackLayout(indicatorView); + + // Act + indicatorStackLayout.ResetIndicators(); + + // Assert + Assert.Equal(2, indicatorStackLayout.Children.Count); + } + + [Theory] + [InlineData(1, 2)] + [InlineData(0, 2)] + [InlineData(-2, 2)] + public void IndicatorStackLayout_ResetIndicatorCount_ShouldBindChildren(int oldCount, int expected) + { + // Arrange + var indicatorView = new IndicatorView() { ItemsSource = new List{"item1", "item2"} }; + var indicatorStackLayout = new IndicatorStackLayout(indicatorView); + Assert.Empty(indicatorStackLayout.Children); + + // Act + indicatorStackLayout.ResetIndicatorCount(oldCount); + + // Assert + Assert.Equal(expected, indicatorStackLayout.Children.Count); + } + } +}