From fb2d6f500fa0c7ae7a5542c3c51e5c61376748bf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:13:07 +0000 Subject: [PATCH 1/3] Initial plan From dba5b884acecf46c8773c00da090189358e81612 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:29:31 +0000 Subject: [PATCH 2/3] Improve layout performance across components - AlertView: Skip redundant OnSizeAllocated, use Grid.SetRow/SetColumn instead of Remove/Add - ListItem: Create dividers once and toggle IsVisible instead of recreating on property changes - ListItem: Add InputTransparent to decorative icon and title/subtitle container - NavigationListItem: Add InputTransparent to decorative arrow icon - BottomSheetHeader: Add InputTransparent to decorative back button image - AlertView: Add InputTransparent to decorative icon - Tab (iOS): Flatten nested VerticalStackLayout + HorizontalStackLayout to single Grid Co-authored-by: Vetle444 <35739538+Vetle444@users.noreply.github.com> --- .../Components/Alerting/Alert/AlertView.cs | 81 +++++++++++++------ .../BottomSheets/Header/BottomSheetHeader.cs | 3 +- .../Extensions/NavigationListItem.cs | 1 + .../ListItems/ListItem.Properties.cs | 4 +- .../Components/ListItems/ListItem.cs | 35 +++++--- .../DIPS.Mobile.UI/Components/Tabs/iOS/Tab.cs | 37 ++++++--- 6 files changed, 111 insertions(+), 50 deletions(-) diff --git a/src/library/DIPS.Mobile.UI/Components/Alerting/Alert/AlertView.cs b/src/library/DIPS.Mobile.UI/Components/Alerting/Alert/AlertView.cs index bdbd5990a..6cb808b50 100644 --- a/src/library/DIPS.Mobile.UI/Components/Alerting/Alert/AlertView.cs +++ b/src/library/DIPS.Mobile.UI/Components/Alerting/Alert/AlertView.cs @@ -30,6 +30,11 @@ public partial class AlertView : Grid private Image? m_icon; private ImageButton? m_closeIcon; private CustomTruncationTextView? m_titleAndDescriptionLabel; + + private double m_lastAllocatedWidth = -1; + private bool m_buttonsContainerAdded; + private int m_lastButtonColumn = -1; + private int m_lastButtonRow = -1; public AlertView() { @@ -71,6 +76,10 @@ protected override void OnSizeAllocated(double width, double height) { base.OnSizeAllocated(width, height); + if (Math.Abs(width - m_lastAllocatedWidth) < 0.001) + return; + + m_lastAllocatedWidth = width; UpdateButtonAlignment(); } @@ -79,38 +88,59 @@ private void UpdateButtonAlignment() if (LeftButtonCommand is null && RightButtonCommand is null) return; - Remove(m_buttonsContainer); + int targetColumn; + int targetRow; if (IsLargeAlert) { m_buttonsContainer.Margin = new Thickness(0, Sizes.GetSize(SizeName.content_margin_small), 0, 0); - this.Add(m_buttonsContainer, 1, 1); - return; + m_buttonsContainer.HorizontalOptions = LayoutOptions.Start; + targetColumn = 1; + targetRow = 1; + } + else + { + // Temporarily hide the buttons container so we can measure the alert without it + var wasVisible = m_buttonsContainer.IsVisible; + m_buttonsContainer.IsVisible = false; + + var maxWidth = Measure(int.MaxValue, int.MaxValue).Width; + var buttonsWidth = m_buttonsContainer.Measure(int.MaxValue, int.MaxValue).Width; + var remainingWidth = Width - maxWidth - buttonsWidth; + + m_buttonsContainer.IsVisible = wasVisible; + + var buttonsWillFit = remainingWidth >= (Sizes.GetSize(SizeName.content_margin_small)); + + if (buttonsWillFit) + { + m_buttonsContainer.Margin = new Thickness(0, 0, 0, 0); + m_buttonsContainer.HorizontalOptions = LayoutOptions.End; + targetColumn = 2; + targetRow = 0; + } + else + { + m_buttonsContainer.Margin = new Thickness(0, Sizes.GetSize(SizeName.content_margin_small), 0, 0); + m_buttonsContainer.HorizontalOptions = LayoutOptions.Start; + targetColumn = 1; + targetRow = 1; + } } - var maxWidth = Measure(int.MaxValue, int.MaxValue).Width; - var buttonsWidth = m_buttonsContainer.Measure(int.MaxValue, int.MaxValue).Width; - var remainingWidth = Width - maxWidth - buttonsWidth; - - var buttonsWillFit = remainingWidth >= (Sizes.GetSize(SizeName.content_margin_small)); - - if (buttonsWillFit) + if (!m_buttonsContainerAdded) { - m_buttonsContainer.Margin = new Thickness(0, 0, 0, 0); - m_buttonsContainer.HorizontalOptions = LayoutOptions.End; - this.Add(m_buttonsContainer, 2); + this.Add(m_buttonsContainer, targetColumn, targetRow); + m_buttonsContainerAdded = true; } - else + else if (targetColumn != m_lastButtonColumn || targetRow != m_lastButtonRow) { - m_buttonsContainer.Margin = new Thickness(0, Sizes.GetSize(SizeName.content_margin_small), 0, 0); - m_buttonsContainer.HorizontalOptions = LayoutOptions.Start; - this.Add(m_buttonsContainer, 1, 1); + Grid.SetColumn(m_buttonsContainer, targetColumn); + Grid.SetRow(m_buttonsContainer, targetRow); } - -#if __ANDROID__ - // Workaround for Android layout issue where buttons is not visible in some cases - InvalidateMeasure(); -#endif + + m_lastButtonColumn = targetColumn; + m_lastButtonRow = targetRow; } private void OnButtonChanged() @@ -132,6 +162,8 @@ private void OnButtonChanged() m_buttonsContainer.Add(CreateButton(RightButtonText, RightButtonCommand, RightButtonCommandParameter, "RightButton".ToDUIAutomationId())); } + // Reset width tracking to force recalculation of button alignment + m_lastAllocatedWidth = -1; UpdateButtonAlignment(); UpdateAccessibility(); } @@ -206,6 +238,8 @@ private void OnTitleOrDescriptionChanged() this.Add(m_titleAndDescriptionLabel, 1); + // Reset width tracking to force recalculation of button alignment + m_lastAllocatedWidth = -1; UpdateButtonAlignment(); UpdateAccessibility(); } @@ -317,7 +351,8 @@ private void OnIconChanged() HeightRequest = Sizes.GetSize(SizeName.size_6), WidthRequest = Sizes.GetSize(SizeName.size_6), VerticalOptions = IsLargeAlert ? LayoutOptions.Start : LayoutOptions.Center, - Source = Icon + Source = Icon, + InputTransparent = true }; // Hide icon from screen readers since alert type is already announced diff --git a/src/library/DIPS.Mobile.UI/Components/BottomSheets/Header/BottomSheetHeader.cs b/src/library/DIPS.Mobile.UI/Components/BottomSheets/Header/BottomSheetHeader.cs index 886354515..5271ce2ab 100644 --- a/src/library/DIPS.Mobile.UI/Components/BottomSheets/Header/BottomSheetHeader.cs +++ b/src/library/DIPS.Mobile.UI/Components/BottomSheets/Header/BottomSheetHeader.cs @@ -77,7 +77,8 @@ private void AddBackButtonAndTitleLabel() Source = Icons.GetIcon(IconName.chevron_left_line), WidthRequest = Sizes.GetSize(SizeName.size_4), HeightRequest = Sizes.GetSize(SizeName.size_4), - Margin = new Thickness(0, 0, Sizes.GetSize(SizeName.content_margin_medium), 0) + Margin = new Thickness(0, 0, Sizes.GetSize(SizeName.content_margin_medium), 0), + InputTransparent = true }; var titleLabel = new Label diff --git a/src/library/DIPS.Mobile.UI/Components/ListItems/Extensions/NavigationListItem.cs b/src/library/DIPS.Mobile.UI/Components/ListItems/Extensions/NavigationListItem.cs index dac72012c..b7a603029 100644 --- a/src/library/DIPS.Mobile.UI/Components/ListItems/Extensions/NavigationListItem.cs +++ b/src/library/DIPS.Mobile.UI/Components/ListItems/Extensions/NavigationListItem.cs @@ -28,6 +28,7 @@ public NavigationListItem() VerticalOptions = LayoutOptions.Center, HorizontalOptions = LayoutOptions.End, TintColor = Colors.GetColor(ColorName.color_icon_subtle), + InputTransparent = true }, 1); TitleOptions = new TitleOptions() diff --git a/src/library/DIPS.Mobile.UI/Components/ListItems/ListItem.Properties.cs b/src/library/DIPS.Mobile.UI/Components/ListItems/ListItem.Properties.cs index a8544d963..de3548411 100644 --- a/src/library/DIPS.Mobile.UI/Components/ListItems/ListItem.Properties.cs +++ b/src/library/DIPS.Mobile.UI/Components/ListItems/ListItem.Properties.cs @@ -219,13 +219,13 @@ public bool DisableInternalAccessibility nameof(HasTopDivider), typeof(bool), typeof(ListItem), - propertyChanged: ((bindable, _, _) => ((ListItem)bindable).AddDivider(true))); + propertyChanged: ((bindable, _, _) => ((ListItem)bindable).UpdateDivider(true))); public static readonly BindableProperty HasBottomDividerProperty = BindableProperty.Create( nameof(HasBottomDivider), typeof(bool), typeof(ListItem), - propertyChanged: ((bindable, _, _) => ((ListItem)bindable).AddDivider(false))); + propertyChanged: ((bindable, _, _) => ((ListItem)bindable).UpdateDivider(false))); public static readonly BindableProperty CommandParameterProperty = BindableProperty.Create( nameof(CommandParameter), diff --git a/src/library/DIPS.Mobile.UI/Components/ListItems/ListItem.cs b/src/library/DIPS.Mobile.UI/Components/ListItems/ListItem.cs index e18e47281..a22a16e31 100644 --- a/src/library/DIPS.Mobile.UI/Components/ListItems/ListItem.cs +++ b/src/library/DIPS.Mobile.UI/Components/ListItems/ListItem.cs @@ -29,6 +29,7 @@ public partial class ListItem : Grid { AutomationId = "TitleAndSubtitleContainer".ToDUIAutomationId(), VerticalOptions = LayoutOptions.Center, + InputTransparent = true, RowDefinitions = new RowDefinitionCollection(new RowDefinition(GridLength.Star), new RowDefinition(GridLength.Auto)) }; @@ -94,7 +95,7 @@ private void AddIcon() if (ImageIcon is not null) return; - ImageIcon = new Image(); + ImageIcon = new Image { InputTransparent = true }; ImageIcon.SetBinding(Microsoft.Maui.Controls.Image.SourceProperty, static (ListItem listItem) => listItem.Icon, source: this); SetDefaultValuesOrBindToOptions(IconOptions, () => @@ -142,25 +143,31 @@ private void AddUnderlyingContent() m_oldUnderlyingContent = UnderlyingContent; } - private void AddDivider(bool top) + private void UpdateDivider(bool top) { - var divider = new Divider(); + var shouldShow = top ? HasTopDivider : HasBottomDivider; + if (top) { - if (Contains(TopDivider)) - Remove(TopDivider); - - TopDivider = divider; - TopDivider.VerticalOptions = LayoutOptions.Start; + if (TopDivider is null) + { + TopDivider = CreateAndAddDivider(LayoutOptions.Start); + } + TopDivider.IsVisible = shouldShow; } else { - if (Contains(BottomDivider)) - Remove(BottomDivider); - - BottomDivider = divider; - BottomDivider.VerticalOptions = LayoutOptions.End; + if (BottomDivider is null) + { + BottomDivider = CreateAndAddDivider(LayoutOptions.End); + } + BottomDivider.IsVisible = shouldShow; } + } + + private Divider CreateAndAddDivider(LayoutOptions verticalOptions) + { + var divider = new Divider { VerticalOptions = verticalOptions }; this.SetRowSpan(divider, RowDefinitions.Count); this.SetColumnSpan(divider, ColumnDefinitions.Count); @@ -171,6 +178,8 @@ private void AddDivider(bool top) { Options.Dividers.DividersOptions.SetupDefaults(this); }); + + return divider; } private void AddTouch() diff --git a/src/library/DIPS.Mobile.UI/Components/Tabs/iOS/Tab.cs b/src/library/DIPS.Mobile.UI/Components/Tabs/iOS/Tab.cs index 0d077db06..a9bb79868 100644 --- a/src/library/DIPS.Mobile.UI/Components/Tabs/iOS/Tab.cs +++ b/src/library/DIPS.Mobile.UI/Components/Tabs/iOS/Tab.cs @@ -1,7 +1,6 @@ using DIPS.Mobile.UI.Converters.ValueConverters; using DIPS.Mobile.UI.Effects.Touch; using Colors = DIPS.Mobile.UI.Resources.Colors.Colors; -using VerticalStackLayout = DIPS.Mobile.UI.Components.Lists.VerticalStackLayout; namespace DIPS.Mobile.UI.Components.Tabs { @@ -18,19 +17,34 @@ public Tab() internal void ConstructView() { - var container = new VerticalStackLayout(); - var titleContainer = new HorizontalStackLayout() + // Single Grid replaces nested VerticalStackLayout + HorizontalStackLayout + var container = new Grid { - Spacing = Sizes.GetSize(SizeName.size_1), - HorizontalOptions = LayoutOptions.Center, - Padding = new Thickness(Sizes.GetSize(SizeName.size_3), Sizes.GetSize(SizeName.size_4), Sizes.GetSize(SizeName.size_3), Sizes.GetSize(SizeName.size_1)) + ColumnDefinitions = + [ + new ColumnDefinition(GridLength.Star), + new ColumnDefinition(GridLength.Auto), + new ColumnDefinition(GridLength.Auto), + new ColumnDefinition(GridLength.Star) + ], + RowDefinitions = + [ + new RowDefinition(GridLength.Star), + new RowDefinition(GridLength.Auto) + ] }; + m_titleLabel = CreateTitleLabel(); m_counterLabel = CreateCounterLabel(); - - titleContainer.Add(m_titleLabel); - titleContainer.Add(m_counterLabel); - container.Add(titleContainer); + + // Replicate original HorizontalStackLayout padding (size_3, size_4, size_3, size_1) + // and VerticalStackLayout spacing (content_margin_xsmall) as margins + var bottomMargin = Sizes.GetSize(SizeName.size_1) + Sizes.GetSize(SizeName.content_margin_xsmall); + m_titleLabel.Margin = new Thickness(0, Sizes.GetSize(SizeName.size_4), 0, bottomMargin); + m_counterLabel.Margin = new Thickness(Sizes.GetSize(SizeName.size_1), Sizes.GetSize(SizeName.size_4), 0, bottomMargin); + + container.Add(m_titleLabel, 1, 0); + container.Add(m_counterLabel, 2, 0); var boxView = new BoxView { @@ -39,7 +53,8 @@ internal void ConstructView() BackgroundColor = Colors.GetColor(ColorName.color_border_button_active) }; boxView.SetBinding(IsVisibleProperty, static (Tab tab) => tab.IsSelected, source: this); - container.Add(boxView); + container.Add(boxView, 0, 1); + Grid.SetColumnSpan(boxView, 4); m_border = new Border { From 70a3a6a967c7fa476529a45a5455f1c934414d61 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:31:08 +0000 Subject: [PATCH 3/3] Address code review comments: improve documentation and clarity Co-authored-by: Vetle444 <35739538+Vetle444@users.noreply.github.com> --- CHANGELOG.md | 6 ++++++ .../DIPS.Mobile.UI/Components/Alerting/Alert/AlertView.cs | 5 +++-- src/library/DIPS.Mobile.UI/Components/Tabs/iOS/Tab.cs | 6 ++++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 566fc8355..f57f8a569 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [55.3.0] +- [AlertView] Improved layout performance by avoiding redundant measure and Remove/Add cycles on every size change. +- [ListItem] Improved layout performance by reusing dividers with visibility toggling instead of recreating them on property changes. +- [Tab][iOS] Improved layout performance by flattening nested StackLayouts into a single Grid. +- [ListItem][AlertView][NavigationListItem][BottomSheetHeader] Added InputTransparent to decorative elements to reduce hit-testing overhead. + ## [55.2.2] - [iOS26][Tip] Added more padding. diff --git a/src/library/DIPS.Mobile.UI/Components/Alerting/Alert/AlertView.cs b/src/library/DIPS.Mobile.UI/Components/Alerting/Alert/AlertView.cs index 6cb808b50..1c33ea141 100644 --- a/src/library/DIPS.Mobile.UI/Components/Alerting/Alert/AlertView.cs +++ b/src/library/DIPS.Mobile.UI/Components/Alerting/Alert/AlertView.cs @@ -76,7 +76,8 @@ protected override void OnSizeAllocated(double width, double height) { base.OnSizeAllocated(width, height); - if (Math.Abs(width - m_lastAllocatedWidth) < 0.001) + const double widthChangeTolerance = 0.001; + if (Math.Abs(width - m_lastAllocatedWidth) < widthChangeTolerance) return; m_lastAllocatedWidth = width; @@ -100,7 +101,7 @@ private void UpdateButtonAlignment() } else { - // Temporarily hide the buttons container so we can measure the alert without it + // Temporarily hide buttons so they don't affect the alert width measurement var wasVisible = m_buttonsContainer.IsVisible; m_buttonsContainer.IsVisible = false; diff --git a/src/library/DIPS.Mobile.UI/Components/Tabs/iOS/Tab.cs b/src/library/DIPS.Mobile.UI/Components/Tabs/iOS/Tab.cs index a9bb79868..43d2ca805 100644 --- a/src/library/DIPS.Mobile.UI/Components/Tabs/iOS/Tab.cs +++ b/src/library/DIPS.Mobile.UI/Components/Tabs/iOS/Tab.cs @@ -37,9 +37,11 @@ internal void ConstructView() m_titleLabel = CreateTitleLabel(); m_counterLabel = CreateCounterLabel(); - // Replicate original HorizontalStackLayout padding (size_3, size_4, size_3, size_1) - // and VerticalStackLayout spacing (content_margin_xsmall) as margins + // Bottom margin combines the original HorizontalStackLayout's bottom padding (size_1) + // with VerticalStackLayout's inter-child spacing (content_margin_xsmall) var bottomMargin = Sizes.GetSize(SizeName.size_1) + Sizes.GetSize(SizeName.content_margin_xsmall); + // Top margin replicates the original HorizontalStackLayout's top padding (size_4) + // Counter left margin replicates the original HorizontalStackLayout's Spacing (size_1) m_titleLabel.Margin = new Thickness(0, Sizes.GetSize(SizeName.size_4), 0, bottomMargin); m_counterLabel.Margin = new Thickness(Sizes.GetSize(SizeName.size_1), Sizes.GetSize(SizeName.size_4), 0, bottomMargin);