Skip to content

Commit 89d1d89

Browse files
Copilothaavamoa
andcommitted
Rewrite Toolbar to use native platform components via handler pattern
Co-authored-by: haavamoa <2527084+haavamoa@users.noreply.github.com>
1 parent 12952db commit 89d1d89

6 files changed

Lines changed: 220 additions & 65 deletions

File tree

src/library/DIPS.Mobile.UI/API/Builder/AppHostBuilderExtensions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using DIPS.Mobile.UI.Components.Navigation.FloatingNavigationButton;
1010
using DIPS.Mobile.UI.Components.PanZoomContainer;
1111
using DIPS.Mobile.UI.Components.Pickers.ScrollPicker;
12+
using DIPS.Mobile.UI.Components.Toolbar;
1213
using DIPS.Mobile.UI.Effects.Animation.Effects;
1314
using DIPS.Mobile.UI.Effects.Touch;
1415
using DotNet.Meteor.HotReload.Plugin;
@@ -73,6 +74,7 @@ public static MauiAppBuilder UseDIPSUI(
7374
handlers.AddHandler<PanZoomContainer, PanZoomContainerHandler>();
7475
handlers.AddHandler<Components.Shell.Shell, ShellRenderer>();
7576
handlers.AddHandler<PreviewView, PreviewViewHandler>();
77+
handlers.AddHandler<Toolbar, ToolbarHandler>();
7678

7779
AddPlatformHandlers(handlers);
7880
});
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
using Android.Content.Res;
2+
using Android.Views;
3+
using Android.Widget;
4+
using DIPS.Mobile.UI.API.Library;
5+
using Google.Android.Material.Button;
6+
using Microsoft.Maui.Handlers;
7+
using Microsoft.Maui.Platform;
8+
9+
namespace DIPS.Mobile.UI.Components.Toolbar;
10+
11+
public partial class ToolbarHandler : ViewHandler<Toolbar, LinearLayout>
12+
{
13+
private LinearLayout? m_buttonsLayout;
14+
private readonly List<(MaterialButton Button, EventHandler Handler)> m_buttonHandlers = [];
15+
16+
protected override LinearLayout CreatePlatformView()
17+
{
18+
// Outer vertical layout: [top border row] + [buttons row]
19+
var outer = new LinearLayout(Context)
20+
{
21+
Orientation = Orientation.Vertical,
22+
};
23+
outer.SetBackgroundColor(Resources.Colors.Colors.GetColor(ColorName.color_surface_default).ToPlatform());
24+
25+
// Top border
26+
var density = Context?.Resources?.DisplayMetrics?.Density ?? 1f;
27+
var borderPx = (int)(Sizes.GetSize(SizeName.stroke_small) * density);
28+
var border = new View(Context);
29+
border.SetBackgroundColor(Resources.Colors.Colors.GetColor(ColorName.color_border_default).ToPlatform());
30+
outer.AddView(border, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MatchParent, borderPx));
31+
32+
// Buttons row
33+
m_buttonsLayout = new LinearLayout(Context)
34+
{
35+
Orientation = Orientation.Horizontal,
36+
};
37+
outer.AddView(m_buttonsLayout, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MatchParent, ViewGroup.LayoutParams.WrapContent));
38+
39+
return outer;
40+
}
41+
42+
protected override void ConnectHandler(LinearLayout platformView)
43+
{
44+
base.ConnectHandler(platformView);
45+
UpdateButtons();
46+
}
47+
48+
protected override void DisconnectHandler(LinearLayout platformView)
49+
{
50+
UnsubscribeAllButtonHandlers();
51+
base.DisconnectHandler(platformView);
52+
}
53+
54+
private static partial void MapButtons(ToolbarHandler handler, Toolbar toolbar)
55+
{
56+
handler.UpdateButtons();
57+
}
58+
59+
private void UpdateButtons()
60+
{
61+
UnsubscribeAllButtonHandlers();
62+
m_buttonsLayout?.RemoveAllViews();
63+
64+
if (VirtualView.Buttons is null || m_buttonsLayout is null)
65+
return;
66+
67+
foreach (var toolbarButton in VirtualView.Buttons)
68+
{
69+
var (button, handler) = CreateMaterialIconButton(toolbarButton);
70+
var layoutParams = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WrapContent, 1f);
71+
m_buttonsLayout.AddView(button, layoutParams);
72+
m_buttonHandlers.Add((button, handler));
73+
}
74+
}
75+
76+
private void UnsubscribeAllButtonHandlers()
77+
{
78+
foreach (var (button, handler) in m_buttonHandlers)
79+
{
80+
button.Click -= handler;
81+
}
82+
m_buttonHandlers.Clear();
83+
}
84+
85+
private (MaterialButton Button, EventHandler Handler) CreateMaterialIconButton(ToolbarButton toolbarButton)
86+
{
87+
var iconColor = Resources.Colors.Colors.GetColor(ColorName.color_icon_action).ToPlatform();
88+
var iconColorStateList = new ColorStateList(
89+
[[Android.Resource.Attribute.StateEnabled], [-Android.Resource.Attribute.StateEnabled]],
90+
[iconColor, Resources.Colors.Colors.GetColor(ColorName.color_icon_disabled).ToPlatform()]);
91+
92+
var button = new MaterialButton(Context!, null, Resource.Attribute.materialIconButtonStyle)
93+
{
94+
IconGravity = MaterialButton.IconGravityTextTop,
95+
IconTint = iconColorStateList,
96+
SoundEffectsEnabled = false,
97+
};
98+
99+
button.Enabled = toolbarButton.IsEnabled;
100+
101+
if (!string.IsNullOrEmpty(toolbarButton.Title))
102+
{
103+
button.ContentDescription = toolbarButton.Title;
104+
}
105+
106+
if (toolbarButton.Icon is not null &&
107+
DUI.TryGetDrawableFromFileImageSource(toolbarButton.Icon, out var drawable) &&
108+
drawable is not null)
109+
{
110+
button.Icon = drawable;
111+
}
112+
113+
void OnClick(object? sender, EventArgs e) =>
114+
toolbarButton.Command?.Execute(toolbarButton.CommandParameter);
115+
116+
button.Click += OnClick;
117+
118+
return (button, OnClick);
119+
}
120+
}
Lines changed: 2 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,15 @@
1-
using DIPS.Mobile.UI.Components.Buttons;
2-
using DIPS.Mobile.UI.Components.Dividers;
3-
using DIPS.Mobile.UI.Resources.Styles;
4-
using DIPS.Mobile.UI.Resources.Styles.Button;
5-
61
namespace DIPS.Mobile.UI.Components.Toolbar;
72

83
/// <summary>
9-
/// A cross-platform toolbar component that displays a horizontal bar of icon buttons.
4+
/// A cross-platform toolbar component that displays a horizontal bar of icon buttons using native platform views.
105
/// </summary>
116
/// <remarks>
127
/// iOS: https://developer.apple.com/design/human-interface-guidelines/toolbars
138
/// Android: https://m3.material.io/components/toolbars/overview
149
/// </remarks>
1510
[ContentProperty(nameof(Buttons))]
16-
public partial class Toolbar : ContentView
11+
public partial class Toolbar : View
1712
{
18-
private readonly Grid m_buttonsGrid = new();
19-
20-
public Toolbar()
21-
{
22-
this.SetAppThemeColor(BackgroundColorProperty, ColorName.color_surface_default);
23-
24-
var topBorder = new Divider
25-
{
26-
HeightRequest = Sizes.GetSize(SizeName.stroke_small)
27-
};
28-
29-
Content = new VerticalStackLayout
30-
{
31-
Spacing = 0,
32-
Children = { topBorder, m_buttonsGrid }
33-
};
34-
}
35-
36-
private void OnButtonsChanged()
37-
{
38-
m_buttonsGrid.ColumnDefinitions.Clear();
39-
m_buttonsGrid.Children.Clear();
40-
41-
if (Buttons is null || Buttons.Count == 0)
42-
return;
43-
44-
for (var i = 0; i < Buttons.Count; i++)
45-
{
46-
m_buttonsGrid.ColumnDefinitions.Add(new ColumnDefinition(GridLength.Star));
47-
var toolbarButton = Buttons[i];
48-
toolbarButton.BindingContext = BindingContext;
49-
var buttonView = CreateButtonView(toolbarButton);
50-
Grid.SetColumn(buttonView, i);
51-
m_buttonsGrid.Add(buttonView);
52-
}
53-
}
54-
5513
protected override void OnBindingContextChanged()
5614
{
5715
base.OnBindingContextChanged();
@@ -64,25 +22,4 @@ protected override void OnBindingContextChanged()
6422
toolbarButton.BindingContext = BindingContext;
6523
}
6624
}
67-
68-
private static View CreateButtonView(ToolbarButton toolbarButton)
69-
{
70-
var button = new Button
71-
{
72-
Style = Styles.GetButtonStyle(ButtonStyle.GhostIconSmall),
73-
HorizontalOptions = LayoutOptions.Center,
74-
};
75-
76-
button.SetBinding(Button.ImageSourceProperty, new Binding(nameof(ToolbarButton.Icon), source: toolbarButton));
77-
button.SetBinding(IsEnabledProperty, new Binding(nameof(ToolbarButton.IsEnabled), source: toolbarButton));
78-
button.SetBinding(Button.CommandProperty, new Binding(nameof(ToolbarButton.Command), source: toolbarButton));
79-
button.SetBinding(Button.CommandParameterProperty, new Binding(nameof(ToolbarButton.CommandParameter), source: toolbarButton));
80-
81-
if (!string.IsNullOrEmpty(toolbarButton.Title))
82-
{
83-
SemanticProperties.SetDescription(button, toolbarButton.Title);
84-
}
85-
86-
return button;
87-
}
8825
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
namespace DIPS.Mobile.UI.Components.Toolbar;
2+
3+
public partial class ToolbarHandler
4+
{
5+
public ToolbarHandler() : base(PropertyMapper)
6+
{
7+
}
8+
9+
public static readonly IPropertyMapper<Toolbar, ToolbarHandler> PropertyMapper =
10+
new PropertyMapper<Toolbar, ToolbarHandler>(ViewMapper)
11+
{
12+
[nameof(Toolbar.Buttons)] = MapButtons,
13+
};
14+
15+
private static partial void MapButtons(ToolbarHandler handler, Toolbar toolbar);
16+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using DIPS.Mobile.UI.Exceptions;
2+
using Microsoft.Maui.Handlers;
3+
4+
namespace DIPS.Mobile.UI.Components.Toolbar;
5+
6+
public partial class ToolbarHandler : ViewHandler<Toolbar, object>
7+
{
8+
protected override object CreatePlatformView() => throw new Only_Here_For_UnitTests();
9+
10+
private static partial void MapButtons(ToolbarHandler handler, Toolbar toolbar) =>
11+
throw new Only_Here_For_UnitTests();
12+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
using DIPS.Mobile.UI.API.Library;
2+
using Microsoft.Maui.Handlers;
3+
using Microsoft.Maui.Platform;
4+
using UIKit;
5+
6+
namespace DIPS.Mobile.UI.Components.Toolbar;
7+
8+
public partial class ToolbarHandler : ViewHandler<Toolbar, UIToolbar>
9+
{
10+
protected override UIToolbar CreatePlatformView()
11+
{
12+
var toolbar = new UIToolbar();
13+
toolbar.BarTintColor = Resources.Colors.Colors.GetColor(ColorName.color_surface_default).ToPlatform();
14+
toolbar.Translucent = false;
15+
return toolbar;
16+
}
17+
18+
protected override void ConnectHandler(UIToolbar platformView)
19+
{
20+
base.ConnectHandler(platformView);
21+
UpdateButtons();
22+
}
23+
24+
private static partial void MapButtons(ToolbarHandler handler, Toolbar toolbar)
25+
{
26+
handler.UpdateButtons();
27+
}
28+
29+
private void UpdateButtons()
30+
{
31+
var items = new List<UIBarButtonItem>();
32+
33+
// Add a flexible space before the first button to help center/distribute them
34+
items.Add(new UIBarButtonItem(UIBarButtonSystemItem.FlexibleSpace));
35+
36+
foreach (var toolbarButton in VirtualView.Buttons)
37+
{
38+
items.Add(CreateBarButtonItem(toolbarButton));
39+
items.Add(new UIBarButtonItem(UIBarButtonSystemItem.FlexibleSpace));
40+
}
41+
42+
PlatformView.SetItems(items.ToArray(), false);
43+
}
44+
45+
private static UIBarButtonItem CreateBarButtonItem(ToolbarButton toolbarButton)
46+
{
47+
UIImage? icon = null;
48+
if (DUI.TryGetUIImageFromImageSource(toolbarButton.Icon, out var uiImage))
49+
{
50+
icon = uiImage?.WithRenderingMode(UIImageRenderingMode.AlwaysTemplate);
51+
}
52+
53+
var item = new UIBarButtonItem(icon, UIBarButtonItemStyle.Plain, (_, _) =>
54+
{
55+
toolbarButton.Command?.Execute(toolbarButton.CommandParameter);
56+
});
57+
58+
item.Enabled = toolbarButton.IsEnabled;
59+
item.TintColor = Resources.Colors.Colors.GetColor(ColorName.color_icon_action).ToPlatform();
60+
61+
if (!string.IsNullOrEmpty(toolbarButton.Title))
62+
{
63+
item.AccessibilityLabel = toolbarButton.Title;
64+
}
65+
66+
return item;
67+
}
68+
}

0 commit comments

Comments
 (0)