diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a5a83283..9fda1551e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## [55.5.0] +- [iOS][BottomSheet] Replaced MAUI SearchBar with native UISearchController for bottom sheet search, integrating the search bar into the navigation bar +- [Android][BottomSheet] Replaced MAUI SearchBar with Material 3 SearchBar and SearchView components for bottom sheet search +- [ContentPage] Added `SearchBehavior` property for native platform search on regular pages (iOS: UISearchController, Android: Material 3 SearchBar + SearchView) + ## [55.4.0] - [iOS][BottomSheet] Use native UINavigationBar for bottom sheet header with centered title, system close/back buttons, and proper blur behavior - [Android][BottomSheet] Fixed edge-to-edge constraints not applying until scroll when start Positioning is Large diff --git a/src/app/Components/ComponentsSamples/BottomSheets/BottomSheetSamples.xaml b/src/app/Components/ComponentsSamples/BottomSheets/BottomSheetSamples.xaml index 966232d2d..c5b902f8c 100644 --- a/src/app/Components/ComponentsSamples/BottomSheets/BottomSheetSamples.xaml +++ b/src/app/Components/ComponentsSamples/BottomSheets/BottomSheetSamples.xaml @@ -25,6 +25,12 @@ + + \ No newline at end of file diff --git a/src/app/Components/ComponentsSamples/BottomSheets/Sheets/BottomSheetWithSearch.xaml b/src/app/Components/ComponentsSamples/BottomSheets/Sheets/BottomSheetWithSearch.xaml new file mode 100644 index 000000000..56bc0e618 --- /dev/null +++ b/src/app/Components/ComponentsSamples/BottomSheets/Sheets/BottomSheetWithSearch.xaml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/app/Components/ComponentsSamples/BottomSheets/Sheets/BottomSheetWithSearch.xaml.cs b/src/app/Components/ComponentsSamples/BottomSheets/Sheets/BottomSheetWithSearch.xaml.cs new file mode 100644 index 000000000..105357a7f --- /dev/null +++ b/src/app/Components/ComponentsSamples/BottomSheets/Sheets/BottomSheetWithSearch.xaml.cs @@ -0,0 +1,9 @@ +namespace Components.ComponentsSamples.BottomSheets.Sheets; + +public partial class BottomSheetWithSearch +{ + public BottomSheetWithSearch() + { + InitializeComponent(); + } +} diff --git a/src/app/Components/ComponentsSamples/BottomSheets/Sheets/BottomSheetWithSearchViewModel.cs b/src/app/Components/ComponentsSamples/BottomSheets/Sheets/BottomSheetWithSearchViewModel.cs new file mode 100644 index 000000000..9308807b5 --- /dev/null +++ b/src/app/Components/ComponentsSamples/BottomSheets/Sheets/BottomSheetWithSearchViewModel.cs @@ -0,0 +1,40 @@ +using System.Windows.Input; +using Components.SampleData; +using DIPS.Mobile.UI.MVVM; + +namespace Components.ComponentsSamples.BottomSheets.Sheets; + +public class BottomSheetWithSearchViewModel : ViewModel +{ + private List m_people; + private readonly List m_originalPeople; + + public BottomSheetWithSearchViewModel() + { + People = SampleDataStorage.People.ToList(); + m_originalPeople = People.ToList(); + SearchCommand = new Command(FilterItems); + } + + private void FilterItems(string filterText) + { + if (string.IsNullOrEmpty(filterText)) + { + People = m_originalPeople.ToList(); + } + else + { + People = m_originalPeople + .Where(p => p.DisplayName.ToLower().Contains(filterText.ToLower())) + .ToList(); + } + } + + public List People + { + get => m_people; + set => RaiseWhenSet(ref m_people, value); + } + + public ICommand SearchCommand { get; } +} diff --git a/src/app/Components/ComponentsSamples/Searching/NativeSearchPageSamples.xaml b/src/app/Components/ComponentsSamples/Searching/NativeSearchPageSamples.xaml new file mode 100644 index 000000000..01068470f --- /dev/null +++ b/src/app/Components/ComponentsSamples/Searching/NativeSearchPageSamples.xaml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/app/Components/ComponentsSamples/Searching/NativeSearchPageSamples.xaml.cs b/src/app/Components/ComponentsSamples/Searching/NativeSearchPageSamples.xaml.cs new file mode 100644 index 000000000..f8a633d15 --- /dev/null +++ b/src/app/Components/ComponentsSamples/Searching/NativeSearchPageSamples.xaml.cs @@ -0,0 +1,9 @@ +namespace Components.ComponentsSamples.Searching; + +public partial class NativeSearchPageSamples +{ + public NativeSearchPageSamples() + { + InitializeComponent(); + } +} diff --git a/src/app/Components/ComponentsSamples/Searching/NativeSearchPageSamplesViewModel.cs b/src/app/Components/ComponentsSamples/Searching/NativeSearchPageSamplesViewModel.cs new file mode 100644 index 000000000..1ef202a62 --- /dev/null +++ b/src/app/Components/ComponentsSamples/Searching/NativeSearchPageSamplesViewModel.cs @@ -0,0 +1,40 @@ +using System.Windows.Input; +using Components.SampleData; +using DIPS.Mobile.UI.MVVM; + +namespace Components.ComponentsSamples.Searching; + +public class NativeSearchPageSamplesViewModel : ViewModel +{ + private List m_people; + private readonly List m_originalPeople; + + public NativeSearchPageSamplesViewModel() + { + People = SampleDataStorage.People.ToList(); + m_originalPeople = People.ToList(); + SearchCommand = new Command(FilterItems); + } + + private void FilterItems(string filterText) + { + if (string.IsNullOrEmpty(filterText)) + { + People = m_originalPeople.ToList(); + } + else + { + People = m_originalPeople + .Where(p => p.DisplayName.ToLower().Contains(filterText.ToLower())) + .ToList(); + } + } + + public List People + { + get => m_people; + set => RaiseWhenSet(ref m_people, value); + } + + public ICommand SearchCommand { get; } +} diff --git a/src/app/Components/ComponentsSamples/Searching/SearchingSamples.xaml b/src/app/Components/ComponentsSamples/Searching/SearchingSamples.xaml index 4117f07f4..fbbae4ebf 100644 --- a/src/app/Components/ComponentsSamples/Searching/SearchingSamples.xaml +++ b/src/app/Components/ComponentsSamples/Searching/SearchingSamples.xaml @@ -11,6 +11,10 @@ HasBottomDivider="True"/> + Command="{helpers:NavigationCommand {x:Type searching:SearchPageSamples}}" + HasBottomDivider="True" /> + + \ No newline at end of file diff --git a/src/library/DIPS.Mobile.UI/Components/BottomSheets/Android/BottomSheetHandler.cs b/src/library/DIPS.Mobile.UI/Components/BottomSheets/Android/BottomSheetHandler.cs index 3e5d0ab41..46b0d0e8e 100644 --- a/src/library/DIPS.Mobile.UI/Components/BottomSheets/Android/BottomSheetHandler.cs +++ b/src/library/DIPS.Mobile.UI/Components/BottomSheets/Android/BottomSheetHandler.cs @@ -19,6 +19,7 @@ using Paint = Android.Graphics.Paint; using System.ComponentModel; using DIPS.Mobile.UI.API.Library; +using DIPS.Mobile.UI.Components.BottomSheets.Android; using DIPS.Mobile.UI.Components.BottomSheets.Header; using SearchBar = DIPS.Mobile.UI.Components.Searching.SearchBar; using View = Microsoft.Maui.Controls.View; @@ -35,7 +36,7 @@ public partial class BottomSheetHandler : ContentViewHandler internal AView? m_bottomBar; private static AView? s_mEmptyNonFitToContentView; - private AView? m_searchBarView; + private BottomSheetSearchField? m_searchField; private BottomSheetHeader m_bottomSheetHeader; private List> m_weakSearchBars = []; private WeakReference? m_weakCurrentFocusedSearchBar; @@ -88,9 +89,15 @@ public AView OnBeforeOpening(IMauiContext mauiContext, Context context, AView bo m_bottomSheetHeader = new BottomSheetHeader(m_bottomSheet); bottomSheetLayout.AddView(m_bottomSheetHeader.ToPlatform(mauiContext)); - m_searchBarView = m_bottomSheet.SearchBar.ToPlatform(mauiContext); - bottomSheetLayout.AddView(m_searchBarView); + // Create native Material 3 search field + m_searchField = new BottomSheetSearchField(context, m_bottomSheet); + bottomSheetLayout.AddView(m_searchField.View); ToggleSearchBar(); + + // Set focus/unfocus actions on the BottomSheet + m_bottomSheet.FocusSearchAction = () => m_searchField?.Focus(); + m_bottomSheet.UnfocusSearchAction = () => m_searchField?.Unfocus(); + FindAndSetupSearchBars(); bottomSheetLayout.AddView(bottomSheetAndroidView); @@ -134,23 +141,15 @@ private void FindAndSetupSearchBars() searchBar.Focused += SearchBarOnFocused; searchBar.Unfocused += SearchBarOnUnfocused; } - - // Also, setup the internal search bar in BottomSheet - if (m_bottomSheet.SearchBar is { } searchBarInternal) - { - searchBarInternal.Focused += SearchBarOnFocused; - searchBarInternal.Unfocused += SearchBarOnUnfocused; - } - } private void ToggleSearchBar() { - if (m_searchBarView == null) + if (m_searchField?.View == null) return; - m_searchBarView.Visibility = m_bottomSheet.HasSearchBar ? ViewStates.Visible : ViewStates.Gone; + m_searchField.View.Visibility = m_bottomSheet.HasSearchBar ? ViewStates.Visible : ViewStates.Gone; } private void SearchBarOnUnfocused(object? sender, EventArgs e) @@ -246,12 +245,11 @@ protected override void DisconnectHandler(ContentViewGroup platformView) searchBar.Unfocused -= SearchBarOnUnfocused; } - // Also, dispose the internal search bar in BottomSheet - if (m_bottomSheet.SearchBar is { } searchBarInternal) - { - searchBarInternal.Focused -= SearchBarOnFocused; - searchBarInternal.Unfocused -= SearchBarOnUnfocused; - } + // Clean up native search field + m_searchField?.Cleanup(); + m_searchField = null; + m_bottomSheet.FocusSearchAction = null; + m_bottomSheet.UnfocusSearchAction = null; } /// diff --git a/src/library/DIPS.Mobile.UI/Components/BottomSheets/Android/BottomSheetSearchField.cs b/src/library/DIPS.Mobile.UI/Components/BottomSheets/Android/BottomSheetSearchField.cs new file mode 100644 index 000000000..50b3feacd --- /dev/null +++ b/src/library/DIPS.Mobile.UI/Components/BottomSheets/Android/BottomSheetSearchField.cs @@ -0,0 +1,124 @@ +using Android.Content; +using Android.Text; +using Android.Views; +using Android.Views.InputMethods; +using Android.Widget; +using AView = Android.Views.View; +using MaterialSearchBar = Google.Android.Material.Search.SearchBar; +using MaterialSearchView = Google.Android.Material.Search.SearchView; + +namespace DIPS.Mobile.UI.Components.BottomSheets.Android; + +/// +/// A Material 3 search component for BottomSheets using the actual +/// and components. +/// The SearchBar provides the collapsed pill-shaped search trigger, +/// while the SearchView provides the expanded search input with EditText. +/// +internal class BottomSheetSearchField : Java.Lang.Object, ITextWatcher, MaterialSearchView.ITransitionListener +{ + private readonly WeakReference m_weakBottomSheet; + private readonly MaterialSearchBar m_searchBar; + private readonly MaterialSearchView m_searchView; + private readonly FrameLayout m_container; + private string m_previousText = string.Empty; + + public BottomSheetSearchField(Context context, BottomSheet bottomSheet) + { + m_weakBottomSheet = new WeakReference(bottomSheet); + + // Create wrapper container + m_container = new FrameLayout(context); + m_container.LayoutParameters = new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MatchParent, + ViewGroup.LayoutParams.WrapContent); + + // Material 3 SearchBar (pill-shaped search trigger) + m_searchBar = new MaterialSearchBar(context); + m_searchBar.LayoutParameters = new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MatchParent, + ViewGroup.LayoutParams.WrapContent); + m_container.AddView(m_searchBar); + + // Material 3 SearchView (expanded search with EditText) + m_searchView = new MaterialSearchView(context); + m_searchView.LayoutParameters = new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MatchParent, + ViewGroup.LayoutParams.MatchParent); + + // Connect SearchView to SearchBar for proper M3 transitions + m_searchView.SetupWithSearchBar(m_searchBar); + + // Listen for text changes on the SearchView's EditText + m_searchView.EditText.AddTextChangedListener(this); + + // Listen for transition events (show/hide) + m_searchView.AddTransitionListener(this); + + m_container.AddView(m_searchView); + } + + public AView View => m_container; + + /// + /// Returns the SearchBar for external layout if needed. + /// + public MaterialSearchBar SearchBar => m_searchBar; + + public void Focus() + { + m_searchView.Show(); + m_searchView.EditText.RequestFocus(); + var imm = (InputMethodManager?)m_searchView.EditText.Context?.GetSystemService(Context.InputMethodService); + imm?.ShowSoftInput(m_searchView.EditText, ShowFlags.Implicit); + } + + public void Unfocus() + { + var imm = (InputMethodManager?)m_searchView.EditText.Context?.GetSystemService(Context.InputMethodService); + imm?.HideSoftInputFromWindow(m_searchView.EditText.WindowToken, 0); + m_searchView.Hide(); + } + + // ITextWatcher implementation + public void AfterTextChanged(IEditable? s) + { + var newText = s?.ToString() ?? string.Empty; + + if (!m_weakBottomSheet.TryGetTarget(out var bottomSheet)) + return; + + bottomSheet.OnNativeSearchTextChanged(newText, m_previousText); + m_previousText = newText; + } + + public void BeforeTextChanged(Java.Lang.ICharSequence? s, int start, int count, int after) + { + } + + public void OnTextChanged(Java.Lang.ICharSequence? s, int start, int before, int count) + { + } + + // MaterialSearchView.ITransitionListener implementation + public void OnStateChanged(MaterialSearchView searchView, MaterialSearchView.TransitionState previousState, MaterialSearchView.TransitionState newState) + { + if (!m_weakBottomSheet.TryGetTarget(out var bottomSheet)) + return; + + if (newState == MaterialSearchView.TransitionState.Shown) + { + bottomSheet.OnSearchFieldFocused(); + } + else if (newState == MaterialSearchView.TransitionState.Hidden) + { + bottomSheet.OnSearchFieldUnfocused(); + } + } + + public void Cleanup() + { + m_searchView.EditText.RemoveTextChangedListener(this); + m_searchView.RemoveTransitionListener(this); + } +} diff --git a/src/library/DIPS.Mobile.UI/Components/BottomSheets/BottomSheet.Properties.cs b/src/library/DIPS.Mobile.UI/Components/BottomSheets/BottomSheet.Properties.cs index b693eb819..f9c427887 100644 --- a/src/library/DIPS.Mobile.UI/Components/BottomSheets/BottomSheet.Properties.cs +++ b/src/library/DIPS.Mobile.UI/Components/BottomSheets/BottomSheet.Properties.cs @@ -63,7 +63,9 @@ public ICommand? OnBackButtonPressedCommand } /// - /// Determines if the bottom sheet should have a at the top + /// Determines if the bottom sheet should have a search bar at the top. + /// On iOS, this uses a native UISearchController integrated with the navigation bar. + /// On Android, this uses a Material 3 styled native search field. /// public bool HasSearchBar { diff --git a/src/library/DIPS.Mobile.UI/Components/BottomSheets/BottomSheet.cs b/src/library/DIPS.Mobile.UI/Components/BottomSheets/BottomSheet.cs index e662c280d..852ec6ee0 100644 --- a/src/library/DIPS.Mobile.UI/Components/BottomSheets/BottomSheet.cs +++ b/src/library/DIPS.Mobile.UI/Components/BottomSheets/BottomSheet.cs @@ -1,8 +1,6 @@ using System.Collections.ObjectModel; using DIPS.Mobile.UI.Components.BottomSheets.Header; using DIPS.Mobile.UI.Internal; -using Colors = Microsoft.Maui.Graphics.Colors; -using SearchBar = DIPS.Mobile.UI.Components.Searching.SearchBar; namespace DIPS.Mobile.UI.Components.BottomSheets { @@ -19,9 +17,6 @@ public BottomSheet() BottombarButtons = new ObservableCollection