Skip to content

Commit 5ab0b2b

Browse files
CopilotVetle444
andcommitted
Replace MAUI SearchBar with native UISearchController (iOS) and Material 3 search field (Android) in BottomSheet
Co-authored-by: Vetle444 <35739538+Vetle444@users.noreply.github.com>
1 parent 71b075d commit 5ab0b2b

10 files changed

Lines changed: 333 additions & 54 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## [55.5.0]
2+
- [iOS][BottomSheet] Replaced MAUI SearchBar with native UISearchController for bottom sheet search, integrating the search bar into the navigation bar
3+
- [Android][BottomSheet] Replaced MAUI SearchBar with native Material 3 styled search field for bottom sheet search
4+
15
## [55.4.0]
26
- [iOS][BottomSheet] Use native UINavigationBar for bottom sheet header with centered title, system close/back buttons, and proper blur behavior
37
- [Android][BottomSheet] Fixed edge-to-edge constraints not applying until scroll when start Positioning is Large

src/library/DIPS.Mobile.UI/Components/BottomSheets/Android/BottomSheetHandler.cs

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
using Paint = Android.Graphics.Paint;
2020
using System.ComponentModel;
2121
using DIPS.Mobile.UI.API.Library;
22+
using DIPS.Mobile.UI.Components.BottomSheets.Android;
2223
using DIPS.Mobile.UI.Components.BottomSheets.Header;
2324
using SearchBar = DIPS.Mobile.UI.Components.Searching.SearchBar;
2425
using View = Microsoft.Maui.Controls.View;
@@ -35,7 +36,7 @@ public partial class BottomSheetHandler : ContentViewHandler
3536
internal AView? m_bottomBar;
3637

3738
private static AView? s_mEmptyNonFitToContentView;
38-
private AView? m_searchBarView;
39+
private BottomSheetSearchField? m_searchField;
3940
private BottomSheetHeader m_bottomSheetHeader;
4041
private List<WeakReference<SearchBar>> m_weakSearchBars = [];
4142
private WeakReference<AView>? m_weakCurrentFocusedSearchBar;
@@ -88,9 +89,15 @@ public AView OnBeforeOpening(IMauiContext mauiContext, Context context, AView bo
8889
m_bottomSheetHeader = new BottomSheetHeader(m_bottomSheet);
8990
bottomSheetLayout.AddView(m_bottomSheetHeader.ToPlatform(mauiContext));
9091

91-
m_searchBarView = m_bottomSheet.SearchBar.ToPlatform(mauiContext);
92-
bottomSheetLayout.AddView(m_searchBarView);
92+
// Create native Material 3 search field
93+
m_searchField = new BottomSheetSearchField(context, m_bottomSheet);
94+
bottomSheetLayout.AddView(m_searchField.View);
9395
ToggleSearchBar();
96+
97+
// Set focus/unfocus actions on the BottomSheet
98+
m_bottomSheet.FocusSearchAction = () => m_searchField?.Focus();
99+
m_bottomSheet.UnfocusSearchAction = () => m_searchField?.Unfocus();
100+
94101
FindAndSetupSearchBars();
95102

96103
bottomSheetLayout.AddView(bottomSheetAndroidView);
@@ -134,23 +141,15 @@ private void FindAndSetupSearchBars()
134141
searchBar.Focused += SearchBarOnFocused;
135142
searchBar.Unfocused += SearchBarOnUnfocused;
136143
}
137-
138-
// Also, setup the internal search bar in BottomSheet
139-
if (m_bottomSheet.SearchBar is { } searchBarInternal)
140-
{
141-
searchBarInternal.Focused += SearchBarOnFocused;
142-
searchBarInternal.Unfocused += SearchBarOnUnfocused;
143-
}
144-
145144
}
146145

147146

148147
private void ToggleSearchBar()
149148
{
150-
if (m_searchBarView == null)
149+
if (m_searchField?.View == null)
151150
return;
152151

153-
m_searchBarView.Visibility = m_bottomSheet.HasSearchBar ? ViewStates.Visible : ViewStates.Gone;
152+
m_searchField.View.Visibility = m_bottomSheet.HasSearchBar ? ViewStates.Visible : ViewStates.Gone;
154153
}
155154

156155
private void SearchBarOnUnfocused(object? sender, EventArgs e)
@@ -246,12 +245,11 @@ protected override void DisconnectHandler(ContentViewGroup platformView)
246245
searchBar.Unfocused -= SearchBarOnUnfocused;
247246
}
248247

249-
// Also, dispose the internal search bar in BottomSheet
250-
if (m_bottomSheet.SearchBar is { } searchBarInternal)
251-
{
252-
searchBarInternal.Focused -= SearchBarOnFocused;
253-
searchBarInternal.Unfocused -= SearchBarOnUnfocused;
254-
}
248+
// Clean up native search field
249+
m_searchField?.Cleanup();
250+
m_searchField = null;
251+
m_bottomSheet.FocusSearchAction = null;
252+
m_bottomSheet.UnfocusSearchAction = null;
255253
}
256254

257255
/// <summary>
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
using Android.Content;
2+
using Android.Graphics.Drawables;
3+
using Android.Text;
4+
using Android.Views;
5+
using Android.Views.InputMethods;
6+
using Android.Widget;
7+
using Microsoft.Maui.Platform;
8+
using AView = Android.Views.View;
9+
using Colors = DIPS.Mobile.UI.Resources.Colors.Colors;
10+
11+
namespace DIPS.Mobile.UI.Components.BottomSheets.Android;
12+
13+
/// <summary>
14+
/// A native Material 3 styled search field for use in BottomSheets.
15+
/// Uses an EditText with rounded background, search icon, and clear button
16+
/// to match the Material 3 search bar design.
17+
/// </summary>
18+
internal class BottomSheetSearchField : Java.Lang.Object, ITextWatcher, AView.IOnFocusChangeListener
19+
{
20+
private readonly WeakReference<BottomSheet> m_weakBottomSheet;
21+
private readonly EditText m_editText;
22+
private readonly FrameLayout m_container;
23+
private readonly ImageView m_searchIcon;
24+
private readonly ImageView m_clearButton;
25+
private string m_previousText = string.Empty;
26+
27+
public BottomSheetSearchField(Context context, BottomSheet bottomSheet)
28+
{
29+
m_weakBottomSheet = new WeakReference<BottomSheet>(bottomSheet);
30+
31+
var density = context.Resources?.DisplayMetrics?.Density ?? 1f;
32+
33+
// Create the outer container with M3 search bar styling
34+
m_container = new FrameLayout(context);
35+
var containerParams = new LinearLayout.LayoutParams(
36+
ViewGroup.LayoutParams.MatchParent,
37+
(int)(56 * density)) // M3 search bar height is 56dp
38+
{
39+
LeftMargin = (int)(16 * density),
40+
RightMargin = (int)(16 * density),
41+
TopMargin = (int)(8 * density),
42+
BottomMargin = (int)(8 * density)
43+
};
44+
m_container.LayoutParameters = containerParams;
45+
46+
// Rounded background matching M3 search bar (pill shape)
47+
var background = new GradientDrawable();
48+
background.SetShape(ShapeType.Rectangle);
49+
background.SetCornerRadius(28 * density);
50+
background.SetColor(Colors.GetColor(ColorName.color_surface_elevated).ToPlatform());
51+
m_container.Background = background;
52+
53+
// Search icon
54+
m_searchIcon = new ImageView(context);
55+
m_searchIcon.SetImageResource(global::Android.Resource.Drawable.IcMenuSearch);
56+
m_searchIcon.SetColorFilter(Colors.GetColor(ColorName.color_icon_default).ToPlatform());
57+
var searchIconParams = new FrameLayout.LayoutParams(
58+
(int)(24 * density),
59+
(int)(24 * density),
60+
GravityFlags.CenterVertical | GravityFlags.Start);
61+
searchIconParams.LeftMargin = (int)(16 * density);
62+
m_searchIcon.LayoutParameters = searchIconParams;
63+
m_container.AddView(m_searchIcon);
64+
65+
// EditText
66+
m_editText = new EditText(context);
67+
m_editText.SetHintTextColor(Colors.GetColor(ColorName.color_text_subtle).ToPlatform());
68+
m_editText.SetTextColor(Colors.GetColor(ColorName.color_text_default).ToPlatform());
69+
m_editText.Background = null; // Transparent - container handles background
70+
m_editText.SetSingleLine(true);
71+
m_editText.ImeOptions = ImeAction.Search;
72+
m_editText.InputType = InputTypes.ClassText | InputTypes.TextFlagNoSuggestions;
73+
var editTextParams = new FrameLayout.LayoutParams(
74+
ViewGroup.LayoutParams.MatchParent,
75+
ViewGroup.LayoutParams.MatchParent);
76+
editTextParams.LeftMargin = (int)(48 * density); // After search icon
77+
editTextParams.RightMargin = (int)(48 * density); // Before clear button
78+
m_editText.LayoutParameters = editTextParams;
79+
m_editText.SetPadding(0, 0, 0, 0);
80+
m_editText.AddTextChangedListener(this);
81+
m_editText.OnFocusChangeListener = this;
82+
m_container.AddView(m_editText);
83+
84+
// Clear button
85+
m_clearButton = new ImageView(context);
86+
m_clearButton.SetImageResource(global::Android.Resource.Drawable.IcMenuCloseClearCancel);
87+
m_clearButton.SetColorFilter(Colors.GetColor(ColorName.color_icon_default).ToPlatform());
88+
m_clearButton.Visibility = ViewStates.Gone;
89+
m_clearButton.Clickable = true;
90+
m_clearButton.Focusable = true;
91+
var clearParams = new FrameLayout.LayoutParams(
92+
(int)(24 * density),
93+
(int)(24 * density),
94+
GravityFlags.CenterVertical | GravityFlags.End);
95+
clearParams.RightMargin = (int)(16 * density);
96+
m_clearButton.LayoutParameters = clearParams;
97+
m_clearButton.Click += OnClearButtonClicked;
98+
m_container.AddView(m_clearButton);
99+
}
100+
101+
public AView View => m_container;
102+
103+
public void Focus()
104+
{
105+
m_editText.RequestFocus();
106+
var imm = (InputMethodManager?)m_editText.Context?.GetSystemService(Context.InputMethodService);
107+
imm?.ShowSoftInput(m_editText, ShowFlags.Implicit);
108+
}
109+
110+
public void Unfocus()
111+
{
112+
m_editText.ClearFocus();
113+
var imm = (InputMethodManager?)m_editText.Context?.GetSystemService(Context.InputMethodService);
114+
imm?.HideSoftInputFromWindow(m_editText.WindowToken, 0);
115+
}
116+
117+
private void OnClearButtonClicked(object? sender, EventArgs e)
118+
{
119+
m_editText.Text = string.Empty;
120+
}
121+
122+
// ITextWatcher implementation
123+
public void AfterTextChanged(IEditable? s)
124+
{
125+
var newText = s?.ToString() ?? string.Empty;
126+
m_clearButton.Visibility = string.IsNullOrEmpty(newText) ? ViewStates.Gone : ViewStates.Visible;
127+
128+
if (!m_weakBottomSheet.TryGetTarget(out var bottomSheet))
129+
return;
130+
131+
bottomSheet.OnNativeSearchTextChanged(newText, m_previousText);
132+
m_previousText = newText;
133+
}
134+
135+
public void BeforeTextChanged(Java.Lang.ICharSequence? s, int start, int count, int after)
136+
{
137+
}
138+
139+
public void OnTextChanged(Java.Lang.ICharSequence? s, int start, int before, int count)
140+
{
141+
}
142+
143+
// IOnFocusChangeListener implementation
144+
public void OnFocusChange(AView? v, bool hasFocus)
145+
{
146+
if (!m_weakBottomSheet.TryGetTarget(out var bottomSheet))
147+
return;
148+
149+
if (hasFocus)
150+
{
151+
bottomSheet.OnSearchFieldFocused();
152+
}
153+
else
154+
{
155+
bottomSheet.OnSearchFieldUnfocused();
156+
}
157+
}
158+
159+
public void Cleanup()
160+
{
161+
m_editText.RemoveTextChangedListener(this);
162+
m_editText.OnFocusChangeListener = null;
163+
m_clearButton.Click -= OnClearButtonClicked;
164+
}
165+
}

src/library/DIPS.Mobile.UI/Components/BottomSheets/BottomSheet.Properties.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,9 @@ public ICommand? OnBackButtonPressedCommand
6363
}
6464

6565
/// <summary>
66-
/// Determines if the bottom sheet should have a <see cref="Components.Searching.SearchBar"/> at the top
66+
/// Determines if the bottom sheet should have a search bar at the top.
67+
/// On iOS, this uses a native UISearchController integrated with the navigation bar.
68+
/// On Android, this uses a Material 3 styled native search field.
6769
/// </summary>
6870
public bool HasSearchBar
6971
{

src/library/DIPS.Mobile.UI/Components/BottomSheets/BottomSheet.cs

Lines changed: 53 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
using System.Collections.ObjectModel;
22
using DIPS.Mobile.UI.Components.BottomSheets.Header;
33
using DIPS.Mobile.UI.Internal;
4-
using Colors = Microsoft.Maui.Graphics.Colors;
5-
using SearchBar = DIPS.Mobile.UI.Components.Searching.SearchBar;
64

75
namespace DIPS.Mobile.UI.Components.BottomSheets
86
{
@@ -19,9 +17,6 @@ public BottomSheet()
1917
BottombarButtons = new ObservableCollection<Button>();
2018

2119
BottomSheetHeaderBehavior = new BottomSheetHeaderBehavior();
22-
23-
SearchBar = new SearchBar { AutomationId = "SearchBar".ToDUIAutomationId<BottomSheet>(), HasCancelButton = false, BackgroundColor = Colors.Transparent, ReturnKeyType = ReturnType.Done };
24-
SearchBar.TextChanged += OnSearchTextChanged;
2520
}
2621

2722
/// <summary>
@@ -43,7 +38,55 @@ public Task Open()
4338
return BottomSheetService.Open(this);
4439
}
4540

46-
internal SearchBar SearchBar { get; private set; }
41+
/// <summary>
42+
/// Action delegate for platform-specific search focus. Set by platform handlers/controllers.
43+
/// </summary>
44+
internal Action? FocusSearchAction { get; set; }
45+
46+
/// <summary>
47+
/// Action delegate for platform-specific search unfocus. Set by platform handlers/controllers.
48+
/// </summary>
49+
internal Action? UnfocusSearchAction { get; set; }
50+
51+
/// <summary>
52+
/// Focuses the native search field.
53+
/// </summary>
54+
internal void FocusSearch() => FocusSearchAction?.Invoke();
55+
56+
/// <summary>
57+
/// Unfocuses the native search field.
58+
/// </summary>
59+
internal void UnfocusSearch() => UnfocusSearchAction?.Invoke();
60+
61+
/// <summary>
62+
/// Called by native search controllers when the search text changes.
63+
/// Routes the change through the existing events and virtual method.
64+
/// </summary>
65+
internal void OnNativeSearchTextChanged(string newValue, string oldValue)
66+
{
67+
SearchTextChanged?.Invoke(this, new TextChangedEventArgs(oldValue, newValue));
68+
SearchCommand?.Execute(newValue);
69+
OnSearchTextChanged(newValue);
70+
}
71+
72+
/// <summary>
73+
/// Called by the native search field when it gains focus.
74+
/// </summary>
75+
internal void OnSearchFieldFocused()
76+
{
77+
if (Positioning is Positioning.Large)
78+
return;
79+
80+
Positioning = Positioning.Large;
81+
}
82+
83+
/// <summary>
84+
/// Called by the native search field when it loses focus.
85+
/// </summary>
86+
internal void OnSearchFieldUnfocused()
87+
{
88+
Positioning = Positioning.Medium;
89+
}
4790

4891
internal void SendClose()
4992
{
@@ -59,13 +102,6 @@ internal void SendOpen()
59102
OnOpened();
60103
}
61104

62-
private void OnSearchTextChanged(object? sender, TextChangedEventArgs args)
63-
{
64-
SearchTextChanged?.Invoke(SearchBar, args);
65-
SearchCommand?.Execute(args.NewTextValue);
66-
OnSearchTextChanged(args.NewTextValue);
67-
}
68-
69105
protected virtual void OnClosed()
70106
{
71107
}
@@ -97,7 +133,7 @@ public Grid CreateBottomBar()
97133
},
98134
SafeAreaEdges = SafeAreaEdges.None
99135
};
100-
136+
101137
foreach (var button in BottombarButtons)
102138
{
103139
grid.AddColumnDefinition(new ColumnDefinition(GridLength.Star));
@@ -116,8 +152,9 @@ protected override void OnHandlerChanging(HandlerChangingEventArgs args)
116152
base.OnHandlerChanging(args);
117153
if (args.NewHandler == null) //Disconnect
118154
{
119-
SearchBar.TextChanged -= OnSearchTextChanged;
155+
FocusSearchAction = null;
156+
UnfocusSearchAction = null;
120157
}
121158
}
122159
}
123-
}
160+
}

src/library/DIPS.Mobile.UI/Components/BottomSheets/BottomSheetService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public static async Task Open(BottomSheet bottomSheet)
2828
if (shouldFocusSearchBarOnOpen)
2929
{
3030
await Task.Delay(1);
31-
bottomSheet.SearchBar.Focus();
31+
bottomSheet.FocusSearch();
3232
}
3333
}
3434

0 commit comments

Comments
 (0)